AndroidX RecyclerView总结-Recycler
文章目录
- 概述
- 源码探究
- 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相关推荐
- AndroidX RecyclerView总结-滑动处理
文章目录 概述 源码探究 RecyclerView中的处理 onInterceptTouchEvent 滑动状态 onTouchEvent scrollStep LinearLayoutManager ...
- Android之提示androidx.recyclerview.widget.LinearLayoutManager@51ddcd is already attached to a RecyclerV
1 问题 我有2个recycleView,但是用的同一个线性布局导致错误如下 LayoutManager androidx.recyclerview.widget.LinearLayoutManage ...
- androidx.recyclerview:recyclerview的使用
添加扩展 或手动修改app/build.gradle: 在dependencies里添加 implementation 'androidx.recyclerview:recyclerview:1.0. ...
- RecyclerView的Recycler
处理回收复用相关工作的 参考博客:https://www.jianshu.com/p/9306b365da57 几个重要的集合 mAttachedScrap 缓存显示到屏幕的item的holder.临 ...
- RecyclerView安卓androidx.widget.RecyclerView
导入RecyclerView implementation 'com.android.support:recyclerview-v7:28.0.0' 或者把常用的组件都导入了,包括了RecyclerV ...
- 【JetPack】视图绑定 ( ViewBinding ) 各种应用 ( 视图绑定两种方式 | Activity 布局 | 对话框布局 | 自定义组件布局 | RecyclerView 列表布局 )
文章目录 I . 视图绑定 ( ViewBinding ) 界面的两种方式 II . Activity 界面中 应用 视图绑定 ( ViewBinding ) III . Dialog 对话框界面中 ...
- recyclerview item点击无效_让你彻底掌握RecyclerView的缓存机制
点击上方蓝字关注 ?? 来源:肖邦kakahttps://www.jianshu.com/p/3e9aa4bdaefd 前言 RecyclerView这个控件几乎所有的Android开发者都使用过(甚 ...
- viewpager 自定义翻页效果_Android RecyclerView自定义LayoutManager
在第一篇中已经讲过,LayoutManager主要用于布局其中的Item,在LayoutManager中能够对每个Item的大小,位置进行更改,将它放在我们想要的位置,在很多优秀的效果中,都是通过自定 ...
- android zxing-3.3.3的用法_Android使用RecyclerView实现复杂的多布局效果
引言 RecyclerView不只能进行单页面布局,而且可以通过重写adapter中的getItemViewType方法来进行多种View布局,今天就来使用RecyclerView实现复杂的多布局效果 ...
最新文章
- 修复Long类型太长转为JSON格式的时候出错的问题
- linux tar命令详解,Linux tar命令详解
- what kind of supervision will i do next semester?
- java uri_Android中的Uri与Java中的URI类
- 如何解决U盘无法停止通用卷设备
- 美团数据平台Kerberos优化实战
- python模块分类_Python-模块分类及导入
- 敏捷开发系列学习总结(1)——版本管理发布流程
- 4 基于优化的攻击——CW
- Navicat for MySQL 64位官方中文版
- 【1800题】一、函数、极限、连续
- 史上最全的ocr文字识别体验,让你一次用个够
- bootstrap导航栏.nav和.navbar区别
- 每日学习(Git和Github)
- Matlab 仿真——直流电机速度控制(5)通过频域分析进行控制器设计
- 自然语言处理入门学习笔记3:词向量
- vue组件挂载与html加载区别,vue中的挂载是什么意思?
- 树的递归与非递归遍历算法
- Android源码分析 - Parcel 与 Parcelable
- 计算机控制电机启动接线图,详解单相电机电容接线图