RecyclerView
沒有提供表項點擊事件監聽器,只能本身處理。git
最容易想到的方案是給每一個表項的itemView
設置View.OnClickListener
,代碼以下:github
//'定義點擊接口'
public interface OnItemClickListener {
void onItemClick(int position);
}
複製代碼
讓Adapter
持有接口:算法
public class MyAdapter extends RecyclerView.Adapter<MyViewHolder> {
//'持有接口'
private OnItemClickListener onItemClickListener;
//'注入接口'
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);
}
//'將接口傳遞給ViewHolder'
@Override
public void onBindViewHolder(MyViewHolder holder, int position) {
holder.bind(onItemClickListener);
}
}
複製代碼
而後就能在ViewHolder
中調用接口:bash
public class MyViewHolder extends RecyclerView.ViewHolder {
public MyViewHolder(View itemView) {
super(itemView);
}
public void bind(final OnItemClickListener onItemClickListener){
//'爲ItemView設置點擊事件'
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (onItemClickListener != null) {
onItemClickListener.onItemClick(getAdapterPosition());
}
}
});
}
}
複製代碼
這個方案的優勢是簡單易懂,但缺點是點擊事件的接口通過多方傳遞:爲了給itemView
設置點擊事件,須要ViewHolder
和Adapter
的傳遞(由於不能直接拿到itemView
)。這就使它們和點擊事件接口耦合在一塊兒,若是點擊事件接口改動,這兩個類須要跟着一塊兒改。dom
還有一個缺點是,內存中會多出 N 個 OnClickListener
對象(N爲一屏的表項個數)。雖然這也不是一個很大的開銷。ide
有沒有更解耦且全部表項共用一個點擊事件監聽器的方案?ui
忽然想到ListView.setOnItemClickListener()
,這不就是全部表項共享的一個監聽器嗎?看看它是怎麼實現的:this
/**
* 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.
* '第二個參數是被點擊的表項'
* @param view The view within the AdapterView that was clicked
* '第三個參數是被點擊表項的適配器位置'
* @param position The position of the view in the adapter.
*/
void onItemClick(AdapterView<?> parent, View view, int position, long id);
}
/**
* '注入表項點擊監聽器'
*/
public void setOnItemClickListener(@Nullable OnItemClickListener listener) {
mOnItemClickListener = listener;
}
複製代碼
這是定義在ListView
中的表項點擊監聽器接口,接口的實例經過setOnItemClickListener()
注入並保存在mOnItemClickListener
中。spa
接口參數中有被點擊的表項View
和其適配器索引,好奇這兩個參數是如何從點擊事件生成的?沿着mOnItemClickListener
向上查找調用鏈:code
public boolean performItemClick(View view, int position, long id) {
final boolean result;
if (mOnItemClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
//'調用點擊事件監聽器'
mOnItemClickListener.onItemClick(this, view, position, id);
result = true;
} else {
result = false;
}
if (view != null) {
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
}
return result;
}
複製代碼
mOnItemClickListener
只有在performItemClick(View view, int position, long id)
中被調用,沿着調用鏈繼續向上查找第一個參數view
是如何生成的:
private class PerformClick extends WindowRunnnable implements Runnable {
//'被點擊表項的索引值'
int mClickMotionPosition;
@Override
public void run() {
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)) {
//'經過motionPosition索引值定位到被點擊的View'
final View view = getChildAt(motionPosition - mFirstPosition);
if (view != null) {
performItemClick(view, motionPosition, adapter.getItemId(motionPosition));
}
}
}
}
複製代碼
被點擊的view
是經過getChildAt(index)
得到的,問題就轉變成對應的索引值是如何產生的?搜索全部PerformClick.mClickMotionPosition
被賦值的地方:
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:
//'被AbsListView.mMotionPosition賦值'
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;
//'被AbsListView.mMotionPosition賦值'
performClick.mClickMotionPosition = motionPosition;
...
}
}
複製代碼
PerformClick.mClickMotionPosition
被賦值的地方只有一個,在AbsListView.onTouchUp()
中被AbsListView.mMotionPosition
賦值,看着它的註釋感受好像沒有找錯方向,繼續搜索它是在哪裏被賦值的:
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;
//'得到點擊表項索引的關鍵代碼'
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;
}
}
複製代碼
最終在onTouchEvent()
中找到了索引值產生的方法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;
}
//'遍歷列表表項'
final int count = getChildCount();
for (int i = count - 1; i >= 0; i--) {
final View child = getChildAt(i);
if (child.getVisibility() == View.VISIBLE) {
//'獲取表項區域並存儲在frame中'
child.getHitRect(frame);
//'若是點擊座標落在表項區域內則返回當前表項的索引'
if (frame.contains(x, y)) {
return mFirstPosition + i;
}
}
}
return INVALID_POSITION;
}
複製代碼
原來是經過遍歷表項,判斷點擊座標是否落在表項區域內來獲取點擊表項在列表中的索引。
只要把這個算法移植到RecyclerView
就能夠了!可是有一個新的問題:如何在RecyclerView
中檢測到單擊事件? 固然能夠經過綜合判斷ACTION_DOWN
和ACTION_UP
來實現,但這略複雜,Andriod 提供的GestureDetector
能幫咱們處理這個需求:
public class BaseRecyclerView extends RecyclerView {
//'持有GestureDetector'
private GestureDetector gestureDetector;
public BaseRecyclerView(Context context) {
super(context);
init();
}
private void init() {
//'新建GestureDetector'
gestureDetector = new GestureDetector(getContext(), new GestureListener());
}
@Override
public boolean onTouchEvent(MotionEvent e) {
//'讓觸摸事件經由GestureDetector處理'
gestureDetector.onTouchEvent(e);
//'必定要調super.onTouchEvent()不然列表就不會滾動了'
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; }
}
}
複製代碼
這樣BaseRecyclerView
就具備檢測單擊事件的能力了,下一步就是將AbsListView.pointToPosition()
複製過來,重寫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) {
//'獲取單擊座標'
int x = (int) e.getX();
int y = (int) e.getY();
//'得到單擊座標對應的表項索引'
int position = pointToPosition(x, y);
if (position != INVALID_POSITION) {
try {
//'獲取索引位置的表項,經過接口傳遞出去'
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)) {
return i;
}
}
}
return INVALID_POSITION;
}
}
//'將表項單擊事件傳遞出去的接口'
public interface OnItemClickListener {
//'將表項view,表項適配器位置,適配器傳遞出去'
void onItemClick(View item, int adapterPosition, Adapter adapter);
}
private OnItemClickListener onItemClickListener;
public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
this.onItemClickListener = onItemClickListener;
}
}
複製代碼
大功告成!,如今就能夠像這樣監聽RecyclerView
的點擊事件了
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));
//'爲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();
}
});
}
}
複製代碼