RecyclerView does not provide an item click event listener and can only handle it itself.

This is the first of a series of long knowledge of read source code, which is characterized by the application of design ideas in the source code to real projects, the list of articles in the series is as follows:

  1. Read the source code long knowledge better RecyclerView | click listener

  2. Android custom controls | source there is treasure in the automatic line feed control

  3. Android custom controls | three implementation of little red dot (below)

  4. Reading knowledge source long | dynamic extension class and bind the new way of the life cycle

  5. Reading knowledge source long | Android caton true because “frame”?

Plan 1: Layer by layer pass the click-listening callback

The RecyclerView entry is held by the ViewHolder, which is built in the RecyclerView.Adapter. So the most direct solution is to inject the click event callback into recyclerView. Adapter, pass it to ViewHolder, and set view.onClickListener for itemView:

//' Define click callback '
public interface OnItemClickListener {
    void onItemClick(int position);
}
Copy the code

Let the Adapter hold interfaces:

public class MyAdapter extends RecyclerView.Adapter<MyViewHolder> {
    //' hold interface '
    private OnItemClickListener onItemClickListener;
    
    //' inject interface '
    public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
        this.onItemClickListener = onItemClickListener;
    }

    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.grid_item, null);
        return new MyViewHolder(view);
    }

    //' Pass the interface to ViewHolder'
    @Override
    public void onBindViewHolder(MyViewHolder holder, int position) { holder.bind(onItemClickListener); }}Copy the code

The interface can then be called from the ViewHolder:

public class MyViewHolder extends RecyclerView.ViewHolder {
    public MyViewHolder(View itemView) {
        super(itemView);
    }

    public void bind(final OnItemClickListener onItemClickListener){
        //' Set click event for ItemView '
        itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if(onItemClickListener ! =null) { onItemClickListener.onItemClick(getAdapterPosition()); }}}); }}Copy the code

The advantage of this scheme is that it is easy to understand, but the disadvantage is that the interface for the click event is passed through multiple parties: to set up the click event for the itemView, you need to pass the ViewHolder and Adapter (because you can’t get the itemView directly). This makes them coupled to the click-event interface, and if the click-event interface changes, both classes need to change with it.

Another disadvantage is that there are N additional OnClickListener objects in memory (N is the number of entries on a screen). It’s not a huge expense though. Furthermore, onBindViewHolder() will fire multiple times as the list is scrolled, resulting in needlessly setting click listeners for the same entry multiple times.

Setting a click listener in onBindViewHolder() is also a bug because the “snapshot mechanism”, in which the index value passed as an argument to onItemClick() is a snapshot generated at the moment onBindViewHolder() is called, If data is added or deleted, but for some reason the view at the corresponding location is not refreshed in time (onBindViewHolder() is not called again), then the click event will fetch the wrong index.

Is there a more decoupled scenario where all entries share a click event listener?

ListView source code for the answer

Suddenly thought of ListView. The setOnItemClickListener (), is this all item table sharing a listener? See how it works:

    /** * Interface definition for a callback to be invoked when an item in this * AdapterView has been clicked. */
    public interface OnItemClickListener {
        /** * Callback method to be invoked when an item in this AdapterView has been clicked. * 'The second parameter is the clicked entry' *@paramView The view within The AdapterView that was clicked * 'The third parameter is The adapter position of The clicked entry' *@param position The position of the view in the adapter.
         */
        void onItemClick(AdapterView<? > parent, View view,int position, long id);
    }
    
    /** * 'inject table click listener' */
    public void setOnItemClickListener(@Nullable OnItemClickListener listener) {
        mOnItemClickListener = listener;
    }
Copy the code

This is the table click Listener interface defined in the ListView. Instances of the interface are injected via setOnItemClickListener() and stored in mOnItemClickListener.

Interface parameters include the click entry View and its adapter index. Curious how these two parameters are generated from the click event? Look up the call chain along mOnItemClickListener:

    public boolean performItemClick(View view, int position, long id) {
        final boolean result;
        if(mOnItemClickListener ! =null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            //' call click event listener '
            mOnItemClickListener.onItemClick(this, view, position, id);
            result = true;
        } else {
            result = false;
        }

        if(view ! =null) {
            view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        }
        return result;
    }
Copy the code

MOnItemClickListener is called only in performItemClick(View View, int Position, long ID), and continues up the call chain to find out how the first argument View is generated:

    private class PerformClick extends WindowRunnnable implements Runnable {
        // select the index of the selected entry.
        int mClickMotionPosition;

        @Override
        public void run(a) {
            if (mDataChanged) return;
            final ListAdapter adapter = mAdapter;
            final int motionPosition = mClickMotionPosition;
            if(adapter ! =null && mItemCount > 0&& motionPosition ! = INVALID_POSITION && motionPosition < adapter.getCount() && sameWindow() && adapter.isEnabled(motionPosition)) {//' Locate the View by motionPosition index value '
                final View view = getChildAt(motionPosition - mFirstPosition);
                if(view ! =null) { performItemClick(view, motionPosition, adapter.getItemId(motionPosition)); }}}}Copy the code

The view that is clicked is obtained by getChildAt(index). Search all PerformClick mClickMotionPosition assigned place:

public abstract class AbsListView extends AdapterView<ListAdapter>{
    /** * 'The position of The view that received The down motion event */
    int mMotionPosition;
    
    private void onTouchUp(MotionEvent ev) {
        switch (mTouchMode) {
        case TOUCH_MODE_DOWN:
        case TOUCH_MODE_TAP:
        case TOUCH_MODE_DONE_WAITING:
            / / 'be AbsListView. MMotionPosition assignment'
            final int motionPosition = mMotionPosition;
            final View child = getChildAt(motionPosition - mFirstPosition);
            if(child ! =null) {
                if(mTouchMode ! = TOUCH_MODE_DOWN) { child.setPressed(false);
                }

                final float x = ev.getX();
                final boolean inList = x > mListPadding.left && x < getWidth() - mListPadding.right;
                if(inList && ! child.hasExplicitFocusable()) {if (mPerformClick == null) {
                        mPerformClick = new PerformClick();
                    }

                    final AbsListView.PerformClick performClick = mPerformClick;
                    / / 'be AbsListView. MMotionPosition assignment'performClick.mClickMotionPosition = motionPosition; . }}Copy the code

PerformClick. MClickMotionPosition assigned place only one, in AbsListView. OnTouchUp () be AbsListView. MMotionPosition assignment, look at its annotations feeling that did not have the direction, Continue searching for where it was assigned:

public abstract class AbsListView extends AdapterView<ListAdapter>{
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
            case MotionEvent.ACTION_POINTER_UP: {
                onSecondaryPointerUp(ev);
                final int x = mMotionX;
                final int y = mMotionY;
                //' get key code for click table index '
                final int motionPosition = pointToPosition(x, y);
                if (motionPosition >= 0) {
                    // Remember where the motion event started
                    final View child = getChildAt(motionPosition - mFirstPosition);
                    mMotionViewOriginalTop = child.getTop();
                    mMotionPosition = motionPosition;
                }
                mLastY = y;
                break; }}Copy the code

PointToPosition () : pointToPosition()

    /**
     * Maps a point to a position in the list.
     *
     * @param x X in local coordinate
     * @param y Y in local coordinate
     * @return The position of the item which contains the specified point, or
     *         {@link #INVALID_POSITION} if the point does not intersect an item.
     */
    public int pointToPosition(int x, int y) {
        Rect frame = mTouchFrame;
        if (frame == null) {
            mTouchFrame = new Rect();
            frame = mTouchFrame;
        }

        // select * from the list where the table is located
        final int count = getChildCount();
        for (int i = count - 1; i >= 0; i--) {
            final View child = getChildAt(i);
            if (child.getVisibility() == View.VISIBLE) {
                //' Get the entry area and store it in frame '
                child.getHitRect(frame);
                //' Return the index of the current entry if you click the coordinate in the entry area '
                if (frame.contains(x, y)) {
                    returnmFirstPosition + i; }}}return INVALID_POSITION;
    }
Copy the code

The index of the click entry in the list is obtained by traversing the entry and judging whether the click coordinates fall within the entry area.

Scheme 2: Convert click coordinates into entry indexes

Just transplant this algorithm to RecyclerView! But there is a new problem: how can a click event be detected in RecyclerView? Of course, it can be achieved by synthesising ACTION_DOWN and ACTION_UP, but this is slightly complicated. The GestureDetector provided by Android can help us deal with this requirement:

public class BaseRecyclerView extends RecyclerView {
    / / 'hold GestureDetector'
    private GestureDetector gestureDetector;
    public BaseRecyclerView(Context context) {
        super(context);
        init();
    }

    private void init(a) {
        / / 'new GestureDetector'
        gestureDetector = new GestureDetector(getContext(), new GestureListener());
    }

    @Override
    public boolean onTouchEvent(MotionEvent e) {
        //' let the touch event be processed by the GestureDetector '
        gestureDetector.onTouchEvent(e);
        //' Be sure to call super.onTouchEvent() otherwise the list won't scroll '
        return super.onTouchEvent(e);
    }

    private class GestureListener implements GestureDetector.OnGestureListener {
        @Override
        public boolean onDown(MotionEvent e) { return false; }@Override
        public void onShowPress(MotionEvent e) {}
        @Override
        public boolean onSingleTapUp(MotionEvent e) { return false; }
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { return false; }
        @Override
        public void onLongPress(MotionEvent e) {}@Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { return false; }}}Copy the code

Such BaseRecyclerView has the ability of detecting click event, the next step is to AbsListView. PointToPosition () copied, rewrite onSingleTapUp () :

public class BaseRecyclerView extends RecyclerView {...private class GestureListener implements GestureDetector.OnGestureListener {
        private static final int INVALID_POSITION = -1;
        private Rect mTouchFrame;
        @Override
        public boolean onDown(MotionEvent e) { return false; }
        @Override
        public void onShowPress(MotionEvent e) {}
        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            //' Get click coordinates'
            int x = (int) e.getX();
            int y = (int) e.getY();
            //' Get the index of the click coordinates'
            int position = pointToPosition(x, y);
            if(position ! = INVALID_POSITION) {try {
                    //' get the index entry, pass it through the interface '
                    View child = getChildAt(position);
                    if(onItemClickListener ! =null) { onItemClickListener.onItemClick(child, getChildAdapterPosition(child), getAdapter()); }}catch (Exception e1) {
                }
            }
            return false;
        }
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { return false; }
        @Override
        public void onLongPress(MotionEvent e) {}
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { return false; }

        /** * convert pointer to the layout position in RecyclerView */
        public int pointToPosition(int x, int y) {
            Rect frame = mTouchFrame;
            if (frame == null) {
                mTouchFrame = new Rect();
                frame = mTouchFrame;
            }

            final int count = getChildCount();
            for (int i = count - 1; i >= 0; i--) {
                final View child = getChildAt(i);
                if (child.getVisibility() == View.VISIBLE) {
                    child.getHitRect(frame);
                    if (frame.contains(x, y)) {
                        returni; }}}returnINVALID_POSITION; }}//' interface to pass the entry click event '
    public interface OnItemClickListener {
        //' Pass entry view, entry adapter location, adapter '
        void onItemClick(View item, int adapterPosition, Adapter adapter);
    }
    
    private OnItemClickListener onItemClickListener;
    
    public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
        this.onItemClickListener = onItemClickListener; }}Copy the code

And you’re done! Now you can listen for RecyclerView clicks like this

public class MainActivity extends AppCompatActivity {
    public static final String[] DATA = {"item1"."item2"."item3"."item4"};

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        MyAdapter myAdapter = new MyAdapter(Arrays.asList(DATA));
        BaseRecyclerView rv = (BaseRecyclerView) findViewById(R.id.rv);
        rv.setAdapter(myAdapter);
        rv.setLayoutManager(new LinearLayoutManager(this));
        // set RecyclerView click event listener for RecyclerView
        rv.setOnItemClickListener(new BaseRecyclerView.OnItemClickListener() {
            @Override
            public void onItemClick(View item, int adapterPosition, RecyclerView.Adapter adapter) {
                Toast.makeText(MainActivity.this, ((MyAdapter) adapter).getData().get(adapterPosition), Toast.LENGTH_SHORT).show(); }}); }}Copy the code

A more minimalist version of Kotlin

Thanks to HitenDev for his comments, here’s a more concise version of Kotlin:

//' click listener for RecyclerView extension '
fun RecyclerView.setOnItemClickListener(listener: (View.Int) - >Unit) {
    // set touch listener for RecyclerView child control
    addOnItemTouchListener(object : RecyclerView.OnItemTouchListener {
        //' construct gesture detector to parse click event '
        val gestureDetector = GestureDetector(context, object : GestureDetector.OnGestureListener {
            override fun onShowPress(e: MotionEvent?).{}override fun onSingleTapUp(e: MotionEvent?).: Boolean {
                //' When a click event occurs, find the child control under the click coordinates and call back the listener 'e? .let { findChildViewUnder(it.x, it.y)? .let { child -> listener(child, getChildAdapterPosition(child)) } }return false
            }

            override fun onDown(e: MotionEvent?).: Boolean {
                return false
            }

            override fun onFling(e1: MotionEvent? , e2:MotionEvent? , velocityX:Float, velocityY: Float): Boolean {
                return false
            }

            override fun onScroll(e1: MotionEvent? , e2:MotionEvent? , distanceX:Float, distanceY: Float): Boolean {
                return false
            }

            override fun onLongPress(e: MotionEvent?).{}})override fun onTouchEvent(rv: RecyclerView, e: MotionEvent){}//' Parse touch events while intercepting touch events'
        override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
            gestureDetector.onTouchEvent(e)
            return false
        }

        override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean){}})}Copy the code

Then you can listen for RecyclerView item clicks like this:

recyclerView.setOnItemClickListener { view, pos ->
	View is the root view of the entry, and pos is the position of the entry in the Adapter
}
Copy the code

The next article will go further, explaining how to extend this scheme to handle click events for RecyclerView child controls.

talk is cheap, show me the code

Recommended reading

RecyclerView series article directory is as follows:

  1. RecyclerView caching mechanism | how to reuse table?

  2. What RecyclerView caching mechanism | recycling?

  3. RecyclerView caching mechanism | recycling where?

  4. RecyclerView caching mechanism | scrap the view of life cycle

  5. Read the source code long knowledge better RecyclerView | click listener

  6. Proxy mode application | every time for the new type RecyclerView is crazy

  7. Better RecyclerView table sub control click listener

  8. More efficient refresh RecyclerView | DiffUtil secondary packaging

  9. Change an idea, super simple RecyclerView preloading

  10. RecyclerView animation principle | change the posture to see the source code (pre – layout)

  11. RecyclerView animation principle | pre – layout, post – the relationship between the layout and scrap the cache

  12. RecyclerView animation principle | how to store and use animation attribute values?

  13. RecyclerView list of interview questions | scroll, how the list items are filled or recycled?

  14. RecyclerView interview question | what item in the table below is recycled to the cache pool?

  15. RecyclerView performance optimization | to halve load time table item (a)

  16. RecyclerView performance optimization | to halve load time table item (2)

  17. RecyclerView performance optimization | to halve load time table item (3)

  18. How does RecyclerView roll? (a) | unlock reading source new posture

  19. RecyclerView how to achieve the scrolling? (2) | Fling

  20. RecyclerView Refresh list data notifyDataSetChanged() why is it expensive?