文章目录

  • 概述
  • 源码探究
    • ViewHolder的存储
      • 布局期间
      • mCachedViews
      • RecycledViewPool
      • mAttachedScrap、mChangedScrap
      • 滚动期间
    • ViewHolder的获取
      • 从缓存集合中查找
      • 新建ViewHolder
      • 绑定View
  • 总结

概述

通过博文记录RecyclerView的源码学习过程有助于巩固自己的记忆和加深整体实现机制的理解。

接《AndroidX RecyclerView总结-测量布局》,其中LinearLayoutManager在布局时,通过Recycler来获取ViewHolder中的itemView进行添加和布局。众所周知,Recycler负责缓存ViewHolder以供复用,这里通过追踪源码看看Recycler的工作机制。

源码探究

文中源码基于 ‘androidx.recyclerview:recyclerview:1.1.0’

Recycler使用了多个缓存集合进行多级缓存,接下来从LinearLayoutManager的布局过程中看Recycler对ViewHolder的缓存和获取的工作流程。

ViewHolder的存储

先从ViewHolder存储过程入手,看看各个缓存集合的作用。

布局期间

在LinearLayoutManager的布局方法onLayoutChildren中,在确定锚点之后填充布局之前,会调用detachAndScrapAttachedViews方法进行临时回收当前RecyclerView上attached的View对应的ViewHolder。

[LinearLayoutManager#onLayoutChildren]

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {// ···onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);// 该方法中进行回收操作detachAndScrapAttachedViews(recycler);// ···if (mAnchorInfo.mLayoutFromEnd) {// ···// 填充布局fill(recycler, mLayoutState, state, false);// ···} else {// ···fill(recycler, mLayoutState, state, false);// ···}// ···
}

detachAndScrapAttachedViews方法中遍历child,依次调用scrapOrRecycleView方法:
[LinearLayoutManager#scrapOrRecycleView]

private void scrapOrRecycleView(Recycler recycler, int index, View view) {// 获取该view对应的ViewHolderfinal ViewHolder viewHolder = getChildViewHolderInt(view);if (viewHolder.shouldIgnore()) {if (DEBUG) {Log.d(TAG, "ignoring view " + viewHolder);}return;}// 判断是否ViewHolder的item数据被标记无效但还未从适配器数据集中移除。hasStableIds默认返回falseif (viewHolder.isInvalid() && !viewHolder.isRemoved()&& !mRecyclerView.mAdapter.hasStableIds()) {// 将触发detachedFromWindow和ViewGroup.removeViewAt方法removeViewAt(index);// 添加至mCachedViews或RecycledViewPool中缓存recycler.recycleViewHolderInternal(viewHolder);} else {// 将为index对应的viewHolder添加FLAG_TMP_DETACHED标记,触发ViewGroup.detachViewFromParent方法detachViewAt(index);// 添加至mAttachedScrap或mChangedScrap中缓存recycler.scrapView(view);mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);}
}

当重新设置Adapter或调用Adapter的notifyDataSetChanged方法会给ViewHolder标记FLAG_INVALID,需要完全重新绑定View。当移除Adapter中数据集的某个item数据时会给对应的ViewHolder标记FLAG_REMOVED,但它绑定的View可能仍然需要保留以用于item动画。同时满足以上情况时调用recycleViewHolderInternal方法进行缓存,否则调用scrapView缓存。

这里的scrap和recycle是两种不同行为。scrap表示着View仍然在RecyclerView上,只是临时detach,稍后会再attach回来。recycle意味着View将移出RecyclerView,缓存ViewHolder实例,可能不用重新绑定View,但是对应的索引位置将不一致。

mCachedViews

recycleViewHolderInternal方法主要是对要移出RecyclerView的ViewHolder,或是item数据彻底无效或彻底移除的ViewHolder进行缓存。当RecyclerView上下滑动或item消失动画结束或彻底移除适配器数据集中对应的item等情况都会调用该方法进行回收。

[RecyclerView#recycleViewHolderInternal]

void recycleViewHolderInternal(ViewHolder holder) {// 省略异常检查部分 ···final boolean transientStatePreventsRecycling = holder.doesTransientStatePreventRecycling();@SuppressWarnings("unchecked")final boolean forceRecycle = mAdapter != null&& transientStatePreventsRecycling&& mAdapter.onFailedToRecycleView(holder);boolean cached = false;boolean recycled = false;// ···// 判断是否强制回收或ViewHolder设置可回收,默认为trueif (forceRecycle || holder.isRecyclable()) {// mViewCacheMax默认为2,判断ViewHolder是否需要重新绑定if (mViewCacheMax > 0&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID| ViewHolder.FLAG_REMOVED| ViewHolder.FLAG_UPDATE| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {// Retire oldest cached viewint cachedViewSize = mCachedViews.size();// 判断mCachedViews容量是否已满if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {// 移除第一个添加的ViewHolder,并通过addViewHolderToRecycledViewPool方法将其转移到RecycledViewPool中recycleCachedViewAt(0);cachedViewSize--;}int targetCacheIndex = cachedViewSize;if (ALLOW_THREAD_GAP_WORK&& cachedViewSize > 0&& !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {// when adding the view, skip past most recently prefetched viewsint cacheIndex = cachedViewSize - 1;while (cacheIndex >= 0) {int cachedPos = mCachedViews.get(cacheIndex).mPosition;if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {break;}cacheIndex--;}targetCacheIndex = cacheIndex + 1;}// 添加到mCachedViews中mCachedViews.add(targetCacheIndex, holder);cached = true;}if (!cached) {// 若不满足添加进mCachedViews的条件,则添加进RecycledViewPool中addViewHolderToRecycledViewPool(holder, true);recycled = true;}} else {// DEBUG···}// ···
}

这里将无需重新绑定View的ViewHolder保存在mCachedViews中,若mCachedViews容量不足(默认上限2),则将最早添加的转移到RecycledViewPool中。
若不满足添加进mCachedViews的条件,则将ViewHolder添加进RecycledViewPool。

RecycledViewPool

在addViewHolderToRecycledViewPool方法中通过getRecycledViewPool方法获取RecycledViewPool实例:
[RecyclerView#getRecycledViewPool]

RecycledViewPool getRecycledViewPool() {if (mRecyclerPool == null) {mRecyclerPool = new RecycledViewPool();}return mRecyclerPool;
}

若mRecyclerPool已经存在,则直接返回。mRecyclerPool也可通过RecyclerView.setRecycledViewPool方法传入一个实例获得,从而支持多个RecyclerView共用一个RecycledViewPool

这里先看一下RecycledViewPool的主要结构:
[RecycledViewPool]

public static class RecycledViewPool {private static final int DEFAULT_MAX_SCRAP = 5;static class ScrapData {final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();int mMaxScrap = DEFAULT_MAX_SCRAP;// ···}SparseArray<ScrapData> mScrap = new SparseArray<>();// ···
}

RecycledViewPool的mScrap成员以ViewHolder的viewType为key,ScrapData为value。ScrapData中持有一个ViewHolder集合,集合的容量是5。当添加ViewHolder时,需要先取出对应viewType的ScrapData。
(可以理解为类似Map/<Integer, list//>的集合)

在取得RecycledViewPool实例后,调用它的putRecycledView方法进行添加:
[RecycledViewPool#putRecycledView]

public void putRecycledView(ViewHolder scrap) {// 获取该ViewHolder的viewTypefinal int viewType = scrap.getItemViewType();// 获取viewType对应的ViewHolder集合final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;// 判断是否达到容量上限if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {return;}if (DEBUG && scrapHeap.contains(scrap)) {throw new IllegalArgumentException("this scrap item already exists");}// 重置ViewHolder中的数据scrap.resetInternal();// 添加集合保存scrapHeap.add(scrap);
}

RecycledViewPool的作用是一个对象缓存池,避免频繁创建ViewHolder,但是ViewHolder仍然需要重新进行View绑定。

mAttachedScrap、mChangedScrap

回到scrapOrRecycleView方法中,正常布局情况下会进入Recycler的scrapView方法:
[Recycler#scrapView]

void scrapView(View view) {// 获取该view对应的ViewHolderfinal ViewHolder holder = getChildViewHolderInt(view);// 判断存到哪个集合中if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)|| !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {throw new IllegalArgumentException("Called scrap view with an invalid view."+ " Invalid views cannot be reused from scrap, they should rebound from"+ " recycler pool." + exceptionLabel());}holder.setScrapContainer(this, false);mAttachedScrap.add(holder);} else {if (mChangedScrap == null) {mChangedScrap = new ArrayList<ViewHolder>();}holder.setScrapContainer(this, true);mChangedScrap.add(holder);}
}

mAttachedScrap和mChangedScrap都用于缓存RecyclerView上临时detach的ViewHolder。区别是mAttachedScrap保存的是没有变化的ViewHolder,mChangedScrap保存的是有变化的,例如调用了Adapter.notifyItemRangeChanged方法。

滚动期间

RecyclerView触发滚动时会调用LinearLayoutManager的scrollBy方法:
[LinearLayoutManager#scrollBy]

int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {// ···mLayoutState.mRecycle = true;// ···final int consumed = mLayoutState.mScrollingOffset+ fill(recycler, mLayoutState, state, false);// ···
}

该方法中先将mRecycle标记为true(默认为false,其他场景也都为false),之后通过fill方法进行填充。

在fill方法中会判断是否存在滚动,并对移除屏幕的View进行回收缓存:
[LinearLayoutManager#fill]

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,RecyclerView.State state, boolean stopOnFocusable) {// ···// 判断是否滚动if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {// TODO ugly bug fix. should not happenif (layoutState.mAvailable < 0) {layoutState.mScrollingOffset += layoutState.mAvailable;}// 检查回收recycleByLayoutState(recycler, layoutState);}// ···while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {// ···layoutChunk(recycler, state, layoutState, layoutChunkResult);// ···if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {layoutState.mScrollingOffset += layoutChunkResult.mConsumed;if (layoutState.mAvailable < 0) {layoutState.mScrollingOffset += layoutState.mAvailable;}// 检查回收recycleByLayoutState(recycler, layoutState);}}// ···
}

在开始循环填充View前,先检查上下滑动的回收,在每填充一个View后也进行一次检查回收。

recycleByLayoutState方法中会判断mRecycle变量是否为false,该变量默认为true,但在前面的scrollBy中会设置为false。接着根据布局方向从顶部或底部回收ViewHolder,通过for循环逐个回收离开屏幕的View,回收的代码在removeAndRecycleViewAt方法中。

进入removeAndRecycleViewAt方法:
[LinearLayoutManager#removeAndRecycleViewAt]

public void removeAndRecycleViewAt(int index, @NonNull Recycler recycler) {final View view = getChildAt(index);// 从ViewGroup移除ViewremoveViewAt(index);// 使用Recycler进行回收recycler.recycleView(view);
}

在recycleView方法中会进行一些回调和清理工作,并调用recycleViewHolderInternal方法回收ViewHolder,保存进mCachedViews或RecycledViewPool中。

ViewHolder的获取

接着看获取缓存的工作流程,看看各缓存集合的读取优先级。LinearLayoutManager在layoutChunk方法中进行单个View的添加和布局,该方法中首先通过LayoutState的next方法获取View,而next方法中又调用Recycler的getViewForPosition方法并传入当前适配器item数据的索引:

[Recycler#getViewForPosition]

public View getViewForPosition(int position) {return getViewForPosition(position, false);
}View getViewForPosition(int position, boolean dryRun) {return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}

通过tryGetViewHolderForPositionByDeadline获取到ViewHolder后,取里面的itemView返回。

tryGetViewHolderForPositionByDeadline方法较长,这里分成几部分看:

从缓存集合中查找

[Recycler#tryGetViewHolderForPositionByDeadline]

ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) {// 检查索引越界if (position < 0 || position >= mState.getItemCount()) {throw new IndexOutOfBoundsException("Invalid item position " + position+ "(" + position + "). Item count:" + mState.getItemCount()+ exceptionLabel());}boolean fromScrapOrHiddenOrCache = false;ViewHolder holder = null;// 0) If there is a changed scrap, try to find from thereif (mState.isPreLayout()) {// 如果是预布局则从mChangedScrap中查找holder = getChangedScrapViewForPosition(position);fromScrapOrHiddenOrCache = holder != null;}// 1) Find by position from scrap/hidden list/cacheif (holder == null) {// 先从mAttachedScrap中查找,再从ChildHelper的mHiddenViews(保留用于动画的View)// 中查找,再从mCachedViews中查找holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);if (holder != null) {// 检查ViewHolder是否无效if (!validateViewHolderForOffsetPosition(holder)) {// recycle holder (and unscrap if relevant) since it can't be usedif (!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();}// 重新回收该ViewHolderrecycleViewHolderInternal(holder);}holder = null;} else {fromScrapOrHiddenOrCache = true;}}}// ···
}

优先mChangedScrap或mAttachedScrap中查找,找不到再从mCachedViews中查找。

[Recycler#tryGetViewHolderForPositionByDeadline]

ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) {// ···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() + exceptionLabel());}final int type = mAdapter.getItemViewType(offsetPosition);// 2) Find from scrap/cache via stable ids, if exists// hasStableIds默认返回falseif (mAdapter.hasStableIds()) {// 先从mAttachedScrap中查找,再从mCachedViews中查找holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),type, dryRun);if (holder != null) {// update positionholder.mPosition = offsetPosition;fromScrapOrHiddenOrCache = true;}}if (holder == null && mViewCacheExtension != null) {// We are NOT sending the offsetPosition because LayoutManager does not// know it.// 从ViewCacheExtension中查找,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"+ exceptionLabel());} else if (holder.shouldIgnore()) {throw new IllegalArgumentException("getViewForPositionAndType returned"+ " a view that is ignored. You must call stopIgnoring before"+ " returning this view." + exceptionLabel());}}}if (holder == null) { // fallback to poolif (DEBUG) {Log.d(TAG, "tryGetViewHolderForPositionByDeadline("+ position + ") fetching from shared pool");}// 从RecycledViewPool中查找holder = getRecycledViewPool().getRecycledView(type);if (holder != null) {holder.resetInternal();if (FORCE_INVALIDATE_DISPLAY_LIST) {invalidateDisplayListInt(holder);}}}if (holder == null) {long start = getNanoTime();if (deadlineNs != FOREVER_NS&& !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {// abort - we have a deadline we can't meetreturn null;}// 触发Adapter的onCreateViewHolder回调创建ViewHolderholder = mAdapter.createViewHolder(RecyclerView.this, type);if (ALLOW_THREAD_GAP_WORK) {// only bother finding nested RV if prefetchingRecyclerView innerView = findNestedRecyclerView(holder.itemView);if (innerView != null) {holder.mNestedRecyclerView = new WeakReference<>(innerView);}}long end = getNanoTime();mRecyclerPool.factorInCreateTime(type, end - start);if (DEBUG) {Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder");}}}// ···
}

缓存集合查找优先级:mAttachedScrap->mCachedViews->ViewCacheExtension->RecycledViewPool

  • ViewCacheExtension说明
    ViewCacheExtension抽象类需要开发者继承,实现getViewForPositionAndType方法,完成具体的缓存策略。RecyclerView.mViewCacheExtension默认为null,通过RecyclerView.setViewCacheExtension方法设置。

新建ViewHolder

当从以上缓存集合中都没有找到可用的ViewHolder后,会Adapter.createViewHolder方法进行创建。

[Adapter#createViewHolder]

public final VH createViewHolder(@NonNull ViewGroup parent, int viewType) {try {TraceCompat.beginSection(TRACE_CREATE_VIEW_TAG);// onCreateViewHolder由开发者实现,返回具体的ViewHolderfinal VH holder = onCreateViewHolder(parent, viewType);if (holder.itemView.getParent() != null) {throw new IllegalStateException("ViewHolder views must not be attached when"+ " created. Ensure that you are not passing 'true' to the attachToRoot"+ " parameter of LayoutInflater.inflate(..., boolean attachToRoot)");}holder.mItemViewType = viewType;return holder;} finally {TraceCompat.endSection();}
}

触发了onCreateViewHolder回调方法返回ViewHolder。

绑定View

[Recycler#tryGetViewHolderForPositionByDeadline]

ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) {// ···boolean bound = false;// 判断是否需要进行View绑定操作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+ exceptionLabel());}final int offsetPosition = mAdapterHelper.findPositionOffset(position);// 绑定Viewbound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);}// 生成RecyclerView.LayoutParams,并使之和ViewHolder互相持有引用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 = fromScrapOrHiddenOrCache && bound;return holder;// ···
}

若判断ViewHolder未绑定或需要重新绑定,则调用tryBindViewHolderByDeadline方法进行绑定:
[Recycler#tryBindViewHolderByDeadline]

private boolean tryBindViewHolderByDeadline(@NonNull ViewHolder holder, int offsetPosition,int position, long deadlineNs) {holder.mOwnerRecyclerView = RecyclerView.this;final int viewType = holder.getItemViewType();long startBindNs = getNanoTime();if (deadlineNs != FOREVER_NS&& !mRecyclerPool.willBindInTime(viewType, startBindNs, deadlineNs)) {// abort - we have a deadline we can't meetreturn false;}// 调用Adapter的bindViewHolder方法进行绑定mAdapter.bindViewHolder(holder, offsetPosition);long endBindNs = getNanoTime();mRecyclerPool.factorInBindTime(holder.getItemViewType(), endBindNs - startBindNs);attachAccessibilityDelegateOnBind(holder);if (mState.isPreLayout()) {holder.mPreLayoutPosition = position;}return true;
}

该方法中调用Adapter的bindViewHolder方法,bindViewHolder又调用Adapter的onBindViewHolder回调方法,又开发者实现具体的绑定逻辑。

总结

在Recycler中的缓存集合:

  • mAttachedScrap、mChangedScrap
    填充布局前缓存当前RecyclerView上的ViewHolder,ViewHolder会短暂detach,不会remove。mAttachedScrap和mChangedScrap的区别是mChangedScrap保存需要局部刷新的ViewHolder,例如Adapter.notifyItemRangeChanged指定范围之间的。查找优先级最高。

  • mCachedViews
    RecyclerView上下滑动或Adapter数据集变更或item移出等导致ViewHolder索引位置失效或item内容变化等数据无效的情况下缓存ViewHolder。容量上限默认是2。优先级次高。

  • RecycledViewPool
    当往mCachedViews添加但mCachedViews超限时,会将mCachedViews里面最早添加的ViewHolder转存到RecycledViewPool中,同时重置该ViewHolder的数据。RecycledViewPool中根据viewType区分保存ViewHolder,每种viewType的默认存储上限是5。可以多个RecyclerView共用一个RecycledViewPool。优先级最低。

  • ViewCacheExtension
    默认为空,需要开发者自行继承实现,只有获取缓存时调用,返回缓存View。优先级介于mCachedViews和RecycledViewPool之间。

当没有可用缓存时,会通过Adapter的onCreateViewHolder回调返回开发者创建的ViewHolder。在获取到ViewHolder后,判断该ViewHolder是否未绑定View(新创建的)或需要重新绑定View(数据无效、数据重置),若要绑定的话,通过Adapter的onBindViewHolder回调执行开发者实现的具体绑定逻辑。

AndroidX RecyclerView总结-Recycler相关推荐

  1. AndroidX RecyclerView总结-滑动处理

    文章目录 概述 源码探究 RecyclerView中的处理 onInterceptTouchEvent 滑动状态 onTouchEvent scrollStep LinearLayoutManager ...

  2. Android之提示androidx.recyclerview.widget.LinearLayoutManager@51ddcd is already attached to a RecyclerV

    1 问题 我有2个recycleView,但是用的同一个线性布局导致错误如下 LayoutManager androidx.recyclerview.widget.LinearLayoutManage ...

  3. androidx.recyclerview:recyclerview的使用

    添加扩展 或手动修改app/build.gradle: 在dependencies里添加 implementation 'androidx.recyclerview:recyclerview:1.0. ...

  4. RecyclerView的Recycler

    处理回收复用相关工作的 参考博客:https://www.jianshu.com/p/9306b365da57 几个重要的集合 mAttachedScrap 缓存显示到屏幕的item的holder.临 ...

  5. RecyclerView安卓androidx.widget.RecyclerView

    导入RecyclerView implementation 'com.android.support:recyclerview-v7:28.0.0' 或者把常用的组件都导入了,包括了RecyclerV ...

  6. 【JetPack】视图绑定 ( ViewBinding ) 各种应用 ( 视图绑定两种方式 | Activity 布局 | 对话框布局 | 自定义组件布局 | RecyclerView 列表布局 )

    文章目录 I . 视图绑定 ( ViewBinding ) 界面的两种方式 II . Activity 界面中 应用 视图绑定 ( ViewBinding ) III . Dialog 对话框界面中 ...

  7. recyclerview item点击无效_让你彻底掌握RecyclerView的缓存机制

    点击上方蓝字关注 ?? 来源:肖邦kakahttps://www.jianshu.com/p/3e9aa4bdaefd 前言 RecyclerView这个控件几乎所有的Android开发者都使用过(甚 ...

  8. viewpager 自定义翻页效果_Android RecyclerView自定义LayoutManager

    在第一篇中已经讲过,LayoutManager主要用于布局其中的Item,在LayoutManager中能够对每个Item的大小,位置进行更改,将它放在我们想要的位置,在很多优秀的效果中,都是通过自定 ...

  9. android zxing-3.3.3的用法_Android使用RecyclerView实现复杂的多布局效果

    引言 RecyclerView不只能进行单页面布局,而且可以通过重写adapter中的getItemViewType方法来进行多种View布局,今天就来使用RecyclerView实现复杂的多布局效果 ...

最新文章

  1. 修复Long类型太长转为JSON格式的时候出错的问题
  2. linux tar命令详解,Linux tar命令详解
  3. what kind of supervision will i do next semester?
  4. java uri_Android中的Uri与Java中的URI类
  5. 如何解决U盘无法停止通用卷设备
  6. 美团数据平台Kerberos优化实战
  7. python模块分类_Python-模块分类及导入
  8. 敏捷开发系列学习总结(1)——版本管理发布流程
  9. 4 基于优化的攻击——CW
  10. Navicat for MySQL 64位官方中文版
  11. 【1800题】一、函数、极限、连续
  12. 史上最全的ocr文字识别体验,让你一次用个够
  13. bootstrap导航栏.nav和.navbar区别
  14. 每日学习(Git和Github)
  15. Matlab 仿真——直流电机速度控制(5)通过频域分析进行控制器设计
  16. 自然语言处理入门学习笔记3:词向量
  17. vue组件挂载与html加载区别,vue中的挂载是什么意思?
  18. 树的递归与非递归遍历算法
  19. Android源码分析 - Parcel 与 Parcelable
  20. 计算机控制电机启动接线图,详解单相电机电容接线图

热门文章

  1. 有关1024的冷知识
  2. openssh编译 linux,openssh 的编译安装
  3. el-upload去掉删除按钮图标
  4. 【转】 java的声音技术与AudioStream (包括在java应用程序中循环播放声音)
  5. ogg登陆数据库用户密码加密
  6. 自动字幕对轴软件Autosub 用户手册(2.1)
  7. 【读书笔记】《人生护城河》
  8. 合理利用计算机虚拟内存,合理设置虚拟内存,目前8G内存足以满足日常使用
  9. android软件perthbus,Android – 萬能遙控器 ZAZA
  10. HTML5媒体元素audio,video