最全面的RecyclerView源码解析(一)
来源:互联网 发布:安卓开发艺术探索源码 编辑:程序博客网 时间:2024/05/17 03:30
相信很多人用RecyclerView已经很久了,但还是不得不感叹 RecyclerView的强大,性能、扩展性等方面都很强大。网上看了很多源码方面对RecyclerView,觉得还不够全面,而且自己不走一遍源码总感觉会很容易忘记。
打开RecyclerView类,发现有11090行代码,千万不要震惊,慢慢看。
整体看了下RecyclerView的架构,你会惊奇于这个优雅的设计,高度解耦,灵活性很强,给开发者一种插拔式的体验,使用者只要通过设置不同的LayoutManager, ItemDecoration, ItemAnimator就可以实现各种各样的效果了。
Recycler
打开RecyclerView的结构,复杂,我们就从上往下说好了。首先来分析Recycler这个内部类,Recycler是整个RecyclerView的精髓所在,那么Recycler到底是什么呢:
Recycler的职责是管理那些已经废弃了的或者从RecyclerView中分离的item view用于复用。
废弃的View是指那些仍然依附于RecyclerView但是已经被标记为可以被移除或者复用的View。
Recycler典型的用法就是当LayoutManager去获取Adapter中的某一项View的时候,如果这个View失效了,则需要重新绑定View,当复用的View是有效的话,View就会被直接被复用。有效的View如果不主动调用requestLayout,那么该View不需要重新测量就可以被复用。
说了一大堆,还是边看代码边解释更加清晰一点,首先来看Recycler几个成员变量:
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();private ArrayList<ViewHolder> mChangedScrap = null;final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();private final List<ViewHolder> mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);private int mViewCacheMax = DEFAULT_CACHE_SIZE;private RecycledViewPool mRecyclerPool;private ViewCacheExtension mViewCacheExtension;
先来看RecycledViewPool和ViewCacheExtension这两个类:
RecycledViewPool让开发者可以在多个RecyclerView之间共享View。
如果你想要垮RecyclerView复用View,创建一个RecycledViewPool实例,然后调用setRecycledViewPool(RecycledViewPool)方法就可以了。
RecyclerView会自动创建一个RecycledViewPool的实例。
有了RecycledViewPool的存在,就能很大程度上减少View的创建,提高性能。
先看下面这两个方法:
private SparseArray<ArrayList<ViewHolder>> mScrap = new SparseArray<ArrayList<ViewHolder>>(); private SparseIntArray mMaxScrap = new SparseIntArray();//此处省略部分代码 //……public void setMaxRecycledViews(int viewType, int max) { mMaxScrap.put(viewType, max); final ArrayList<ViewHolder> scrapHeap = mScrap.get(viewType); if (scrapHeap != null) { while (scrapHeap.size() > max) { scrapHeap.remove(scrapHeap.size() - 1); } } }public ViewHolder getRecycledView(int viewType) { final ArrayList<ViewHolder> scrapHeap = mScrap.get(viewType); if (scrapHeap != null && !scrapHeap.isEmpty()) { final int index = scrapHeap.size() - 1; final ViewHolder scrap = scrapHeap.get(index); scrapHeap.remove(index); return scrap; } return null; }
可以看到,mScrap是一个《viewType, List>的映射,mMaxScrap是一个《viewType, maxNum>的映射,我们可以调用setMaxRecycledViews方法来设置每种viewType的view容量。从源码可以看出,如果viewType类型的list的size大于制定的最大数字的话,会先从列表的末尾开始丢弃超出的部分。调用getRecycledView(int viewType)方法呢,可以将scrapHeap中的最后一项移除并返回viewType对应的List的末尾项。这里需要注意的是,因为是跨RecyclerView进行操作,所以要特别注意对于同一个RecycledViewPool,对ViewType的定义要统一,因为这里是根据viewType来取ViewHolder的。
再来看ViewCacheExtension这个类:
public abstract static class ViewCacheExtension { abstract public View getViewForPositionAndType(Recycler recycler, int position, int type); }
ViewCacheExtension是一个帮助类,给开发者提供了一个可以由他们自己控制View缓存的这么一个类。当调用Recycler的getViewForPosition(int position)方法等时候,Recycler会先去检查attached scrap和一级缓存来这个View,如果都找不到匹配的View的话,Recycler会调用ViewCacheExtension的getViewForPositionAndType方法检查,如果还是没有,再去检查RecycledViewPool。
需要注意的是:Recycler不会在这个类中做任何缓存View的操作,是否需要缓存View由开发者自己控制。
然后回到Recycler,Recycler最终目的还是要去获取到来看这个方法:
View getViewForPosition(int position, boolean dryRun) { if (position < 0 || position >= mState.getItemCount()) { throw new IndexOutOfBoundsException("Invalid item position " + position + "(" + position + "). Item count:" + mState.getItemCount()); } boolean fromScrap = false; ViewHolder holder = null; // 0) If there is a changed scrap, try to find from there if (mState.isPreLayout()) { holder = getChangedScrapViewForPosition(position); fromScrap = holder != null; } // 1) Find from scrap by position if (holder == null) { holder = getScrapViewForPosition(position, INVALID_TYPE, dryRun); if (holder != null) { if (!validateViewHolderForOffsetPosition(holder)) { // recycle this scrap if (!dryRun) { // we would like to recycle this but need to make sure it is not used by // animation logic etc. holder.addFlags(ViewHolder.FLAG_INVALID); if (holder.isScrap()) { removeDetachedView(holder.itemView, false); holder.unScrap(); } else if (holder.wasReturnedFromScrap()) { holder.clearReturnedFromScrapFlag(); } recycleViewHolderInternal(holder); } holder = null; } else { fromScrap = true; } } } if (holder == null) { final int offsetPosition = mAdapterHelper.findPositionOffset(position); if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) { throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item " + "position " + position + "(offset:" + offsetPosition + ")." + "state:" + mState.getItemCount()); } final int type = mAdapter.getItemViewType(offsetPosition); // 2) Find from scrap via stable ids, if exists if (mAdapter.hasStableIds()) { holder = getScrapViewForId(mAdapter.getItemId(offsetPosition), type, dryRun); if (holder != null) { // update position holder.mPosition = offsetPosition; fromScrap = true; } } if (holder == null && mViewCacheExtension != null) { // We are NOT sending the offsetPosition because LayoutManager does not // know it. final View view = mViewCacheExtension .getViewForPositionAndType(this, position, type); if (view != null) { holder = getChildViewHolder(view); if (holder == null) { throw new IllegalArgumentException("getViewForPositionAndType returned" + " a view which does not have a ViewHolder"); } else if (holder.shouldIgnore()) { throw new IllegalArgumentException("getViewForPositionAndType returned" + " a view that is ignored. You must call stopIgnoring before" + " returning this view."); } } } if (holder == null) { // fallback to recycler // try recycler. // Head to the shared pool. if (DEBUG) { Log.d(TAG, "getViewForPosition(" + position + ") fetching from shared " + "pool"); } holder = getRecycledViewPool().getRecycledView(type); if (holder != null) { holder.resetInternal(); if (FORCE_INVALIDATE_DISPLAY_LIST) { invalidateDisplayListInt(holder); } } } if (holder == null) { holder = mAdapter.createViewHolder(RecyclerView.this, type); if (DEBUG) { Log.d(TAG, "getViewForPosition created new ViewHolder"); } } } // This is very ugly but the only place we can grab this information // before the View is rebound and returned to the LayoutManager for post layout ops. // We don't need this in pre-layout since the VH is not updated by the LM. if (fromScrap && !mState.isPreLayout() && holder .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) { holder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST); if (mState.mRunSimpleAnimations) { int changeFlags = ItemAnimator .buildAdapterChangeFlagsForAnimations(holder); changeFlags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT; final ItemHolderInfo info = mItemAnimator.recordPreLayoutInformation(mState, holder, changeFlags, holder.getUnmodifiedPayloads()); recordAnimationInfoIfBouncedHiddenView(holder, info); } } boolean bound = false; if (mState.isPreLayout() && holder.isBound()) { // do not update unless we absolutely have to. holder.mPreLayoutPosition = position; } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) { if (DEBUG && holder.isRemoved()) { throw new IllegalStateException("Removed holder should be bound and it should" + " come here only in pre-layout. Holder: " + holder); } final int offsetPosition = mAdapterHelper.findPositionOffset(position); holder.mOwnerRecyclerView = RecyclerView.this; mAdapter.bindViewHolder(holder, offsetPosition); attachAccessibilityDelegate(holder.itemView); bound = true; if (mState.isPreLayout()) { holder.mPreLayoutPosition = position; } } final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams(); final LayoutParams rvLayoutParams; if (lp == null) { rvLayoutParams = (LayoutParams) generateDefaultLayoutParams(); holder.itemView.setLayoutParams(rvLayoutParams); } else if (!checkLayoutParams(lp)) { rvLayoutParams = (LayoutParams) generateLayoutParams(lp); holder.itemView.setLayoutParams(rvLayoutParams); } else { rvLayoutParams = (LayoutParams) lp; } rvLayoutParams.mViewHolder = holder; rvLayoutParams.mPendingInvalidate = fromScrap && bound; return holder.itemView; }
从代码中可以看出,要获取到某个position下的view,有这么一个过程:
首先Recycler会先去检查mChangedScrap,如果匹配成功则返回相应的viewHolder。mChangedScrap就是所谓的detachedView,与RecyclerView分离的viewHolder列表。
然后是mAttachedScrap,mViewCacheExtension,mRecyclerPool,如果都没有,则去创建,调用Adapter.createViewHolder()。
其实相当于RecyclerView也做到了二级缓存的概念,mCachedViews是一层,默认大小是DEFAULT_CACHE_SIZE = 2, 还有一层就是RecycledViewPool。我们可以调用这个方法来控制第一层的缓存数量:
public void setViewCacheSize(int viewCount) { mViewCacheMax = viewCount; // first, try the views that can be recycled for (int i = mCachedViews.size() - 1; i >= 0 && mCachedViews.size() > viewCount; i--) { recycleCachedViewAt(i); } }
这就是Recycler去根据position获取相应的itemView的过程,既然有获取,那么就肯定有放入,来看这个方法:
void recycleViewHolderInternal(ViewHolder holder) { //省略一部分代码。。。 if (forceRecycle || holder.isRecyclable()) { if (!holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_UPDATE)) { // Retire oldest cached view final int cachedViewSize = mCachedViews.size(); if (cachedViewSize == mViewCacheMax && cachedViewSize > 0) { recycleCachedViewAt(0); } if (cachedViewSize < mViewCacheMax) { mCachedViews.add(holder); cached = true; } } if (!cached) { addViewHolderToRecycledViewPool(holder); recycled = true; } } else if (DEBUG) { Log.d(TAG, "trying to recycle a non-recycleable holder. Hopefully, it will " + "re-visit here. We are still removing it from animation lists"); } mViewInfoStore.removeViewHolder(holder); if (!cached && !recycled && transientStatePreventsRecycling) { holder.mOwnerRecyclerView = null; } }
这个方法其实是先去判断了下mCachedViews是否已经满了,如果满了,则移除一个到recycledViewPool中去,然后将新的itemView添加到mCachedViews,如果没满那最好了,直接添加。
LayoutManager
LayoutManager的主要职责就是去测量和摆放itemView和当itemView对用户来说不再能看见的时候去复用它。LayoutManager牵扯到的东西其实也蛮多的,我特意去翻看了下LinearLayoutManager的源码,挺复杂的,就不再这篇讲了,到时候另起一篇来讲下这个东西。
可以看到,RecyclerView的onMeasure和onLayout方法都是交给了LayoutManager来处理。
@Override protected void onMeasure(int widthSpec, int heightSpec) { //省略一段代码 if (mLayout.mAutoMeasure) { final int widthMode = MeasureSpec.getMode(widthSpec); final int heightMode = MeasureSpec.getMode(heightSpec); final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY; mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); if (skipMeasure || mAdapter == null) { return; } if (mState.mLayoutStep == State.STEP_START) { dispatchLayoutStep1(); } mLayout.setMeasureSpecs(widthSpec, heightSpec); //省略一段代码 } else { if (mHasFixedSize) { mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); return; } //省略一段代码。。。。。。 if (mAdapter != null) { mState.mItemCount = mAdapter.getItemCount(); } else { mState.mItemCount = 0; } eatRequestLayout(); mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); resumeRequestLayout(false); mState.mInPreLayout = false; } }
滑动
RecyclerView的onTouchEvent方法比较长,这里就不贴了。其实滑动的套路都差不多的,RecyclerView也一样。那就讲一下我看了源码后的理解吧,这里以垂直方向的滑动为力。
在ACTION_MOVE中,首先计算出手指滑动的距离,先跟TouchSlop进行比较,如果大雨这个值,那么就是确定为师滑动了,而不是点击,然后调用scrollByInternal()方法,scrollByInternal其实最后还是用了scrollBy方法,拖拽性质的滑动就解决了。当然我们也发现当我们拖拽RecyclerView后,当我们的手指离开屏幕以后,RecyclerView还会滑动一段时间,这个其实就是fling事件。RecyclerView在ACTION_UP事件的时候,VelocityTracker根据拖拽时候滑动的距离和时间计算出一个速度,然后调用fling方法就行了,其实fling里面还是用Scrooler来实现的。这里贴下fling方法的代码:
public void fling(int velocityX, int velocityY) { setScrollState(SCROLL_STATE_SETTLING); mLastFlingX = mLastFlingY = 0; mScroller.fling(0, 0, velocityX, velocityY, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE); postOnAnimation();}
源码这个东西真的是越看越多,尤其RecyclerView感觉更是。还是分篇写吧打算,下一篇继续分析下LayoutManager,ViewHolder,ItemDecoration这些。第一次分析源码,如果有错误的地方请及时指出
- 最全面的RecyclerView源码解析(一)
- RecyclerView(一)最简单的recyclerview
- RecyclerView的使用全面解析
- RecyclerView 解析(一)
- RecyclerView.ItemAnimator终极解读(一)--RecyclerView源码解析
- android Fragmentation框架源码全面解析一
- Android-RecyclerView(解析一)
- ListView、AdapterView、RecyclerView全面解析
- ListView、AdapterView、RecyclerView全面解析
- 最全面的toolbar自定义源码
- service全面解析(一)
- 最全面的linux信号量解析
- 最全面的linux信号量解析
- 最全面的linux信号量解析
- 最全面的linux信号量解析
- 嵌入式 最全面的linux信号量解析
- 最全面的linux信号量解析
- 最全面的Java多线程用法解析
- 工具网站
- 初学者svn项目发布和下载和如果项目不全
- swift学习一月总结
- bootstrap modal模态窗口拖拽功能
- 【Tomcat】Filter 原理
- 最全面的RecyclerView源码解析(一)
- 第90篇一对多之学生举手提问及老师点课堂模式掉线
- Unity 光标控制进度条Slider
- 【HDU4035】Maze-期望DP+树形DP
- EB propsim F8
- javascript 剑指offer
- ubuntu下安装并配置Nginx、php、mysql
- ORACLE 中ROWNUM用法总结!
- 工资读取excel表格发送工具