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:
Read the source code long knowledge better RecyclerView | click listener
Android custom controls | source there is treasure in the automatic line feed control
Android custom controls | three implementation of little red dot (below)
Reading knowledge source long | dynamic extension class and bind the new way of the life cycle
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:
-
RecyclerView caching mechanism | how to reuse table?
-
What RecyclerView caching mechanism | recycling?
-
RecyclerView caching mechanism | recycling where?
-
RecyclerView caching mechanism | scrap the view of life cycle
-
Read the source code long knowledge better RecyclerView | click listener
-
Proxy mode application | every time for the new type RecyclerView is crazy
-
Better RecyclerView table sub control click listener
-
More efficient refresh RecyclerView | DiffUtil secondary packaging
-
Change an idea, super simple RecyclerView preloading
-
RecyclerView animation principle | change the posture to see the source code (pre – layout)
-
RecyclerView animation principle | pre – layout, post – the relationship between the layout and scrap the cache
-
RecyclerView animation principle | how to store and use animation attribute values?
-
RecyclerView list of interview questions | scroll, how the list items are filled or recycled?
-
RecyclerView interview question | what item in the table below is recycled to the cache pool?
-
RecyclerView performance optimization | to halve load time table item (a)
-
RecyclerView performance optimization | to halve load time table item (2)
-
RecyclerView performance optimization | to halve load time table item (3)
-
How does RecyclerView roll? (a) | unlock reading source new posture
-
RecyclerView how to achieve the scrolling? (2) | Fling
-
RecyclerView Refresh list data notifyDataSetChanged() why is it expensive?