在RecyclerView中的行内单击处理按钮


81

我正在使用以下代码来处理行点击。(来源

static class RecyclerTouchListener implements RecyclerView.OnItemTouchListener {

    private GestureDetector gestureDetector;
    private ClickListener clickListener;

    public RecyclerTouchListener(Context context, final RecyclerView recyclerView, final ClickListener clickListener) {
        this.clickListener = clickListener;
        gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
            @Override
            public boolean onSingleTapUp(MotionEvent e) {
                return true;
            }

            @Override
            public void onLongPress(MotionEvent e) {
                View child = recyclerView.findChildViewUnder(e.getX(), e.getY());
                if (child != null && clickListener != null) {
                    clickListener.onLongClick(child, recyclerView.getChildPosition(child));
                }
            }
        });
    }

    @Override
    public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {

        View child = rv.findChildViewUnder(e.getX(), e.getY());
        if (child != null && clickListener != null && gestureDetector.onTouchEvent(e)) {
            clickListener.onClick(child, rv.getChildPosition(child));
        }
        return false;
    }

    @Override
    public void onTouchEvent(RecyclerView rv, MotionEvent e) {
    }
}

但是,如果我想在每行上说一个删除按钮,则此方法有效。我不确定如何用此实现。

我在OnClick侦听器上附加了“删除”按钮,该按钮可以工作(删除行),但也会触发整行的onclick。

如果单击单个按钮,有人可以帮助我如何避免全行单击。

谢谢。

Answers:


128

这是我如何处理recyclerView中的多个onClick事件:

编辑:已更新为包括回调(如其他注释中所述)。我已经使用了一个WeakReferenceViewHolder消除潜在的内存泄漏。

定义接口:

public interface ClickListener {

    void onPositionClicked(int position);
    
    void onLongClicked(int position);
}

然后适配器:

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {
    
    private final ClickListener listener;
    private final List<MyItems> itemsList;

    public MyAdapter(List<MyItems> itemsList, ClickListener listener) {
        this.listener = listener;
        this.itemsList = itemsList;
    }

    @Override public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return new MyViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.my_row_layout), parent, false), listener);
    }

    @Override public void onBindViewHolder(MyViewHolder holder, int position) {
        // bind layout and data etc..
    }

    @Override public int getItemCount() {
        return itemsList.size();
    }

    public static class MyViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {

        private ImageView iconImageView;
        private TextView iconTextView;
        private WeakReference<ClickListener> listenerRef;

        public MyViewHolder(final View itemView, ClickListener listener) {
            super(itemView);

            listenerRef = new WeakReference<>(listener);
            iconImageView = (ImageView) itemView.findViewById(R.id.myRecyclerImageView);
            iconTextView = (TextView) itemView.findViewById(R.id.myRecyclerTextView);

            itemView.setOnClickListener(this);
            iconTextView.setOnClickListener(this);
            iconImageView.setOnLongClickListener(this);
        }

        // onClick Listener for view
        @Override
        public void onClick(View v) {

            if (v.getId() == iconTextView.getId()) {
                Toast.makeText(v.getContext(), "ITEM PRESSED = " + String.valueOf(getAdapterPosition()), Toast.LENGTH_SHORT).show();
            } else {
                Toast.makeText(v.getContext(), "ROW PRESSED = " + String.valueOf(getAdapterPosition()), Toast.LENGTH_SHORT).show();
            }
            
            listenerRef.get().onPositionClicked(getAdapterPosition());
        }


        //onLongClickListener for view
        @Override
        public boolean onLongClick(View v) {

            final AlertDialog.Builder builder = new AlertDialog.Builder(v.getContext());
            builder.setTitle("Hello Dialog")
                    .setMessage("LONG CLICK DIALOG WINDOW FOR ICON " + String.valueOf(getAdapterPosition()))
                    .setPositiveButton("OK", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {

                        }
                    });

            builder.create().show();
            listenerRef.get().onLongClicked(getAdapterPosition());
            return true;
        }
    }
}

然后在您的活动/片段中-您可以实现的方式:Clicklistener-或匿名类(如果您愿意):

MyAdapter adapter = new MyAdapter(myItems, new ClickListener() {
            @Override public void onPositionClicked(int position) {
                // callback performed on click
            }

            @Override public void onLongClicked(int position) {
                // callback performed on click
            }
        });

要获得单击的项目,请匹配视图ID ievgetId()== everythingItem.getId()

希望这种方法有所帮助!


1
谢谢,我只在实现时使用了这种模式。但是问题是另外一回事。看看这里stackoverflow.com/questions/30287411/...
Ashwaniķ

1
“这个”从哪里被提取?除非您匹配上下文,否则适配器内没有此对象,而当我出于某种原因这样做时,它也正在对其进行Cast View.OnclickListener
Lion789

2
this在引用自身,即ViewHolder(在这种情况下,这是一个单独的静态类)-您正在将ViewViewolder绑定到其上的数据的Viewholder上设置侦听器onBindViewHolder(),它与Adapter中的上下文无关。我不知道您究竟遇到了什么问题,但是这种解决方案效果很好。
Mark Keen

1
@YasithaChinthaka您是否尝试过android:background="?attr/selectableItemBackground"在视图的xml中设置此属性:
Mark Keen

2
只是想说谢谢,这个解决方案非常容易遵循和实施。
CodeGeass

50

我通常会发现:

  • 我需要使用多个侦听器,因为我有多个按钮。
  • 我希望我的逻辑出现在活动中,而不是适配器或Viewholder中。

因此,@ mark-keen的答案很好用,但是拥有一个接口可以提供更大的灵活性:

public static class MyViewHolder extends RecyclerView.ViewHolder {

    public ImageView iconImageView;
    public TextView iconTextView;

    public MyViewHolder(final View itemView) {
        super(itemView);

        iconImageView = (ImageView) itemView.findViewById(R.id.myRecyclerImageView);
        iconTextView = (TextView) itemView.findViewById(R.id.myRecyclerTextView);

        iconTextView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                onClickListener.iconTextViewOnClick(v, getAdapterPosition());
            }
        });
        iconImageView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                onClickListener.iconImageViewOnClick(v, getAdapterPosition());
            }
        });
    }
}

在适配器中定义onClickListener的位置:

public MyAdapterListener onClickListener;

public interface MyAdapterListener {

    void iconTextViewOnClick(View v, int position);
    void iconImageViewOnClick(View v, int position);
}

并可能通过您的构造函数进行设置:

public MyAdapter(ArrayList<MyListItems> newRows, MyAdapterListener listener) {

    rows = newRows;
    onClickListener = listener;
}

然后,您可以在“活动”中或使用RecyclerView的任何地方处理事件:

mAdapter = new MyAdapter(mRows, new MyAdapter.MyAdapterListener() {
                    @Override
                    public void iconTextViewOnClick(View v, int position) {
                        Log.d(TAG, "iconTextViewOnClick at position "+position);
                    }

                    @Override
                    public void iconImageViewOnClick(View v, int position) {
                        Log.d(TAG, "iconImageViewOnClick at position "+position);
                    }
                });
mRecycler.setAdapter(mAdapter);

这是另一种方式,它建立在我的答案之上(我使用的是类似的答案),但是如何传递onClickListener给静态嵌套的Viewholder类?除非我缺少任何东西,否则我看不到如何将其传递给ViewHolder。同样,如果仅使用一种接口方法,则可以使用Lambda表达式,该表达式将所有内容压缩。
马克·基恩

公共MyAdapterListener onClickListener; 是在上面的代码中的适配器中定义并在适配器构造函数中设置的成员变量。(或者,但上面未显示,您也可以使用自定义设置器,如setOnClickListener。)
LordParsley16年

5
我只是问您如何从静态嵌套类(ViewHolder)中访问Adapter类中的成员/实例变量。
马克·基恩

像Mark一样,我无法嵌套在适配器中。看到我关于如何避免嵌套的答案
Tony BenBrahim

@MarkKeen ..完全一样的问题。
user2695433

6

我想要一个不会产生任何额外影响的解决方案对象(即,侦听器),这些对象以后必须进行垃圾收集,并且不需要在适配器类中嵌套视图持有者。

ViewHolder班上

private static class MyViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {

        private final TextView ....// declare the fields in your view
        private ClickHandler ClickHandler;

        public MyHolder(final View itemView) {
            super(itemView);
            nameField = (TextView) itemView.findViewById(R.id.name);
            //find other fields here...
            Button myButton = (Button) itemView.findViewById(R.id.my_button);
            myButton.setOnClickListener(this);
        }
        ...
        @Override
        public void onClick(final View view) {
            if (clickHandler != null) {
                clickHandler.onMyButtonClicked(getAdapterPosition());
            }
        }

注意事项:ClickHandler接口已定义,但此处未初始化,因此在接口中没有假设onClick方法中曾经被初始化过。

ClickHandler界面如下所示:

private interface ClickHandler {
    void onMyButtonClicked(final int position);
} 

在适配器中,在构造函数中设置“ ClickHandler”的实例,并覆盖onBindViewHolder,以在视图持有人上初始化“ clickHandler”:

private class MyAdapter extends ...{

    private final ClickHandler clickHandler;

    public MyAdapter(final ClickHandler clickHandler) {
        super(...);
        this.clickHandler = clickHandler;
    }

    @Override
    public void onBindViewHolder(final MyViewHolder viewHolder, final int position) {
        super.onBindViewHolder(viewHolder, position);
        viewHolder.clickHandler = this.clickHandler;
    }

注意:我知道viewHolder.clickHandler可能会被设置多次且具有完全相同的值,但这比检查null和分支要便宜,而且没有内存成本,只是一条额外的指令。

最后,在创建适配器时,必须将ClickHandler实例传递给构造函数,如下所示:

adapter = new MyAdapter(new ClickHandler() {
    @Override
    public void onMyButtonClicked(final int position) {
        final MyModel model = adapter.getItem(position);
        //do something with the model where the button was clicked
    }
});

请注意,adapter这里是成员变量,而不是局部变量


谢谢您的回答:)我想在此处添加的一件事不要使用adapter.getItem(position)而不是使用yourmodel.get(position)
库贝卜·拉扎

5

如果您已经有一个回收站触摸侦听器并且想要处理其中的所有触摸事件,而不是在视图持有器中单独处理按钮触摸事件,则只想添加另一个解决方案。该类的改编版所做的关键操作是在点按onItemClick()回调时返回按钮视图,而不是item容器。然后,您可以测试视图是否为按钮,并执行其他操作。请注意,长按该按钮将被解释为长按整行。

public class RecyclerItemClickListener implements RecyclerView.OnItemTouchListener
{
    public static interface OnItemClickListener
    {
        public void onItemClick(View view, int position);
        public void onItemLongClick(View view, int position);
    }

    private OnItemClickListener mListener;
    private GestureDetector mGestureDetector;

    public RecyclerItemClickListener(Context context, final RecyclerView recyclerView, OnItemClickListener listener)
    {
        mListener = listener;

        mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener()
        {
            @Override
            public boolean onSingleTapUp(MotionEvent e)
            {
                // Important: x and y are translated coordinates here
                final ViewGroup childViewGroup = (ViewGroup) recyclerView.findChildViewUnder(e.getX(), e.getY());

                if (childViewGroup != null && mListener != null) {
                    final List<View> viewHierarchy = new ArrayList<View>();
                    // Important: x and y are raw screen coordinates here
                    getViewHierarchyUnderChild(childViewGroup, e.getRawX(), e.getRawY(), viewHierarchy);

                    View touchedView = childViewGroup;
                    if (viewHierarchy.size() > 0) {
                        touchedView = viewHierarchy.get(0);
                    }
                    mListener.onItemClick(touchedView, recyclerView.getChildPosition(childViewGroup));
                    return true;
                }

                return false;
            }

            @Override
            public void onLongPress(MotionEvent e)
            {
                View childView = recyclerView.findChildViewUnder(e.getX(), e.getY());

                if(childView != null && mListener != null)
                {
                    mListener.onItemLongClick(childView, recyclerView.getChildPosition(childView));
                }
            }
        });
    }

    public void getViewHierarchyUnderChild(ViewGroup root, float x, float y, List<View> viewHierarchy) {
        int[] location = new int[2];
        final int childCount = root.getChildCount();

        for (int i = 0; i < childCount; ++i) {
            final View child = root.getChildAt(i);
            child.getLocationOnScreen(location);
            final int childLeft = location[0], childRight = childLeft + child.getWidth();
            final int childTop = location[1], childBottom = childTop + child.getHeight();

            if (child.isShown() && x >= childLeft && x <= childRight && y >= childTop && y <= childBottom) {
                viewHierarchy.add(0, child);
            }
            if (child instanceof ViewGroup) {
                getViewHierarchyUnderChild((ViewGroup) child, x, y, viewHierarchy);
            }
        }
    }

    @Override
    public boolean onInterceptTouchEvent(RecyclerView view, MotionEvent e)
    {
        mGestureDetector.onTouchEvent(e);

        return false;
    }

    @Override
    public void onTouchEvent(RecyclerView view, MotionEvent motionEvent){}

    @Override
    public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {

    }
}

然后从活动/片段中使用它:

recyclerView.addOnItemTouchListener(createItemClickListener(recyclerView));

    public RecyclerItemClickListener createItemClickListener(final RecyclerView recyclerView) {
        return new RecyclerItemClickListener (context, recyclerView, new RecyclerItemClickListener.OnItemClickListener() {
            @Override
            public void onItemClick(View view, int position) {
                if (view instanceof AppCompatButton) {
                    // ... tapped on the button, so go do something
                } else {
                    // ... tapped on the item container (row), so do something different
                }
            }

            @Override
            public void onItemLongClick(View view, int position) {
            }
        });
    }

1

onInterceptTouchEvent()处理点击事件时,您需要在内部返回true 。


1
嗨,您能详细说明一下吗?我正在使用以下代码绑定到删除按钮btnDelete =(ImageButton)itemView.findViewById(R.id.btnDelete); btnDelete.setOnClickListener(new View.OnClickListener(){@Override public void onClick(View view){remove(getLayoutPosition());}});
Ashwani K

像onTouchEvent()一样,返回值是指示事件是否已被处理,以及何时未将事件传递给完整行。
Eliyahu Shwartz,2015年


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.