RecyclerView 是 Android 5.0 提出的新 UI 控件,用来替代 ListView。
RecyclerView 官方定义如下:
A flexible view for providing a limited window into a large data set.
重要类介绍 LayoutManager 布局管理器 LayoutManager 负责 RecyclerView 的布局。LayoutManager 是一个抽象类,SDK 实现有 LinearLayoutManager
、 GridLayoutManager
和 StaggeredGridLayoutManager
,分别是线性布局、网格布局和瀑布流布局。
LayoutManager#onLayoutChildren() 是对 RecyclerView 进行布局的入口方法。LinearLayoutManager 的核心实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public void onLayoutChildren (RecyclerView.Recycler recycler, RecyclerView.State state) { detachAndScrapAttachedViews(recycler); fill(recycler, mLayoutState, state, false ); } int fill (RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) { while ((layoutState.mInfinite || remainingSpace > 0 ) && layoutState.hasMore(state)) { layoutChunk(recycler, state, layoutState, layoutChunkResult); } } void layoutChunk (RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) { View view = layoutState.next(recycler); if (mShouldReverseLayout == (layoutState.mLayoutDirection == LayoutState.LAYOUT_START)) { addView(view); } else { addView(view, 0 ); } measureChildWithMargins(view, 0 , 0 ); if (mOrientation == VERTICAL) { } else { } layoutDecoratedWithMargins(view, left, top, right, bottom); }
ViewHolder 负责 itemView 中 childView 的管理。
1 2 3 4 5 6 7 public static class VH extends RecyclerView .ViewHolder { private TextView mTextView; public VH (View itemView) { super (itemView); mTextView = (TextView) itemView.findViewById(android.R.id.text1); } }
Adapter 负责创建视图并把指定位置上的数据填充到 View 上。
1 public abstract static class Adapter <VH extends ViewHolder >
notifyDataSetChanged()
notifyItemChanged()/notifyItemRangeChanged()
notifyItemInserted()/notifyItemRangeInserted()
notifyItemMoved()/notifyItemRangeRemoved()
notifyItemRangeChanged()
ItemAnimator 动画 负责处理数据添加或者删除时的动画效果。SimpleItemAnimator
是 ItemAnimator 的子类,也是抽象类。DefaultItemAnimator
是 SimpleItemAnimator 的子类,是 SDK 提供的 RecycleView 默认动画。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 public class RecyclerView { ItemAnimator mItemAnimator = new DefaultItemAnimator (); private Runnable mItemAnimatorRunner = new Runnable () { @Override public void run () { mItemAnimator.runPendingAnimations(); } }; void postAnimationRunner () { ViewCompat.postOnAnimation(this , mItemAnimatorRunner); } public void setItemAnimator (ItemAnimator animator) { if (mItemAnimator != null ) { mItemAnimator.endAnimations(); mItemAnimator.setListener(null ); } mItemAnimator = animator; if (mItemAnimator != null ) { mItemAnimator.setListener(mItemAnimatorListener); } } public abstract static class ItemAnimator { public abstract boolean animateDisappearance (ViewHolder viewHolder, ItemHolderInfo preLayoutInfo, ItemHolderInfo postLayoutInfo) ; public abstract boolean animateAppearance (ViewHolder viewHolder, ItemHolderInfo preLayoutInfo, ItemHolderInfo postLayoutInfo) ; public abstract boolean animatePersistence (ViewHolder viewHolder, ItemHolderInfo preLayoutInfo, ItemHolderInfo postLayoutInfo) ; public abstract boolean animateChange (ViewHolder oldHolder, ViewHolder newHolder, ItemHolderInfo preLayoutInfo, ItemHolderInfo postLayoutInfo) ; public abstract void runPendingAnimations () ; } }
RecyclerView 默认使用的是 DefaultItemAnimator 动画,也可以通过 setItemAnimator() 设置自定义动画。
ItemAnimator 是抽象类,定义了动画相关的抽象方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public abstract class SimpleItemAnimator extends RecyclerView .ItemAnimator { public abstract boolean animateRemove (RecyclerView.ViewHolder holder) ; public abstract boolean animateAdd (RecyclerView.ViewHolder holder) ; public abstract boolean animateMove (RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) ; public abstract boolean animateChange (RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop) ; public final void dispatchXxxStarting (RecyclerView.ViewHolder item) {} public final void dispatchXxxFinished (RecyclerView.ViewHolder item) {} }
SimpleItemAnimator 对 ItemAnimator 进行了封装,提供了更简洁、更容易理解的 Api。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 public class DefaultItemAnimator extends SimpleItemAnimator { @Override public boolean animateAdd (final RecyclerView.ViewHolder holder) { resetAnimation(holder); holder.itemView.setAlpha(0 ); mPendingAdditions.add(holder); return true ; } void animateAddImpl (final RecyclerView.ViewHolder holder) { final View view = holder.itemView; final ViewPropertyAnimator animation = view.animate(); mAddAnimations.add(holder); animation.alpha(1 ).setDuration(getAddDuration()) .setListener(new AnimatorListenerAdapter () { @Override public void onAnimationStart (Animator animator) { dispatchAddStarting(holder); } @Override public void onAnimationCancel (Animator animator) { view.setAlpha(1 ); } @Override public void onAnimationEnd (Animator animator) { animation.setListener(null ); dispatchAddFinished(holder); mAddAnimations.remove(holder); dispatchFinishedWhenDone(); } }).start(); } @Override public void runPendingAnimations () { for (RecyclerView.ViewHolder holder : mPendingRemovals) { animateRemoveImpl(holder); } if (movesPending) { final ArrayList<MoveInfo> moves = new ArrayList <>(); moves.addAll(mPendingMoves); Runnable mover = new Runnable () { @Override public void run () { for (MoveInfo moveInfo : moves) { animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY, moveInfo.toX, moveInfo.toY); } } }; if (removalsPending) { View view = moves.get(0 ).holder.itemView; ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration()); } else { mover.run(); } } if (changesPending) { } if (additionsPending) { if (removalsPending || movesPending || changesPending) { long totalDelay = removeDuration + Math.max(moveDuration, changeDuration); View view = additions.get(0 ).itemView; ViewCompat.postOnAnimationDelayed(view, adder, totalDelay); } else { adder.run(); } } } }
RecyclerView 动画三方库
ItemDecoration 装饰 负责给 Item 添加额外的操作,例如画分隔线。ItemDecoration 可以添加多个,保存在集合中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 public static class LayoutParams extends android .view.ViewGroup.MarginLayoutParams { final Rect mDecorInsets = new Rect (); } Rect getItemDecorInsetsForChild (View child) { final Rect insets = lp.mDecorInsets; insets.set(0 , 0 , 0 , 0 ); final int decorCount = mItemDecorations.size(); for (int i = 0 ; i < decorCount; i++) { mTempRect.set(0 , 0 , 0 , 0 ); mItemDecorations.get(i).getItemOffsets(mTempRect, child, this , mState); insets.left += mTempRect.left; insets.top += mTempRect.top; insets.right += mTempRect.right; insets.bottom += mTempRect.bottom; } lp.mInsetsDirty = false ; return insets; } public abstract static class ItemDecoration { public void onDraw (Canvas c, RecyclerView parent, State state) { } public void onDrawOver (Canvas c, RecyclerView parent, State state) { } public void getItemOffsets (Rect outRect, View view, RecyclerView parent, State state) { } } public void layoutDecoratedWithMargins (View child, int left, int top, int right, int bottom) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); final Rect insets = lp.mDecorInsets; child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin, right - insets.right - lp.rightMargin, bottom - insets.bottom - lp.bottomMargin); } @Override public void draw (Canvas c) { super .draw(c); final int count = mItemDecorations.size(); for (int i = 0 ; i < count; i++) { mItemDecorations.get(i).onDrawOver(c, this , mState); } } @Override public void onDraw (Canvas c) { super .onDraw(c); final int count = mItemDecorations.size(); for (int i = 0 ; i < count; i++) { mItemDecorations.get(i).onDraw(c, this , mState); } }
Item 滑动删除和拖拽 Android 提供了 ItemTouchHelper
类,使得 RecyclerView 能够轻易地实现滑动和拖拽。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public class ItemTouchHelper extends RecyclerView .ItemDecoration implements RecyclerView .OnChildAttachStateChangeListener { public void startDrag (ViewHolder viewHolder) {} public abstract static class Callback { public abstract int getMovementFlags (RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) public abstract boolean onMove (RecyclerView recyclerView, ViewHolder viewHolder, ViewHolder target) ; public abstract void onSwiped (ViewHolder viewHolder, int direction) ; public void onSelectedChanged (ViewHolder viewHolder, int actionState) { ItemTouchUIUtilImpl.INSTANCE.onSelected(viewHolder.itemView); } public void clearView (RecyclerView recyclerView, ViewHolder viewHolder) { ItemTouchUIUtilImpl.INSTANCE.clearView(viewHolder.itemView); } public boolean isLongPressDragEnabled () { return true ; } } }
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 public class SimpleItemTouchCallback extends ItemTouchHelper .Callback { private MyAdapter mAdapter; private List<ObjectModel> mData; public SimpleItemTouchCallback (MyAdapter adapter, List<ObjectModel> data) { mAdapter = adapter; mData = data; } @Override public int getMovementFlags (RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { int dragFlag = ItemTouchHelper.UP | ItemTouchHelper.DOWN; int swipeFlag = ItemTouchHelper.START | ItemTouchHelper.END; return makeMovementFlags(dragFlag, swipeFlag); } @Override public boolean onMove (RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { int from = viewHolder.getAdapterPosition(); int to = target.getAdapterPosition(); Collections.swap(mData, from, to); mAdapter.notifyItemMoved(from, to); return true ; } @Override public void onSwiped (RecyclerView.ViewHolder viewHolder, int direction) { int pos = viewHolder.getAdapterPosition(); mData.remove(pos); mAdapter.notifyItemRemoved(pos); } @Override public void onSelectedChanged (RecyclerView.ViewHolder viewHolder, int actionState) { super .onSelectedChanged(viewHolder, actionState); if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) { viewHolder.itemView.setBackgroundColor(Color.BLACK); } } @Override public void clearView (RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { super .clearView(recyclerView, viewHolder); viewHolder.itemView.setBackgroundColor(Color.White); } } ItemTouchHelper itemTouchHelper = new ItemTouchHelper (new SimpleItemTouchCallback (adapter, data));itemTouchHelper.attachToRecyclerView(mRecyclerView);
缓存机制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public final class Recycler { final ArrayList<ViewHolder> mAttachedScrap = new ArrayList <>(); ArrayList<ViewHolder> mChangedScrap = null ; final ArrayList<ViewHolder> mCachedViews = new ArrayList <ViewHolder>(); RecycledViewPool mRecyclerPool; private ViewCacheExtension mViewCacheExtension; static final int DEFAULT_CACHE_SIZE = 2 ; ViewHolder tryGetViewHolderForPositionByDeadline (int position, boolean dryRun, long deadlineNs) { return holder; } }
RecyclerView 缓存的是 RecyclerView.ViewHolder。内部实现了四级缓存:
mAttachedScrap 和 mChangedScrap :第一级缓存,用来保存被 RecycledView 移除掉但最近又马上要使用的缓存,比如说 RecycledView 中自带 item 的动画效果。
mCachedViews :第二级缓存,缓存屏幕外的 ViewHolder,默认为 2 个。
mViewCacheExtension : 第三级缓存,自定义的缓存,默认不实现。
mRecyclerPool : 第四级缓存,缓存池,多个 RecyclerView 共用。
LinearLayoutManager 在 fill()
方法中获取缓存的 ViewHolder 进行填充。具体方法是 View view = layoutState.next(recycler)
源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 static class LayoutState { View next (RecyclerView.Recycler recycler) { final View view = recycler.getViewForPosition(mCurrentPosition); return view; } } public final class Recycler { View getViewForPosition (int position, boolean dryRun) { return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView; } ViewHolder tryGetViewHolderForPositionByDeadline (int position, boolean dryRun, long deadlineNs) { ViewHolder holder = null ; if (holder == null ) { holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun); } if (holder == null ) { final int type = mAdapter.getItemViewType(offsetPosition); if (mAdapter.hasStableIds()) { holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun); } } if (holder == null && mViewCacheExtension != null ) { final View view = mViewCacheExtension .getViewForPositionAndType(this , position, type); } if (holder == null ) { holder = getRecycledViewPool().getRecycledView(type); } if (holder == null ) { holder = mAdapter.createViewHolder(RecyclerView.this , type); } if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) { tryBindViewHolderByDeadline(...) } } private boolean tryBindViewHolderByDeadline (...) { mAdapter.bindViewHolder(holder, offsetPosition); } }
从上述实现可以看出,依次从 mAttachedScrap, mCachedViews, mViewCacheExtension, mRecyclerPool 中寻找可复用的 ViewHolder。
如果是从 mAttachedScrap 或 mCachedViews 中获取的 ViewHolder,则不会调用 onCreateViewHolder()创建方法,只会调用onBindViewHolder()绑定数据。
如果从 mViewCacheExtension 或 mRecyclerPool 中获取的 ViewHolder,则会调用 onBindViewHolder()。
mAttachedScrap : 不参与滑动时的回收复用,只保存重新布局时从RecyclerView分离的item的无效、未移除、未更新的holder。因为RecyclerView在onLayout的时候,会先把children全部移除掉,再重新添加进入,mAttachedScrap临时保存这些holder复用。
mChangedScrap : mChangedScrap和mAttachedScrap类似,不参与滑动时的回收复用,只是用作临时保存的变量,它只会负责保存重新布局时发生变化的item的无效、未移除的holder,那么会重走adapter绑定数据的方法。
mCachedViews : 用于保存最新被移除(remove)的ViewHolder,已经和RecyclerView分离的视图;它的作用是滚动的回收复用时如果需要新的ViewHolder时,精准匹配(根据position/id判断)是不是原来被移除的那个item;如果是,则直接返回ViewHolder使用,不需要重新绑定数据;如果不是则不返回,再去mRecyclerPool中找holder实例返回,并重新绑定数据。这一级的缓存是有容量限制的,最大数量为2。
mViewCacheExtension : RecyclerView给开发者预留的缓存池,开发者可以自己拓展回收池,一般不会用到,用RecyclerView系统自带的已经足够了。
mRecyclerPool : 是一个终极回收站,真正存放着被标识废弃(其他池都不愿意回收)的ViewHolder的缓存池,如果上述mAttachedScrap、mChangedScrap、mCachedViews、mViewCacheExtension都找不到ViewHolder的情况下,就会从mRecyclerPool返回一个废弃的ViewHolder实例,但是这里的ViewHolder是已经被抹除数据的,没有任何绑定的痕迹,需要重新绑定数据。它是根据itemType来存储的,是以SparseArray嵌套一个ArraryList的形式保存ViewHolder的。
ListView 缓存机制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 public abstract class AbsListView { class RecycleBin { private View[] mActiveViews = new View [0 ]; private ArrayList<View>[] mScrapViews; private ArrayList<View> mCurrentScrap; void addScrapView (View scrap, int position) { final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams(); final int viewType = lp.viewType; if (mViewTypeCount == 1 ) { mCurrentScrap.add(scrap); } else { mScrapViews[viewType].add(scrap); } } View getScrapView (int position) { final int whichScrap = mAdapter.getItemViewType(position); if (whichScrap < 0 ) { return null ; } if (mViewTypeCount == 1 ) { return retrieveFromScrap(mCurrentScrap, position); } else if (whichScrap < mScrapViews.length) { return retrieveFromScrap(mScrapViews[whichScrap], position); } return null ; } private View retrieveFromScrap (ArrayList<View> scrapViews, int position) { final int size = scrapViews.size(); if (size > 0 ) { final View scrap = scrapViews.remove(size - 1 ); return scrap; } else { return null ; } } void scrapActiveViews () { } View getActiveView (int position) { int index = position - mFirstActivePosition; final View[] activeViews = mActiveViews; if (index >=0 && index < activeViews.length) { final View match = activeViews[index]; activeViews[index] = null ; return match; } return null ; } } protected void layoutChildren () {} View obtainView (int position, boolean [] outMetadata) { final View scrapView = mRecycler.getScrapView(position); final View child = mAdapter.getView(position, scrapView, this ); return child; } } public class ListView extends AbsListView { @Override protected void layoutChildren () { if (dataChanged) { for (int i = 0 ; i < childCount; i++) { recycleBin.addScrapView(getChildAt(i), firstPosition+i); } } else { recycleBin.fillActiveViews(childCount, firstPosition); } mRecycler.scrapActiveViews(); } private View fillSpecific (int position, int top) { View temp = makeAndAddView(position, top, true , mListPadding.left, tempIsSelected); } private View makeAndAddView (int position, int y, boolean flow, int childrenLeft, boolean selected) { if (!mDataChanged) { final View activeView = mRecycler.getActiveView(position); if (activeView != null ) { setupChild(activeView, position, y, flow, childrenLeft, selected, true ); return activeView; } } final View child = obtainView(position, mIsScrap); setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0 ]); return child; } }
ListView 和 RecyclerView 缓存机制基本一致:
mActiveViews 和 mAttachedScrap 功能相似,意义在于快速重用屏幕上可见的 ItemView,而不需要重新 createView 和 bindView;
mScrapView 和 mCachedViews + mReyclerViewPool 功能相似,意义在于缓存离开屏幕的 ItemView,目的是让即将进入屏幕的 ItemView 重用;
RecyclerView 的优势在于:
mCacheViews 的使用,可以做到屏幕外的列表项 ItemView 进入屏幕内时也无须 bindView 快速重用;
mRecyclerPool 可以供多个 RecyclerView 共同使用。在特定场景下,如 viewpaper+多个列表页下有优势.客观来说,RecyclerView 在特定场景下对 ListView 的缓存机制做了补强和完善。
RecyclerView 优化 预取功能 (Prefetch) 为了充分利用 CPU,当 CPU 空闲时 RecyclerView 会预取接下来可能要显示的 item,在下一帧到来之前提前处理完数据,然后将得到的 itemholder 缓存起来,等到真正要使用的时候直接从缓存取出来即可。
LinearLayoutManager#setInitialPrefetchItemCount(int itemCount)
1 2 3 4 5 6 7 8 9 10 11 @Override public boolean onTouchEvent (MotionEvent e) { case MotionEvent.ACTION_MOVE: { if (mScrollState == SCROLL_STATE_DRAGGING) { if (mGapWorker != null && (dx != 0 || dy != 0 )) { mGapWorker.postFromTraversal(this , dx, dy); } } } }
根据创建同类型的 ViewHolder 所需要的时间来判断。
setHasFixedSize(true) RecyclerView 的大小不会因数据集的更新而改变时设置为 ture, 当添加和删除 item 时不会重新 requestLayout()。
stackoverflow 回答如下:
RecyclerView size changes every time you add something no matter what. What setHasFixedSize does is that it makes sure (by user input) that this change of size of RecyclerView is constant. The height (or width) of the item won’t change. Every item added or removed will be the same. If you dont set this it will check if the size of the item has changed and thats expensive. Just clarifying because this answer is confusing. – ArnoldB May 25 at 18:42
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public void setHasFixedSize (boolean hasFixedSize) { mHasFixedSize = hasFixedSize; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public class RecyclerView { @Override protected void onMeasure (int widthSpec, int heightSpec) { if (mLayout.isAutoMeasureEnabled()) { } else { if (mHasFixedSize) { mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); return ; } } } } private class RecyclerViewDataObserver extends AdapterDataObserver { @Override public void onChanged () { requestLayout(); } @Override public void onItemRangeXXX (int positionStart, int itemCount, Object payload) { triggerUpdateProcessor(); } void triggerUpdateProcessor () { if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) { ViewCompat.postOnAnimation(RecyclerView.this , mUpdateChildViewsRunnable); } else { requestLayout(); } } }
swapAdapter() setAdapter() 会直接清空 RecycledView 上的所有缓存,而 swapAdapter() 会重用 ViewHolder,而且不会清空 RecycledViewPool。适用于两个数据集大致相同的情况。
获取 ViewHolder 的位置
findViewHolderForPosition()
findViewHolderForAdapterPosition()
findViewHolderForLayoutPosition()
RecyclerView 拓展 添加 Item 点击事件 添加 EmptyView 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class EmptyRecyclerView extends RecyclerView { private RecyclerView.AdapterDataObserver mObserver = new RecyclerView .AdapterDataObserver() { @Override public void onChanged () { RecyclerView.Adapter adapter = mRecyclerView.getAdapter(); if (adapter.getItemCount() == 0 ) { mEmptyView.setVisibility(View.VISIBLE); setVisibility(View.GONE); } else { mEmptyView.setVisibility(View.GONE); setVisibility(View.VISIBLE); } } }; public void setAdapter (RecyclerView.Adapter adapter) { super .setAdapter(adapter); adapter.registerAdapterDataObserver(mObserver); mObserver.onChanged(); } public void setEmptyView (View view) { mEmptyView = view; ((ViewGroup)getRootView()).addView(mEmptyView); } }
嵌套滑动机制 遇到的问题 滑动冲突:
ScrollView 换成 NestedScrollView;
设置 RecyclerView#setNestedScrollingEnabled(false);
RecyclerView#setHasFixedSize(boolean hasFixedSize):
notifyDataSetChanged() 和 notifyItemInserted():
notifyDataSetChanged() 会渲染整个数据集。
notifyItemInserted() 不嵌套 NestedScrollView 时,在屏幕内才渲染。嵌套 NestedScrollView 时,添加数据时就会渲染,不管新添加的数据在不在屏幕内。但是滑动到屏幕内时就不再渲染了。
当 Adapter 添加新数据时 notifyDataSetChanged() 会创建新的 ViewHolder,而 notifyItemInserted() 不会
notifyItemInserted() 不显示数据问题:
设置 RecyclerView#setHasFixedSize(false),重新测量 RecyclerView 的大小。
RecyclerView.OnScrollListener 不回调问题:
ScrollView 和 RecycleView 都是垂直方向
如果 RecyclerView.setNestedScrollingEnabled(false),RecyclerView.OnScrollListener 接收不到通知。
如果 RecyclerView.setNestedScrollingEnabled(true),onScrolled() 只回调一次,onScrollStateChanged() 在一定方向上每次会回调一次,但是再反向滚动回去就不会回调了。
ScrollView 垂直方向, RecycleView 横向。没有问题.
HorizontalScrollView(横向)嵌套 RecyclerView(横向),RecycleView 不能滚动。
CoordinatorLayout + AppBarLayout + RecyclerView,滚动到最底部延迟问题 material 包从某个升级引入了惯性下滑的概念。滑动的速度越快,下滑的时间越长,就会导致 RecyclerView 实际上已经滑动到最底部了,但是还是卡在了下滑的过程之中,等惯性下滑的触发结束以后,才会触发 RecyclerView#onScrollStateChanged()。 用户体验上就是 RecyclerView 明明已经滑动到最底部,应该显示加载中去获取下一页数据了,却一直卡着不动。需要等一小会才加载数据。
解决方法是自定义一个 behavior 类继承 AppBarLayout.Behavior,加速结束惯性下滑的回调,当滑动到顶部或到底就认为滑动结束。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class ScrollAppBarLayoutBehavior extends AppBarLayout .Behavior { public ScrollAppBarLayoutBehavior (Context context, AttributeSet attrs) { super (context, attrs); } @Override public void onNestedPreScroll (CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int [] consumed, int type) { super .onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type); stopNestedScrollIfNeeded(dy, child, target, type); } @Override public void onNestedScroll (CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type, int [] consumed) { super .onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed); stopNestedScrollIfNeeded(dyUnconsumed, child, target, type); } private void stopNestedScrollIfNeeded (int dy, AppBarLayout child, View target, int type) { if (type == ViewCompat.TYPE_NON_TOUCH) { final int currOffset = getTopAndBottomOffset(); if ((dy < 0 && currOffset == 0 ) || (dy > 0 && currOffset == -child.getTotalScrollRange())) { ViewCompat.stopNestedScroll(target, ViewCompat.TYPE_NON_TOUCH); } } } }
java.lang.IllegalArgumentException: Called attach on a child which is not detached 报错日志如下:
1 2 java.lang.IllegalArgumentException: Called attach on a child which is not detached: c419ef6e position=11 id=-1, oldPos=-1, pLpos:-1 ...
出现这个问题的原因是更新了不在屏幕中显示的 item。操作的这个 ViewHolder 当前不是被绑定的,因为 RecyclerView 有缓存机制,未在屏幕上显示的 item 会被暂时回收,即 detached。
出现这个问题的原因是更新了不在屏幕中显示的 item。解决办法是判断要更新的 item 是不是在屏幕中,判断方法是获取 RecyclerView 的 LayoutManager,前提是 RecyclerView 设置的 LayoutManager 是 LinearLayoutManager。获取第一个可见位置和最后一个可见位置的 position,判断当前要更新的 item 的 position 在这个范围内才更新。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @Override public void attachViewToParent (View child, int index, ViewGroup.LayoutParams layoutParams) { final ViewHolder vh = getChildViewHolderInt(child); if (vh != null ) { if (!vh.isTmpDetached() && !vh.shouldIgnore()) { throw new IllegalArgumentException ("Called attach on a child which is not" + " detached: " + vh + exceptionLabel()); } vh.clearTmpDetachFlag(); } RecyclerView.this .attachViewToParent(child, index, layoutParams); } @Override public void detachViewFromParent (int offset) { final View view = getChildAt(offset); if (view != null ) { final ViewHolder vh = getChildViewHolderInt(view); if (vh != null ) { if (vh.isTmpDetached() && !vh.shouldIgnore()) { throw new IllegalArgumentException ("called detach on an already" + " detached child " + vh + exceptionLabel()); } vh.addFlags(ViewHolder.FLAG_TMP_DETACHED); } } RecyclerView.this .detachViewFromParent(offset); }
相关工具类 DiffUtil SortedList 适用于列表有序的场景(城市列表页,中文首字母排序)。并且 SortedList 会帮助你比较数据的差异,定向刷新数据,而不是简单粗暴的 notifyDataSetChanged()。
AsyncListUtil 用于异步加载数据,我们无需在 UI 线程上查询游标,同时它可以保持 UI 和缓存同步,并且始终只在内存中保留有限数量的数据。使用它可以获得更好的用户体验。 这个类使用单线程来加载数据,因此它适合从磁盘、数据库加载数据,不适用于从网络加载数据。
AsyncListDiffer SnapHelper 用于辅助 RecyclerView 在滚动结束时将 Item 对齐到某个位置。
参考 [1] Developer Docs [2] 【腾讯 Bugly 干货分享】Android ListView 与 RecyclerView 对比浅析—缓存机制 [3] 腾讯 BuglyRecyclerView 必知必会