先說說RecyclerView是怎么使用的,以其基礎(chǔ)代碼為本,大體以如下順序進(jìn)行分析。
- 組件介紹
- 點擊事件
- ListView為什么會點擊item事件失效?
- RecyclerView怎么設(shè)計點擊回調(diào)?
- 與ListView的區(qū)別
- 布局文件
public class testForRecyclerViewActivity{
// 裝載item中數(shù)據(jù)
private List<String> mDatas;
// 初始化item中的數(shù)據(jù)
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 , 按照其規(guī)定好的設(shè)計規(guī)范,定義具體內(nèi)容。
class LogAdapter extends RecyclerView.Adapter<ViewHolder> {
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, "我是標(biāo)題");
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", "標(biāo)題");
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),該方法旨在創(chuàng)造一個持有者的類,將對應(yīng)id的view存到內(nèi)存中,避免每次都需要去布局文件中讀取對應(yīng)的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),給持有者包含的組件賦予數(shù)據(jù),同時可以給組件添加事件。
@Override public void onBindViewHolder(@NonNull @NotNull ViewHolder viewHolder, int i) { viewHolder.setTextView(R.id.title, "我是標(biāo)題"); viewHolder.setTextView(R.id.date, "我是日期"); } -
getItemCount(),返回item的條目數(shù)
@Override public int getItemCount() { return mDatas.size(); }
-
點擊事件(分析RecyclerView和ListView在點擊事件的差異)
-
item點擊事件:與ListView不同的是,RecyclerView并沒有配備setOnItemClickListener()方法,只能通過配置回調(diào)接口來設(shè)置對應(yīng)的點擊事件。
- 可能你會覺得,wc這么不方便,RecyclerView不用也罷。不急,慢慢聽我解釋,對于ListView的點擊事件有很重要的一個弊端,那就是某個時候你給ListView的item組件設(shè)置了setOnItemClickListener事件,正準(zhǔn)備高高興興去測試時,卻發(fā)現(xiàn)死活都點擊無效甚是懊惱,上網(wǎng)一查同仁還不在少數(shù),解釋為:當(dāng)listview中包含button,checkbox等控件的時候,android會默認(rèn)將focus給了這些控件,也就是說listview的item根本就獲取不到focus,所以導(dǎo)致onitemclick時間不能觸發(fā)。那我們就詳細(xì)聊聊為什么會產(chǎn)生這種情況,就隨著關(guān)鍵部分的代碼慢慢探索答案吧。
- 對于ListView,點擊事件發(fā)生后,經(jīng)過事件分發(fā)機制判定(默認(rèn)不攔截),遂調(diào)用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; }- 看著這么多的事件處理入口,是否一籌莫展?想當(dāng)初曹孟德東臨碣石以觀滄海,從大處著眼忽略細(xì)節(jié),望水何澹澹山島竦峙。同樣的道理,我們看著一大片的代碼段時,應(yīng)當(dāng)結(jié)合我們的目標(biāo)找尋與其最有關(guān)的方法,其他的大可以不看。比如我們需要的是看處理onItemClick的事件,而這個事件的觸發(fā)是來自我們的手指從屏幕抬起的那一刻,因此直接就定位到第47行的onTouchUp(ev)方法?,F(xiàn)在我們進(jìn)去該方法,看它是怎么處理事件的:
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判斷觸發(fā)是否是item范圍內(nèi)的事件為true,重要的是 child.hasExplicitFocusable() 取反,該方法用來判斷該節(jié)點是否是獲取焦點的,如果是則不會觸發(fā)后續(xù)的點擊回調(diào)。由此變引申出了兩種解決方法:- 在button/checkbox等控件處設(shè)置
android:clickable=”false” android:focusableInTouchMode=”false”,使其在點擊item時不會因該組件的獲取焦點屬性而影響了回調(diào)事件。 - 在item最外層添加屬性
android:descendantFocusability=”blocksDescendants”,該屬性使item覆蓋所有的子節(jié)點獲取焦點,故里面的子節(jié)點均不可獲取焦點。
- 在button/checkbox等控件處設(shè)置
- 既然都到這一步了,不如看看他是怎么調(diào)用我們的方法的吧~~點擊第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)); } } }- 可以看到該方法最終調(diào)用的是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行見到了它的廬山真面目,在這里調(diào)用的我們設(shè)置的mOnItemClickListener()方法。
-
RecyclerView的點擊事件應(yīng)該如何設(shè)計其回調(diào)接口呢,我歸納了2種方式,當(dāng)然思想都是一樣的,那就是配置回調(diào)接口然后在對應(yīng)的時機實現(xiàn)該方法。
-
第一種方式 按照我在開頭貼的代碼,在Activity中就定義需要用的回調(diào)接口
//定義點擊事件的接口 public interface OnItemClickListener { void onItemClick(View view, int position); }然后在定義Adapter時,聲明變量中加入對應(yīng)變量與構(gòu)造方法,并且在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中實現(xiàn)該接口并重寫該點擊方法,怎么樣,是不是很簡單,條理清晰井井有條。
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", "標(biāo)題"); startActivity(intent); } }); -
第二種方式 ,在定義持有者時實現(xiàn)OnClickListener接口,重寫其onClick方法,根據(jù)點擊對象的不同而配置不同的方法。組件多的時候,可以通過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<MyAdapter.ViewHolder> { @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的區(qū)別
- RecyclerView與ListView最大的區(qū)別就是它的布局方式十分豐富:線性布局(橫向或者縱向)、表格布局、瀑布流布局。而ListView只有一個縱向布局的效果,若需要不同的呈現(xiàn)方式還得自己去定義。面對現(xiàn)在更多樣的需求,無論是從美觀還是效率上說,RecyclerView都是首選;
- RecyclerView編寫的規(guī)范化,從上面可以得知RecyclerView的組件都是定義好的,在什么階段定義什么得到什么。而ListView需要重寫getView,布局的載入、持有、資源設(shè)置全部在里面完成。
- 緩存方法的優(yōu)勢,RecyclerView比ListView多兩級緩存,開發(fā)有緩存池,支持多個RecyclerView共同使用;RV緩存的是ViewHolder,支持屏幕外的列表項進(jìn)入屏幕時無須bindView就可以快速重用。
RecyclerView布局文件(test_rv.xml)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/xxxx">
<include layout="@layout/xxxx" />
<ProgressBar
android:id="@+id/我是進(jìn)度條"
style="@android:style/xxx"
android:layout_width="match_parent"
android:layout_height="2dip"
android:layout_below="@+id/title_bar"
android:gravity="center_vertical"
android:max="100"
android:progressDrawable="@drawable/play_progress"
android:indeterminateDrawable="@null" />
<TextView
android:id="@+id/activity_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_5dp"
android:layout_marginStart="25dp"
android:layout_marginBottom="23dp"
android:textColor="@color/x"
android:textSize="@dimen/x"
android:text="xxx"
/>
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
android:id="@+id/xxx"/>
</LinearLayout>
布局內(nèi)item組件(test_rv_item.xml)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="@dimen/xxx">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginEnd="@dimen/xxx"
android:layout_marginRight="@dimen/xxx"
android:importantForAccessibility="no"
android:src="@drawable/我是右側(cè)的圖標(biāo)" />
<LinearLayout
android:id="@+id/xxxxxx"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:layout_marginStart="24dp"
android:layout_marginEnd="70dp"
android:layout_marginRight="70dp"
android:layout_toStartOf="@id/xxx"
android:layout_toLeftOf="@id/xxx"
android:orientation="vertical">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/xxx"
android:text=""
android:textSize="@dimen/xxx" />
<TextView
android:id="@+id/date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textColor="@color/xxx"
android:textSize="@dimen/xxx" />
</LinearLayout>
</LinearLayout>