先说说RecyclerView是怎么使用的,以其基础代码为本,大体以如下顺序进行分析。
- 组件介绍
- 点击事件
- ListView为什么会点击item事件失效?
- RecyclerView怎么设计点击回调?
- 与ListView的区别
- 布局文件
public class testForRecyclerViewActivity{
// 装载item中数据
private List mDatas;
// 初始化item中的数据
protected void initData() {
mDatas = new ArrayList<>();
for (int i = 0; i < 100; i++) {
mDatas.add("" + i);
}
}
//定义点击事件的接口
public interface OnItemClickListener {
void onItemClick(View view, int position);
}
// 继承RecyclerView.Adapter , 按照其规定好的设计规范,定义具体内容。
class LogAdapter extends RecyclerView.Adapter {
private OnItemClickListener mOnItemClickListener;
public void setOnItemClickListener(OnItemClickListener mOnItemClickListener) {
this.mOnItemClickListener = mOnItemClickListener;
}
@NonNull
@NotNull
@Override
public ViewHolder onCreateViewHolder(@NonNull @NotNull ViewGroup viewGroup, int i) {
View view = LayoutInflater.from(testForRecyclerViewActivity.this).inflate(R.layout.test_rv_item, viewGroup, false);
ViewHolder viewHolder = ViewHolder.create(view);
viewHolder.getItemView(R.id.title);
viewHolder.getItemView(R.id.date);
if (mOnItemClickListener != null) {
viewHolder.getmConvertView().setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mOnItemClickListener.onItemClick(view, i);
}
});
}
return viewHolder;
}
@Override
public void onBindViewHolder(@NonNull @NotNull ViewHolder viewHolder, int i) {
viewHolder.setTextView(R.id.title, "我是标题");
viewHolder.setTextView(R.id.date, "我是日期");
}
@Override
public int getItemCount() {
return mDatas.size();
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.test_rv);
initData();
RecyclerView mRecyclerView = findViewById(R.id.test_item_log);
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
LogAdapter mLogAdapter = new LogAdapter();
mLogAdapter.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(View view, int position) {
String webUrl = "https://www.baidu.com/";
Intent intent = new Intent(testForRecyclerViewActivity.this, newDetailsActivity.class);
intent.putExtra("ARGS_KEY_URL", webUrl );
intent.putExtra("ARGS_KEY_TITLE", "标题");
startActivity(intent);
}
});
mRecyclerView.setAdapter(mLogAdapter);
//返回按钮
findViewById(R.id.return_btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
finish();
}
});
}
}
代码介绍
-
组件:(其包含的组件较多,挑选最常用的介绍)
-
onCreateViewHolder(ViewGroup viewGroup, int i),该方法旨在创造一个持有者的类,将对应id的view存到内存中,避免每次都需要去布局文件中读取对应的view。需要注意的是,下面的ViewHolder为公司封装后的类,使用更简洁方便。
public ViewHolder onCreateViewHolder(@NonNull @NotNull ViewGroup viewGroup, int i) { View view = LayoutInflater.from(testForRecyclerViewActivity.this).inflate(R.layout.test_rv_item, viewGroup, false); ViewHolder viewHolder = ViewHolder.create(view); viewHolder.getItemView(R.id.title); viewHolder.getItemView(R.id.date); if (mOnItemClickListener != null) { viewHolder.getmConvertView().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { mOnItemClickListener.onItemClick(view, i); } }); } return viewHolder; }
-
onBindViewHolder(ViewHolder viewHolder, int i),给持有者包含的组件赋予数据,同时可以给组件添加事件。
@Override public void onBindViewHolder(@NonNull @NotNull ViewHolder viewHolder, int i) { viewHolder.setTextView(R.id.title, "我是标题"); viewHolder.setTextView(R.id.date, "我是日期"); }
-
getItemCount(),返回item的条目数
@Override public int getItemCount() { return mDatas.size(); }
-
点击事件(分析RecyclerView和ListView在点击事件的差异)
-
item点击事件:与ListView不同的是,RecyclerView并没有配备setOnItemClickListener()方法,只能通过配置回调接口来设置对应的点击事件。
- 可能你会觉得,wc这么不方便,RecyclerView不用也罢。不急,慢慢听我解释,对于ListView的点击事件有很重要的一个弊端,那就是某个时候你给ListView的item组件设置了setOnItemClickListener事件,正准备高高兴兴去测试时,却发现死活都点击无效甚是懊恼,上网一查同仁还不在少数,解释为:当listview中包含button,checkbox等控件的时候,android会默认将focus给了这些控件,也就是说listview的item根本就获取不到focus,所以导致onitemclick时间不能触发。那我们就详细聊聊为什么会产生这种情况,就随着关键部分的代码慢慢探索答案吧。
- 对于ListView,点击事件发生后,经过事件分发机制判定(默认不拦截),遂调用onTouchEvent()方法去处理该ItemClick时间,该方法处于其继承的AbsListView类中:
@Override public boolean onTouchEvent(MotionEvent ev) { if (!isEnabled()) { // A disabled view that is clickable still consumes the touch // events, it just doesn't respond to them. return isClickable() || isLongClickable(); } if (mPositionScroller != null) { mPositionScroller.stop(); } if (mIsDetaching || !isAttachedToWindow()) { // Something isn't right. // Since we rely on being attached to get data set change notifications, // don't risk doing anything where we might try to resync and find things // in a bogus state. return false; } startNestedScroll(SCROLL_AXIS_VERTICAL); if (mFastScroll != null && mFastScroll.onTouchEvent(ev)) { return true; } initVelocityTrackerIfNotExists(); final MotionEvent vtev = MotionEvent.obtain(ev); final int actionMasked = ev.getActionMasked(); if (actionMasked == MotionEvent.ACTION_DOWN) { mNestedYOffset = 0; } vtev.offsetLocation(0, mNestedYOffset); switch (actionMasked) { case MotionEvent.ACTION_DOWN: { onTouchDown(ev); break; } case MotionEvent.ACTION_MOVE: { onTouchMove(ev, vtev); break; } case MotionEvent.ACTION_UP: { onTouchUp(ev); break; } case MotionEvent.ACTION_CANCEL: { onTouchCancel(); break; } 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; } case MotionEvent.ACTION_POINTER_DOWN: { // New pointers take over dragging duties final int index = ev.getActionIndex(); final int id = ev.getPointerId(index); final int x = (int) ev.getX(index); final int y = (int) ev.getY(index); mMotionCorrection = 0; mActivePointerId = id; mMotionX = x; mMotionY = y; 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; } } if (mVelocityTracker != null) { mVelocityTracker.addMovement(vtev); } vtev.recycle(); return true; }
- 看着这么多的事件处理入口,是否一筹莫展?想当初曹孟德东临碣石以观沧海,从大处着眼忽略细节,望水何澹澹山岛竦峙。同样的道理,我们看着一大片的代码段时,应当结合我们的目标找寻与其最有关的方法,其他的大可以不看。比如我们需要的是看处理onItemClick的事件,而这个事件的触发是来自我们的手指从屏幕抬起的那一刻,因此直接就定位到第47行的onTouchUp(ev)方法。现在我们进去该方法,看它是怎么处理事件的:
private void onTouchUp(MotionEvent ev) { switch (mTouchMode) { case TOUCH_MODE_DOWN: case TOUCH_MODE_TAP: case TOUCH_MODE_DONE_WAITING: 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; performClick.mClickMotionPosition = motionPosition; performClick.rememberWindowAttachCount(); mResurrectToPosition = motionPosition; if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) { removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ? mPendingCheckForTap : mPendingCheckForLongPress); mLayoutMode = LAYOUT_NORMAL; if (!mDataChanged && mAdapter.isEnabled(motionPosition)) { mTouchMode = TOUCH_MODE_TAP; setSelectedPositionInt(mMotionPosition); layoutChildren(); child.setPressed(true); positionSelector(mMotionPosition, child); setPressed(true); if (mSelector != null) { Drawable d = mSelector.getCurrent(); if (d != null && d instanceof TransitionDrawable) { ((TransitionDrawable) d).resetTransition(); } mSelector.setHotspot(x, ev.getY()); } if (mTouchModeReset != null) { removeCallbacks(mTouchModeReset); } mTouchModeReset = new Runnable() { @Override public void run() { mTouchModeReset = null; mTouchMode = TOUCH_MODE_REST; child.setPressed(false); setPressed(false); if (!mDataChanged && !mIsDetaching && isAttachedToWindow()) { performClick.run(); } } }; postDelayed(mTouchModeReset, ViewConfiguration.getPressedStateDuration()); } else { mTouchMode = TOUCH_MODE_REST; updateSelectorState(); } return; } else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) { performClick.run(); } } } mTouchMode = TOUCH_MODE_REST; updateSelectorState(); break; case TOUCH_MODE_SCROLL: ...... case TOUCH_MODE_OVERSCROLL: ...... } setPressed(false); if (shouldDisplayEdgeEffects()) { mEdgeGlowTop.onRelease(); mEdgeGlowBottom.onRelease(); } // Need to redraw since we probably aren't drawing the selector anymore invalidate(); removeCallbacks(mPendingCheckForLongPress); recycleVelocityTracker(); mActivePointerId = INVALID_POINTER; if (PROFILE_SCROLLING) { if (mScrollProfilingStarted) { Debug.stopMethodTracing(); mScrollProfilingStarted = false; } } if (mScrollStrictSpan != null) { mScrollStrictSpan.finish(); mScrollStrictSpan = null; } }
- 我省略掉了部分代码,那些不重要,定位到15行看到了这个大大的判断语句,
if (inList && !child.hasExplicitFocusable())
inList判断触发是否是item范围内的事件为true,重要的是 child.hasExplicitFocusable() 取反,该方法用来判断该节点是否是获取焦点的,如果是则不会触发后续的点击回调。由此变引申出了两种解决方法:- 在button/checkbox等控件处设置
android:clickable=”false” android:focusableInTouchMode=”false”
,使其在点击item时不会因该组件的获取焦点属性而影响了回调事件。 - 在item最外层添加属性
android:descendantFocusability=”blocksDescendants”
,该属性使item覆盖所有的子节点获取焦点,故里面的子节点均不可获取焦点。
- 在button/checkbox等控件处设置
- 既然都到这一步了,不如看看他是怎么调用我们的方法的吧~~点击第67行的performClick.run();
@Override public void run() { // The data has changed since we posted this action in the event queue, // bail out before bad things happen 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)) { final View view = getChildAt(motionPosition - mFirstPosition); // If there is no view, something bad happened (the view scrolled off the // screen, etc.) and we should cancel the click if (view != null) { performItemClick(view, motionPosition, adapter.getItemId(motionPosition)); } } }
- 可以看到该方法最终调用的是AdapterView中的performItemClick()方法,贴出其代码:
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; }
- 终于,在第5行见到了它的庐山真面目,在这里调用的我们设置的mOnItemClickListener()方法。
-
RecyclerView的点击事件应该如何设计其回调接口呢,我归纳了2种方式,当然思想都是一样的,那就是配置回调接口然后在对应的时机实现该方法。
-
第一种方式 按照我在开头贴的代码,在Activity中就定义需要用的回调接口
//定义点击事件的接口 public interface OnItemClickListener { void onItemClick(View view, int position); }
然后在定义Adapter时,声明变量中加入对应变量与构造方法,并且在onCreateViewHolder中绑定该方法
private OnItemClickListener mOnItemClickListener; public void setOnItemClickListener(OnItemClickListener mOnItemClickListener) { this.mOnItemClickListener = mOnItemClickListener; } public ViewHolder onCreateViewHolder(@NonNull @NotNull ViewGroup viewGroup, int i) { View view = LayoutInflater.from(testForRecyclerViewActivity.this).inflate(R.layout.test_rv_item, viewGroup, false); ViewHolder viewHolder = ViewHolder.create(view); viewHolder.getItemView(R.id.title); viewHolder.getItemView(R.id.date); if (mOnItemClickListener != null) { viewHolder.getmConvertView().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { mOnItemClickListener.onItemClick(view, i); } }); } return viewHolder; }
最后在onCreate中实现该接口并重写该点击方法,怎么样,是不是很简单,条理清晰井井有条。
LogAdapter mLogAdapter = new LogAdapter(); mLogAdapter.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(View view, int position) { String webUrl = "https://www.baidu.com/"; Intent intent = new Intent(testForRecyclerViewActivity.this, newDetailsActivity.class); intent.putExtra("ARGS_KEY_URL", webUrl ); intent.putExtra("ARGS_KEY_TITLE", "标题"); startActivity(intent); } });
-
第二种方式 ,在定义持有者时实现OnClickListener接口,重写其onClick方法,根据点击对象的不同而配置不同的方法。组件多的时候,可以通过getId来使用switch,case来分id处理事件。
public static class ViewHolder extends RecyclerView.ViewHolder implements OnClickListener { public TextView txtViewTitle; public ImageView imgViewIcon; public IMyViewHolderClicks mListener; public ViewHolder(View itemLayoutView, IMyViewHolderClicks listener) { super(itemLayoutView); mListener = listener; txtViewTitle = (TextView) itemLayoutView.findViewById(R.id.item_title); imgViewIcon = (ImageView) itemLayoutView.findViewById(R.id.item_icon); imgViewIcon.setOnClickListener(this); itemLayoutView.setOnClickListener(this); } @Override public void onClick(View v) { if (v instanceof ImageView){ mListener.onTomato((ImageView)v); } else { mListener.onPotato(v); } } public static interface IMyViewHolderClicks { public void onPotato(View caller); public void onTomato(ImageView callerImage); } }
在编写好ViewHolder的代码后,在适配器中我们只需要重写其IMyViewHolderClicks方法即可。
public class MyAdapter extends RecyclerView.Adapter
{ @Override public MyAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.my_layout, parent, false); MyAdapter.ViewHolder vh = new ViewHolder(v, new MyAdapter.ViewHolder.IMyViewHolderClicks() { public void onPotato(View caller) { Log.d("VEGETABLES","Poh-tah-tos"); }; public void onTomato(ImageView callerImage) { Log.d("VEGETABLES","To-m8-tohs"); } }); return vh; } ...
-
与ListView的区别
- RecyclerView与ListView最大的区别就是它的布局方式十分丰富:线性布局(横向或者纵向)、表格布局、瀑布流布局。而ListView只有一个纵向布局的效果,若需要不同的呈现方式还得自己去定义。面对现在更多样的需求,无论是从美观还是效率上说,RecyclerView都是首选;
- RecyclerView编写的规范化,从上面可以得知RecyclerView的组件都是定义好的,在什么阶段定义什么得到什么。而ListView需要重写getView,布局的载入、持有、资源设置全部在里面完成。
- 缓存方法的优势,RecyclerView比ListView多两级缓存,开发有缓存池,支持多个RecyclerView共同使用;RV缓存的是ViewHolder,支持屏幕外的列表项进入屏幕时无须bindView就可以快速重用。
RecyclerView布局文件(test_rv.xml)
布局内item组件(test_rv_item.xml)