回收、复用、预加载
当 RecyclerView 上下滑动时那些移出屏幕的 ViewHolder 会进行回收,新进入屏幕的 ViewHolder 会进行复用(如果有缓存),那么对于回收复用的源码分析就要从触摸方法中开始找切入点,先看一下 onTouchEvent 方法中处理滑动的调用流程:
RecyclerView.java
public boolean onTouchEvent(MotionEvent e) {
// ...
// mLayout 即为 LayoutManager 可以看出不设置 LayoutManager 是无法触发滑动的
if (mLayout == null) {
return false;
}
// ...
switch (action) {
case MotionEvent.ACTION_DOWN: {}
case MotionEvent.ACTION_MOVE: {
// ...
// 根据 Down 事件中保存的信息计算出滑动距离
int dx = mLastTouchX - x;
int dy = mLastTouchY - y;
// 滑动前先进行方向判断
if (mScrollState != SCROLL_STATE_DRAGGING) {
boolean startScroll = false;
// ...
if (canScrollVertically) { // 是否可以垂直滑动
if (dy > 0) {
dy = Math.max(0, dy - mTouchSlop);
} else {
dy = Math.min(0, dy + mTouchSlop);
}
if (dy != 0) {
startScroll = true;
}
}
if (startScroll) { // 标记开始滑动
setScrollState(SCROLL_STATE_DRAGGING);
}
}
if (mScrollState == SCROLL_STATE_DRAGGING) {
// ...
// scrollByInternal 是处理滑动的方法 !!!
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
e, TYPE_TOUCH)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
// ...
}
}
// ...
}
// scrollByInternal 源码部分
boolean scrollByInternal(int x, int y, MotionEvent ev, int type) {
// ...
if (mAdapter != null) {
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
// scrollStep 方法中计算出了滑动消耗的距离,并且赋值给了 mReusableIntPair
scrollStep(x, y, mReusableIntPair);
consumedX = mReusableIntPair[0];
consumedY = mReusableIntPair[1];
unconsumedX = x - consumedX;
unconsumedY = y - consumedY;
}
// ...
}
// scrollStep 源码部分
void scrollStep(int dx, int dy, @Nullable int[] consumed) {
// ...
int consumedX = 0;
int consumedY = 0;
// 不管是水平还是垂直滑动 最终都调用到了 LayoutManager 的 scrollXXXBy 方法中
if (dx != 0) {
consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
}
if (dy != 0) {
consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
}
//...
// 赋值
if (consumed != null) {
consumed[0] = consumedX;
consumed[1] = consumedY;
}
}
从上面源码部分中可以看出,RecyclerView 的滑动最终是交给 LayoutManager 去处理,我们以 LinearLayoutManager 为例继续跟踪源码:
LinearLayoutManager.java
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
RecyclerView.State state) {
// ...
return scrollBy(dx, recycler, state);
}
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
RecyclerView.State state) {
// ...
return scrollBy(dy, recycler, state);
}
不论是水平还是垂直滑动都调用到了 scrollBy 方法,继续跟踪源码:
LinearLayoutManager.java
int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
// ...
// 注意这里这个 fill 方法
final int consumed = mLayoutState.mScrollingOffset
+ fill(recycler, mLayoutState, state, false);
// ...
return scrolled;
}
fill 是填充的意思,当发生滑动时,就需要有新的 View 来填充滑动的部分,来看一下 fill 的源码:
LinearLayoutManager.java
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
final int start = layoutState.mAvailable;
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
// ...
// 回收
recycleByLayoutState(recycler, layoutState);
}
int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
// ...
// 复用
layoutChunk(recycler, state, layoutState, layoutChunkResult);
// ...
}
}
在 RecyclerView 滑动时回收和复用操作会同时触发,有移除屏幕的 ViewHolder 就需要有新的 ViewHolder 来填补,这个不难理解。
下面来分别看下回收和复用的方法调用流程。
recycleByLayoutState(recycler, layoutState);
LinearLayoutManager.java
private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
// ...
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
// 回收头部 View
recycleViewsFromEnd(recycler, scrollingOffset, noRecycleSpace);
} else {
// 回收尾部 View 两个方法逻辑都一样
recycleViewsFromStart(recycler, scrollingOffset, noRecycleSpace);
}
}
// recycleViewsFromEnd 和 recycleViewsFromStart 逻辑整体一样 就只贴一个了
private void recycleViewsFromEnd(RecyclerView.Recycler recycler, int scrollingOffset,
int noRecycleSpace) {
final int childCount = getChildCount();
// ...
// 是否倒置 (LinearLayoutManager 构造器中可以设置)
// 无论 if 还是 else 最终都调用到了 recycleChildren
if (mShouldReverseLayout) {
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (mOrientationHelper.getDecoratedStart(child) < limit
|| mOrientationHelper.getTransformedStartWithDecoration(child) < limit) {
// 根据 i 来回收 View
recycleChildren(recycler, 0, i);
return;
}
}
} else {
for (int i = childCount - 1; i >= 0; i--) {
View child = getChildAt(i);
if (mOrientationHelper.getDecoratedStart(child) < limit
|| mOrientationHelper.getTransformedStartWithDecoration(child) < limit) {
// 根据 i 来回收 View
recycleChildren(recycler, childCount - 1, i);
return;
}
}
}
}
private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) {
// ...
// 最终调用到了 removeAndRecycleViewAt 方法
if (endIndex > startIndex) {
for (int i = endIndex - 1; i >= startIndex; i--) {
removeAndRecycleViewAt(i, recycler);
}
} else {
for (int i = startIndex; i > endIndex; i--) {
removeAndRecycleViewAt(i, recycler);
}
}
}
LayoutManager.java
public void removeAndRecycleViewAt(int index, @NonNull Recycler recycler) {
final View view = getChildAt(index); //根据 index 找到 View
removeViewAt(index); // 从 RV 中移除
recycler.recycleView(view); // 交给 recycler 进行回收
}
layoutChunk(recycler, state, layoutState, layoutChunkResult);
LinearLayoutManager.java
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
View view = layoutState.next(recycler);
// ...
}
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
// 最终通过 recycler 的 getViewForPosition 方法获取到了 View
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
Recycler
从上述的源码调用流程中可以看出,回收复用的具体逻辑都放在了 Recycler 中,Recycler 和 LayoutManager 一样是 RecyclerView 的内部类,先来看下 Recycler 中的一些定义:
RecyclerView.java
public final class Recycler {
// 一级缓存
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
ArrayList<ViewHolder> mChangedScrap = null;
// 二级缓存
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
// 三级缓存
private ViewCacheExtension mViewCacheExtension;
// 四级缓存
RecycledViewPool mRecyclerPool;
// ...
}
在这里先对这些定义有个大致的了解:
- mAttachedScrap、mChangedScrap 屏幕上的 ViewHolder 缓存;
- mCachedViews 刚刚移除屏幕的 ViewHolder 会加入进来,默认容量为 2,超出容量后最先放入的会移出,并且加入 mRecyclerPool 缓存池中;
- mViewCacheExtension 供开发者自定义的缓存,一般不实现(在回收源码流程中并没有使用);
- mRecyclerPool 缓存池;
RecycledViewPool
mAttachedScrap、mCachedViews 就是简单的 ArrayList,存储 ViewHolder,RecycledViewPool 就需要查看源码来了解下了:
RecycledView.java
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; // mScrapHeap 的容量 默认为 5
long mCreateRunningAverageNs = 0;
long mBindRunningAverageNs = 0;
}
// 缓存池容器
SparseArray<ScrapData> mScrap = new SparseArray<>();
// ...
}
可以看出 RecycledViewPool 内部的容器为 SparseArray,存储类型是其内部类 ScrapData,而 ScrapData 内部使用 ArrayList 来存储 ViewHolder。当 RecyclerView 多类型 item 进行缓存时,直接以 itemViewType 作为 key,ScrapData 作为 value 对 ViewHolder 进行缓存,默认情况下每种不同的 ViewHolder 可以分别缓存 5 个。
回收
回收源码从 Recycler.recycleView(view) 直接看起:
RecyclerView.java
public void recycleView(@NonNull View view) {
// View 的 LayoutParams 为 RecyclerView.LayoutParams
// 从其内部可以直接获取到 ViewHolder
ViewHolder holder = getChildViewHolderInt(view);
if (holder.isTmpDetached()) {
// 清除动画资源等 并且触发 adapter 的 onViewDetachedFromWindow 方法
removeDetachedView(view, false);
}
// 如果 ViewHolder 存在于 mAttachedScrap 或者 mChangedScrap 中就移除、清除相关 flag
if (holder.isScrap()) {
holder.unScrap();
} else if (holder.wasReturnedFromScrap()) {
holder.clearReturnedFromScrapFlag();
}
// 回收核心方法
recycleViewHolderInternal(holder);
// 停止动画
if (mItemAnimator != null && !holder.isRecyclable()) {
mItemAnimator.endAnimation(holder);
}
}
void recycleViewHolderInternal(ViewHolder holder) {
// ...
final boolean transientStatePreventsRecycling = holder
.doesTransientStatePreventRecycling();
// 这里提一下 可以重写 adapter 的 onFailedToRecycleView 方法返回 ture 来达到强制回收 ViewHolder
final boolean forceRecycle = mAdapter != null
&& transientStatePreventsRecycling
&& mAdapter.onFailedToRecycleView(holder);
boolean cached = false;
boolean recycled = false;
// ...
// 如果可以回收
if (forceRecycle || holder.isRecyclable()) {
if (mViewCacheMax > 0
&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_REMOVED
| ViewHolder.FLAG_UPDATE
| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
// 先获取 mCachedViews 的 size
int cachedViewSize = mCachedViews.size();
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
// 超出容量后移除头部的 item
// 注意这个方法 !!! 内部还将移除的 ViewHolder 加入到了 RecyclerViewPool 中
recycleCachedViewAt(0);
cachedViewSize--;
}
int targetCacheIndex = cachedViewSize;
// 这个 if 中是预加载机制相关代码 后面解释
if (ALLOW_THREAD_GAP_WORK
&& cachedViewSize > 0
&& !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
// when adding the view, skip past most recently prefetched views
int 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) {
// 如果没有添加到 mCachedView 中则直接加入 RecycledViewPool 中
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
} else {
// ...
// 不能回收就什么也不做
}
// 清除保存的 ViewHolder 动画相关的信息
mViewInfoStore.removeViewHolder(holder);
if (!cached && !recycled && transientStatePreventsRecycling) {
holder.mBindingAdapter = null;
holder.mOwnerRecyclerView = null;
}
}
void recycleCachedViewAt(int cachedViewIndex) {
// 根据 index 获取到要从 mCachedViews 中移除的 ViewHolder
ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
// 添加到 RecycledViewPool
addViewHolderToRecycledViewPool(viewHolder, true);
// 从 mCachedViews 移除
mCachedViews.remove(cachedViewIndex);
}
void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {
View itemView = holder.itemView;
// ...
if (dispatchRecycled) {
// 触发 adapter 的 onViewRecycled 方法
dispatchViewRecycled(holder);
}
// ...
// 添加到缓存池
getRecycledViewPool().putRecycledView(holder);
}
public void putRecycledView(ViewHolder scrap) {
// 根据 itemViewType 获取缓存池中对应的 ArrayList
// 没有的话会 new 一个 ScrapData 添加到 mScrap 中
final int viewType = scrap.getItemViewType();
final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
// 判断是否超出容量
if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
return;
}
// ...
// 清除 ViewHolder 的状态信息
scrap.resetInternal();
// 添加到缓存池
scrapHeap.add(scrap);
}
注释中有详细的解释,在这里就简单总结下整体回收的流程:
- 如果要回收的 ViewHolder 存在于一级缓存 mAttachedScrap、mChangedScrap 中则从中移除;
- 将 ViewHolder 加入二级缓存 mCacheViews 中,如果容量超出则移除 index 为 0 的 ViewHolder,并且将其添加到 RecyclerViewPool 中;
- 如果 ViewHolder 在第 2 步中没有成功添加到 mCacheViews 中则直接添加进 RecyclerViewPool 缓存;
- RecyclerViewPool 中会根据 ItemViewType 获取对应 ViewHolder 的 ArrayList 容器(如果还没初始化则直接 new 一个)判断容量后添加进去,添加前会清除 ViewHolder 的一些状态信息;
复用
熟悉了回收流程后趁热打铁来看一下复用的源码流程,从 Recycler.getViewForPosition(position) 直接看起:
RecyclerView.java
public View getViewForPosition(int position) {
// 调用了重载方法
return getViewForPosition(position, false);
}
View getViewForPosition(int position, boolean dryRun) {
// 调用了 tryGetViewHolderForPositionByDeadline 获取到 ViewHolder
// 将 ViewHolder 的 itemView 返回
return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
// 复用机制的核心源码
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
// ...
boolean fromScrapOrHiddenOrCache = false;
ViewHolder holder = null;
// 如果是预布局状态通过一级缓存 mChangedScrap 中查找
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
// 二级缓存 内部会先从 mAttachedScrap 中尝试获取,获取不到则尝试从 mCacheViews 中寻找
// 注意这里是通过 position 寻找
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
// ...
}
// 二级缓存 跟上面的区别是这次根据 mHasStableIds 寻找
// adapter 重写 getItemId 方法设置 id
if (holder == null) {
// ...
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
// ...
}
// 三级缓存 自定义缓存
if (holder == null && mViewCacheExtension != null) {
// 注意这里返回的是 View
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
if (view != null) {
// 通过 RecyclerView.LayoutParams 获取其 ViewHolder
holder = getChildViewHolder(view);
// ...
}
}
// 四级缓存
if (holder == null) { // fallback to pool
holder = getRecycledViewPool().getRecycledView(type);
// ...
}
// 四级缓存都没有获取到
if (holder == null) {
// ...
// 通过 adapter onCreateViewHolder 方法创建 ViewHolder
holder = mAdapter.createViewHolder(RecyclerView.this, type);
// 预加载机制相关代码 后面解释
if (ALLOW_THREAD_GAP_WORK) {
// only bother finding nested RV if prefetching
RecyclerView innerView = findNestedRecyclerView(holder.itemView);
if (innerView != null) {
holder.mNestedRecyclerView = new WeakReference<>(innerView);
}
}
// ...
}
}
}
// ...
boolean bound = false;
if (mState.isPreLayout() && holder.isBound()) {
holder.mPreLayoutPosition = position;
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
// 如果没有绑定过数据 则调用 adapter onBindViewHolder 进行绑定
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
// 给 ViewHolder 的 itemView 设置 LayoutParams
final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
// 这里的 LayoutParams 是 RecyclerView.LayoutParams
final LayoutParams rvLayoutParams;
if (lp == null) {
// LayoutParams 由 LayoutManager 返回
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; // 将 ViewHolder 赋值给 LayoutParams
rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
return holder;
}
总结下,源码中四级缓存的方法分别为:
- 一级缓存 getChangedScrapViewForPosition,根据 position 从 mChangedScrap 中获取;
- 二级缓存 getScrapOrHiddenOrCachedHolderForPosition、getScrapOrCachedViewForId,优先根据 position 从 mAttachScrap、mCacheViews 中获取,其次根据 itemId 再次从 mAttachScrap、mCacheViews 中获取;
- 三级缓存 mViewCacheExtension.getViewForPositionAndType,自定义缓存,一定注意这里返回的是 View;
- 四级缓存 getRecycledViewPool().getRecycledView,根据 itemViewType 获取对应 ViewHolder 的缓存 ArrayList,再从其中尝试获取。
每级缓存对应的方法源码比较易懂,篇幅原因就不挨着贴出来了。
预加载
预加载是 SDK 21 后对 RecyclerView 的一项优化。
在了解与加载机制之前,需要先简单了解下 Android 开启硬件加速后的 UI 渲染流程:
- 当 VSync 信号到来后 UI Thread 处理输入事件、动效、测量、布局、绘制等等,将这些封装成 Render Node 同步给 Render Thread; 2. Render Thread 会将这些绘制指令转为 OpenGL 指令,最终通过 GPU 绘制到 Graphic Buffer; 3. SurfaceFlinger 拿到 buffer 后进行 layer 合并,最终将数据交给 HAL 层绘制到设备屏幕上; 4. 当下一次 VSync 信号到来后重复上述流程;
UI Thread 将 Render Node 同步给 Render Thread 后等待的过程就是预加载要利用的空隙:
原理大概就是这样,具体来看一下源码中是如何实现:
RecyclerView.java
protected void onAttachedToWindow() {
// ...
// SDK >= 21
if (ALLOW_THREAD_GAP_WORK) {
// sGapWorker 是一个 ThreadLocal
mGapWorker = GapWorker.sGapWorker.get();
if (mGapWorker == null) { //初始化 GapWorker
mGapWorker = new GapWorker();
// ...
GapWorker.sGapWorker.set(mGapWorker);
}
mGapWorker.add(this);
}
}
GapWorker 实现了 Runnable 接口,GapWorker 的使用在 onTouchEvent 的 MOVE 事件中:
RecyclerView.java
public boolean onTouchEvent(MotionEvent e) {
// ...
switch (action) {
// ...
case MotionEvent.ACTION_MOVE: {
// ...
if (mGapWorker != null && (dx != 0 || dy != 0)) {
mGapWorker.postFromTraversal(this, dx, dy);
}
}
}
来看一下 postFromTraversal 源码:
GapWorker.java
void postFromTraversal(RecyclerView recyclerView, int prefetchDx, int prefetchDy) {
if (recyclerView.isAttachedToWindow()) {
// ...
if (mPostTimeNs == 0) {
mPostTimeNs = recyclerView.getNanoTime();
// GapWorker 实现了 Runnable
// 通过 View.post 执行 run 方法
recyclerView.post(this);
}
}
// ...
}
继续查看 GapWorker 的 run 方法实现:
GapWorker.java
public void run() {
try {
// Trace
Trace.beginSection(RecyclerView.TRACE_PREFETCH_TAG);
long lastFrameVsyncNs = TimeUnit.MILLISECONDS.toNanos(
mRecyclerViews.get(0).getDrawingTime());
if (lastFrameVsyncNs == 0) {
return;
}
// 获取下次 VSync 信号到来的时间
long nextFrameNs = lastFrameVsyncNs + mFrameIntervalNs;
// 尝试预加载
prefetch(nextFrameNs);
}
// ...
}
void prefetch(long deadlineNs) {
buildTaskList(); // 构建任务
flushTasksWithDeadline(deadlineNs); // 执行
}
private void flushTasksWithDeadline(long deadlineNs) {
for (int i = 0; i < mTasks.size(); i++) {
final Task task = mTasks.get(i);
if (task.view == null) {
break; // done with populated tasks
}
// 循环执行
flushTaskWithDeadline(task, deadlineNs);
task.clear();
}
}
private void flushTaskWithDeadline(Task task, long deadlineNs) {
long taskDeadlineNs = task.immediate ? RecyclerView.FOREVER_NS : deadlineNs;
// prefetchPositionWithDeadline 方法中进行了预加载
RecyclerView.ViewHolder holder = prefetchPositionWithDeadline(task.view,
task.position, taskDeadlineNs);
// ...
}
}
private RecyclerView.ViewHolder prefetchPositionWithDeadline(RecyclerView view,
int position, long deadlineNs) {
// ...
RecyclerView.Recycler recycler = view.mRecycler;
// 这里又调用到了 tryGetViewHolderForPositionByDeadline
RecyclerView.ViewHolder holder = recycler.tryGetViewHolderForPositionByDeadline(
position, false, deadlineNs);
// 预加载创建 View 成功之后
if (holder != null) {
if (holder.isBound()) { // 绑定数据成功 放入 mCacheViews 中
recycler.recycleView(holder.itemView);
} else { // 如果仅仅创建成功 没绑定数据 就放入 RecyclerViewPool 中
recycler.addViewHolderToRecycledViewPool(holder, false);
}
}
return holder;
}
再次回到 方法中:
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
if (holder == null) {
long start = getNanoTime();
// 这里是根据 ViewHolder 的平均创建时间判断
// 判断是否有足够的时间进行预加载操作
if (deadlineNs != FOREVER_NS
&& !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
// abort - we have a deadline we can't meet
return null;
}
// 时间够 则预加载成功
holder = mAdapter.createViewHolder(RecyclerView.this, type);
// ...
}
// ...
// 利用剩余的时间尝试绑定数据
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
// ...
}
可以看出预加载之后对 ViewHolder 的缓存和前面回收复用部分是相符合的。预加载机制一般不需要我们额外编写代码即可实现,这部分内容就当扩展了解吧。
测量、布局、绘制、预布局
onMeasure
RecyclerView.java
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
// 第一种情况:没有设置 LayoutManager
if (mLayout == null) {
defaultOnMeasure(widthSpec, heightSpec);
return;
}
// 第二种情况:设置的 LayoutManager 开启自动测量
if (mLayout.isAutoMeasureEnabled()) {
// ...
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
// ...
mLastAutoMeasureSkippedDueToExact =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
if (mLastAutoMeasureSkippedDueToExact || mAdapter == null) {
return;
}
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
dispatchLayoutStep2();
// 需要二次测量
if (mLayout.shouldMeasureTwice()) {
// ...
dispatchLayoutStep2();
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
}
// ...
} else { // 第三种情况:设置的 LayoutManager 没有开启自动测量
// ...
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
// ...
mState.mInPreLayout = false; // clear
}
}
源码只贴出了重要部分,稍微总结下:
- 没有设置 LayoutManager 时,调用 defaultOnMeasure 方法;
- 设置 LayoutManager 并且开启自动测量时,调用 LayoutManager 的 onMeasure 方法,并且会执行 dispatchLayoutStep1()、dispatchLayoutStep2();
- 设置 LayoutManager 且没有开始自动测量时,仅调用了 LayoutManager 的 onMeasure 方法;
先来看一下 LayoutManager 的 onMeasure 方法:
RecyclerView.java
public abstract static class LayoutManager{
// ...
public void onMeasure(@NonNull Recycler recycler, @NonNull State state, int widthSpec,int heightSpec) {
mRecyclerView.defaultOnMeasure(widthSpec, heightSpec);
}
}
默认实现和第一种情况一样,调用了 defaultOnMeasure 方法,而且 sdk 中给我们提供的三种 LayoutManager(LinearLayoutManager、GridLayoutManager、StaggeredGridLayoutManager)均没有重写 onMeasure 方法。
接着就来看看 defaultOnMeasure 的源码:
RecyclerView.java
void defaultOnMeasure(int widthSpec, int heightSpec) {
// 通过 LayoutManager.chooseSize 获取的宽和高的值
final int width = LayoutManager.chooseSize(widthSpec,
getPaddingLeft() + getPaddingRight(), // 横向内边距
ViewCompat.getMinimumWidth(this)); // 反射获取是否设置最小宽度
final int height = LayoutManager.chooseSize(heightSpec,
getPaddingTop() + getPaddingBottom(), // 纵向内边距
ViewCompat.getMinimumHeight(this)); // 反射获取是否设置最小宽度
// 设置宽高
setMeasuredDimension(width, height);
}
接着看一下 LayoutManager.chooseSize 是如何获取宽高的:
public static int chooseSize(int spec, int desired, int min) {
final int mode = View.MeasureSpec.getMode(spec);
final int size = View.MeasureSpec.getSize(spec);
switch (mode) {
case View.MeasureSpec.EXACTLY:
return size;
case View.MeasureSpec.AT_MOST:
return Math.min(size, Math.max(desired, min));
case View.MeasureSpec.UNSPECIFIED:
default:
return Math.max(desired, min);
}
}
这段代码就不用解释了吧?自定义 View 时经常会根据 mode 不同来处理宽高的最终值。
测量这部分到目前为止的代码都比较简单,测量对于宽高这部分并没有特殊处理,剩余重要逻辑都在 dispatchLayoutStep1()、dispatchLayoutStep2() 方法中,这里先不对其进行详细解释,因为下面的 onLayout 中还有一个 dispatchLayoutStep3() 方法。
稍微总结下,测量部分除非有特殊的自定义 LayoutManager 对宽高有自定义需求,一般情况都会走默认的 defaultOnMeasure 方法,和大部分自定义 View 相同根据 mode 确定宽高。
onLayout
自定义 View 的第二大流程 onLayout,直接看源码:
RecyclerView.java
protected void onLayout(boolean changed, int l, int t, int r, int b) {
TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
dispatchLayout(); // 只有一处方法调用 分发布局
TraceCompat.endSection();
mFirstLayoutComplete = true;
}
void dispatchLayout() {
// ...
// 在 onMeasure 中这个值被设置为 true
mState.mIsMeasuring = false;
// ...
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else if (mAdapterHelper.hasUpdates()
|| needsRemeasureDueToExactSkip
|| mLayout.getWidth() != getWidth()
|| mLayout.getHeight() != getHeight()) {
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else {
mLayout.setExactMeasureSpecsFrom(this);
}
dispatchLayoutStep3(); // mState.mLayoutStep 的值在里面被设置为 State.STEP_START
}
onLayout 中的逻辑并不复杂,逻辑都放在了 dispatchLayout 中,而 dispatchLayout 中又根据各种判断确保了 dispatchLayoutStep1、dispatchLayoutStep2、dispatchLayoutStep3 都会执行。
onDraw
RecyclerView 重写了 draw 方法,那么就先看一下 draw 方法:
RecyclerView.java
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);
}
// ...
}
draw 方法中获取了所有的 ItemDecoration 也就是“分割线”,调用了其 onDrawOver 方法。
draw 的源码中会继续调用 onDraw 方法,继续看一下 onDraw 方法:
RecyclerView.java
public void onDraw(Canvas c) {
super.onDraw(c);
// draw 方法中调用了 ItemDecoration 的 onDrawOver
// onDraw 方法又调用了 ItemDecoration 的 onDraw
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDraw(c, this, mState);
}
}
可以看出 draw 和 onDraw 主要对分割线进行了绘制,由于 draw 方法先执行,那么也就意味着 ItemDecoration 的 onDrawOver 方法会先绘制,之后再执行其 onDraw 方法。
dispatchLayoutStep1、2、3
dispatchLayoutStep1
概述:处理 Adapter 更新,决定哪个动画应该被执行,保存当前的视图信息,如果需要的话进行预布局并保存相关信息。
RecyclerView.java
private void dispatchLayoutStep1() {
// 确认布局步骤 在 dispatchLayoutStep3 会设置为 STEP_START
// 并且 onLayout 调用 dispatchLayoutStep1 之前也进行了判断
mState.assertLayoutStep(State.STEP_START);
// 获取剩余的滚动距离(横竖向)
fillRemainingScrollValues(mState);
// onMeasure 中标记为 true 这里再置为 false
mState.mIsMeasuring = false;
startInterceptRequestLayout();
// ViewInfoStore 用于保存动画相关信息
// 清除保存的信息
mViewInfoStore.clear();
// 标记进入布局或者滚动状态 内部是int类型进行++操作
onEnterLayoutOrScroll();
// 适配器更新和动画预处理 设置 mState.mRunSimpleAnimations 和 mState.mRunPredictiveAnimations 的值
processAdapterUpdatesAndSetAnimationFlags();
// 保存焦点信息
saveFocusInfo();
// 一些信息保存
mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged;
mItemsAddedOrRemoved = mItemsChanged = false;
// 预布局标志 和 mRunPredictiveAnimation 有关 这里先记住 后面会解释什么是预布局
mState.mInPreLayout = mState.mRunPredictiveAnimations;
mState.mItemCount = mAdapter.getItemCount();
findMinMaxChildLayoutPositions(mMinMaxLayoutPositions);
// 下面两个 if 都是动画预处理 保存信息等等
// mRunSimpleAnimations 可以理解为 需要执行动画
if (mState.mRunSimpleAnimations) {
// ...
int count = mChildHelper.getChildCount();
for (int i = 0; i < count; ++i) {
// ...
// 保存执行动画所需的信息 (预布局时的信息)
mViewInfoStore.addToPreLayout(holder, animationInfo);
// ...
}
}
// mRunPredictiveAnimations 可以理解为 需要执行动画的情况下需要进行预布局
// 换而言之需要拿到动画执行前后的各种信息(坐标等等)
if (mState.mRunPredictiveAnimations) {
// ...
// 这里如果需要预布局就调用 LayoutManager 的 onLayoutChildren 开始布局
// 注意 mState.mInPreLayout = mRunPredictiveAnimations
// 当 mRunPredictiveAnimations 为 ture 时 mInPreLayout 同样为 true
mLayout.onLayoutChildren(mRecycler, mState);
// ...
} else {
clearOldPositions();
}
onExitLayoutOrScroll();
// 和 startInterceptRequestLayout 成对使用 貌似是防止多次 requestLayout
stopInterceptRequestLayout(false);
// 标记 STEP_START 完成 可以执行 dispatchLayoutStep2
mState.mLayoutStep = State.STEP_LAYOUT;
}
dispatchLayoutStep1 整体上都是预布局处理,对动画信息的保存等等,ViewInfoStore 是用于存储 item 动画相关信息,后面的博客中会分析。注意重点,如果需要执行动画将会执行预布局,也就是调用 mLayout.onLayoutChildren 之前 mInPreLayout 为 true。
dispatchLayoutStep2
概述:预布局状态结束,开始真正的布局。
RecyclerView.java
private void dispatchLayoutStep2() {
startInterceptRequestLayout(); // 和 stopInterceptRequestLayout 成对出现
onEnterLayoutOrScroll(); // 上面已经说过了
// 判断 State ,在 dispatchLayoutStep1 已经标记为 STEP_LAYOUT
mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
mAdapterHelper.consumeUpdatesInOnePass();
mState.mItemCount = mAdapter.getItemCount();
mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;
if (mPendingSavedState != null && mAdapter.canRestoreState()) {
if (mPendingSavedState.mLayoutState != null) {
mLayout.onRestoreInstanceState(mPendingSavedState.mLayoutState);
}
mPendingSavedState = null;
}
// 预布局标记为 fasle
mState.mInPreLayout = false;
// 开始布局 这里是真正的测量和布局items
mLayout.onLayoutChildren(mRecycler, mState);
mState.mStructureChanged = false;
mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
// 标记 STEP_ANIMATIONS
mState.mLayoutStep = State.STEP_ANIMATIONS;
onExitLayoutOrScroll();
stopInterceptRequestLayout(false); // 和 startInterceptRequestLayout 成对出现
}
dispatchLayoutStep2 方法代码不多,注意重点,在调用 mLayout.onLayoutChildren(mRecycler, mState) 之前将 mState.mInPreLayout 预布局标记为 false。
到这里可以看出预布局过程就发生在 dispatchLayoutStep1、2 之间。
dispatchLayoutStep3
概述:执行 item 动画以及布局完成后的收尾工作。
RecyclerView.java
private void dispatchLayoutStep3() {
// 判断状态是否为 STEP_ANIMATIONS
mState.assertLayoutStep(State.STEP_ANIMATIONS);
// 和 stopInterceptRequestLayout 成对出现
startInterceptRequestLayout();
// 和 onExitLayoutOrScroll 成对出现
onEnterLayoutOrScroll();
// 标记为 STEP_START, 在步骤 1 中会判断是否为 STEP_START
mState.mLayoutStep = State.STEP_START;
if (mState.mRunSimpleAnimations) {
for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
// ...
// 保存动画信息
// 布局完成后的坐标等等
mViewInfoStore.addToPostLayout(holder, animationInfo);
// ...
}
}
// 执行 item 动画
mViewInfoStore.process(mViewInfoProcessCallback);
}
// 清理 mAttachedScrap 和 mChangedScraop 缓存
mLayout.removeAndRecycleScrapInt(mRecycler);
// item 数量
mState.mPreviousLayoutItemCount = mState.mItemCount;
// 相关标记设为初始值
mDataSetHasChangedAfterLayout = false;
mDispatchItemsChangedEvent = false;
mState.mRunSimpleAnimations = false;
mState.mRunPredictiveAnimations = false;
mLayout.mRequestedSimpleAnimations = false;
// ...
// 布局完成回调
mLayout.onLayoutCompleted(mState);
onExitLayoutOrScroll();
stopInterceptRequestLayout(false);
// 清理
mViewInfoStore.clear();
// ...
}
mAttachedScrap 和 mChangedScrap
回收部分源码执行时,并没有用到 mAttachedScrap 和 mChangedScrap,复用时却优先在他们俩容器中寻找缓存。现在在布局步骤3 dispatchLayoutStep3 中也对其进行了清空,那么说明在 dispatchLayoutStep3 之前对其肯定有过回收的操作。
布局步骤1、2、3中,大部分逻辑都在 mLayout.onLayoutChildren 中,但其是一个空实现,所以,就以其开发中最常用到的实现类 LinearLayoutManager 源码来分析看看:
LinearLayoutManager.java
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state){
// ...
//
detachAndScrapAttachedViews(recycler);
// ...
// fill 在第一篇博客中提到了 填充布局 算是回收复用的入口
fill(recycler, mLayoutState, state, false);
}
public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
// 从这里可以看出将所有的可见的 item 都回收到了 mAttachedScrap 或者 mChangedScrap 中
final int childCount = getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
final View v = getChildAt(i);
scrapOrRecycleView(recycler, i, v);
}
}
在 onLayoutChildren 中对可见的 item 都进行了回收操作,并且紧接着执行了 fill 进行了填充布局操作。由上述对 dispatchLayout1、2、3 的源码分析可以得知,dispatchLayout3 对 mAttachedScrap 和 mChangedScrap 进行了清空操作,dispatchLayout1 调用 onLayoutChildren 进行预布局操作,而 dispatchLayout2 调用 onLayoutChildren 进行真正的布局操作。
那么显而易见,mAttachedScrap 和 mChangedScrap 是对可见 item 的缓存,目的在于预布局、真正的布局阶段复用,不用重新绑定数据。
预布局
先大概说一下预布局的使用场景,如下图所示:
假如屏幕中有一个 RecyclerView 且其有三个 item,当删除 item3 时,item4 会递补出现在屏幕内。这是开发中非常常见的情况吧,一般执行删除或者新增操作,我们都会添加动画让其显得不生硬,那么思考下 item4 是什么时候添加到屏幕上的呢?
回到 LinearLayoutManager 的 fill 方法查看源码:
LinearLayoutManager.java
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
//...
// 可用空间
int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
// 当 remainingSpace > 0 会继续循环
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
// ...
// 布局
layoutChunk(recycler, state, layoutState, layoutChunkResult);
// 计算可用空间
// 注意这里的判断条件
if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
|| !state.isPreLayout()) {
layoutState.mAvailable -= layoutChunkResult.mConsumed;
remainingSpace -= layoutChunkResult.mConsumed;
}
}
}
在计算可用空间时,有三个判断条件:
- !layoutChunkResult.mIgnoreConsumed
- layoutState.mScrapList != null
- !state.isPreLayout()
重点看 1 和 3,先说 3 吧,如果是预布局状态,也就是 dispatchLayoutStep1 调用进来时第三个条件是 false。至于条件 1 还需要看一下 layoutChunk 方法源码:
LinearLayoutManager.java
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
// ...
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
// ...
// 源码最后部分有这么一处判断
// 如果 viewholder 被标记为了移除或者改变 mIgnoreConsumed 设为 true
if (params.isItemRemoved() || params.isItemChanged()) {
result.mIgnoreConsumed = true;
}
result.mFocusable = view.hasFocusable();
}
看完这段代码再回到上面图示中的场景:
当 item3 被删除时,在预布局阶段它所占用的空间会忽略不计,那么 fill 方法中在计算可用空间时就会多走一次 while 循环,从而多添加一个 item。
那么 dispatchStep1 即可称之为预布局阶段,此时将要移除的 item3 以及即将添加到屏幕上的 item4 的预布局阶段的位置信息等等保存,在 dispatchStep2 真正布局阶段保存完成删除操作后的位置信息等等,即可在 dispatchStep3 中根据两个信息之间的差异做出对应的 item 动画。
LayoutManager
布局
RecyclerView 将布局这个任务完全交给了 LayoutManager,根据上面的回顾可知布局逻辑在 onLayoutChildren 方法,直接查看下 LinearLayoutManager 的 onLayoutChildren 方法源码:
LinearLayoutManager.java
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (mPendingSavedState != null || mPendingScrollPosition != RecyclerView.NO_POSITION) {
if (state.getItemCount() == 0) { // 没有 items 就全部回收
removeAndRecycleAllViews(recycler);
return;
}
}
// mPendingScrollPosition 设置需要滚动到第几个item
if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) {
mPendingScrollPosition = mPendingSavedState.mAnchorPosition;
}
// 初始化 LayoutState ,为 null 则直接 new 出来
ensureLayoutState();
// 标记为不回收
mLayoutState.mRecycle = false;
// 是否倒序(构造函数中可以设置)
resolveShouldLayoutReverse();
// 获取焦点 View
final View focused = getFocusedChild();
// 这个 if 整体是计算锚点信息
if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION
|| mPendingSavedState != null) {
mAnchorInfo.reset();
// 是否从末尾开始 mStackFromEnd 可以设置
mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
// 计算锚点相关信息
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
mAnchorInfo.mValid = true;
} else if (focused != null && (mOrientationHelper.getDecoratedStart(focused)
>= mOrientationHelper.getEndAfterPadding()
|| mOrientationHelper.getDecoratedEnd(focused)
<= mOrientationHelper.getStartAfterPadding())) {
// 这个 else if 为了处理软键盘弹出压缩布局后的情况 我没有细研究
mAnchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused));
}
// 根据滑动值判断布局方向
mLayoutState.mLayoutDirection = mLayoutState.mLastScrollDelta >= 0
? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
// 计算额外的布局空间 也就是预布局的情况下 需要额外计算
calculateExtraLayoutSpace(state, mReusableIntPair);
int extraForStart = Math.max(0, mReusableIntPair[0])
+ mOrientationHelper.getStartAfterPadding();
int extraForEnd = Math.max(0, mReusableIntPair[1])
+ mOrientationHelper.getEndPadding();
// 看第一个判断条件也就知道了 是处理预布局的
if (state.isPreLayout() && mPendingScrollPosition != RecyclerView.NO_POSITION
&& mPendingScrollPositionOffset != INVALID_OFFSET) {
final View existing = findViewByPosition(mPendingScrollPosition);
if (existing != null) {
final int current;
final int upcomingOffset;
// ...
// 最后计算出了两个方向的 额外布局空间
if (upcomingOffset > 0) {
extraForStart += upcomingOffset;
} else {
extraForEnd -= upcomingOffset;
}
}
}
// ...
// 锚点计算完成回调
onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
// 回收屏幕上可见 item 到 scrap 中
detachAndScrapAttachedViews(recycler);
// ...
if (mAnchorInfo.mLayoutFromEnd) { // 从末尾开始布局
// 更新锚点信息 AnchorInfo 对象中存储着锚点的位置、偏移、方向等等
updateLayoutStateToFillStart(mAnchorInfo);
// 设置预布局计算出的额外填充空间
mLayoutState.mExtraFillSpace = extraForStart;
// fill 方法填充
fill(recycler, mLayoutState, state, false);
// ...
// 再次更新锚点信息 和上次不同 上次上 锚点向 start 方向,这次上 锚点向 end 方向
updateLayoutStateToFillEnd(mAnchorInfo);
// 设置与布局 end 方向的 额外填充空间
mLayoutState.mExtraFillSpace = extraForEnd;
// fill 方法中填充
fill(recycler, mLayoutState, state, false);
// ...
} else { // 从头部开始布局
// 和上面 if 反过来 先更新 end 方向锚点信息
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForEnd;
// end 方向布局
fill(recycler, mLayoutState, state, false);
// ...
// 下面是向 start 方向布局的逻辑
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForStart;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state, false);
// ...
}
if (!state.isPreLayout()) { // 不是预布局 则布局完成回调
mOrientationHelper.onLayoutComplete();
} else { // 预布局则重置锚点信息
mAnchorInfo.reset();
}
// 保存这次布局是否从底部填充
mLastStackFromEnd = mStackFromEnd;
}
实际添加 View 以及布局重点逻辑都在 fill 方法中,在之前的博客中以及提到过:
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
//...
// 可用空间
int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
// 当 remainingSpace > 0 会继续循环
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
// ...
// 布局
layoutChunk(recycler, state, layoutState, layoutChunkResult);
// 计算可用空间
// 注意这里的判断条件
if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
|| !state.isPreLayout()) {
layoutState.mAvailable -= layoutChunkResult.mConsumed;
remainingSpace -= layoutChunkResult.mConsumed;
}
}
}
layoutChunk 是获取、添加 View 的方法:
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
// 获取 View 在第一篇有详细说这个方法
View view = layoutState.next(recycler);
// ...
// view 的 LayoutParams 中存储着 ViewHolder
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
if (layoutState.mScrapList == null) {
// 添加 View
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addView(view);
} else {
addView(view, 0);
}
}
// ...
// 测量 view
measureChildWithMargins(view, 0, 0);
result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
// 根据方向计算 view 的位置
int left, top, right, bottom;
if (mOrientation == VERTICAL) {
if (isLayoutRTL()) {
right = getWidth() - getPaddingRight();
left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
} else {
left = getPaddingLeft();
right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
}
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
bottom = layoutState.mOffset;
top = layoutState.mOffset - result.mConsumed;
} else {
top = layoutState.mOffset;
bottom = layoutState.mOffset + result.mConsumed;
}
}
// ...
// 摆放 View
layoutDecoratedWithMargins(view, left, top, right, bottom);
// 如果即将被移除 则标记忽略 不占用可用空间
if (params.isItemRemoved() || params.isItemChanged()) {
result.mIgnoreConsumed = true;
}
result.mFocusable = view.hasFocusable();
}
锚点
如图所示,绿色的位置为锚点,共分为三种情况。第三种情况上下滑动是最常见的情况。在上述源码中进行填充的 if else 中不论进入哪个判断都会有两次 fill,就如图中第三种情况所示,会根据锚点的位置结合 stackFromEnd 的值先后进行两次填充。
下面来进入源码来了解一下 mAnchorInfo:
final AnchorInfo mAnchorInfo = new AnchorInfo();
在 LinearLayoutManager 中定义时就完成了初始化,看下其源码:
static class AnchorInfo {
OrientationHelper mOrientationHelper; // 辅助类获取itemView的相关信息
int mPosition; // 锚点的位置 也就是对应的 itemView 在 rv 中的索引
int mCoordinate; // 偏移量
boolean mLayoutFromEnd; // 是否从末尾开始填充
boolean mValid; // 是否有效
AnchorInfo() { // 构造函数直接调用了 reset 也就知道这些变量的初始值了
reset();
}
void reset() {
mPosition = RecyclerView.NO_POSITION;
mCoordinate = INVALID_OFFSET;
mLayoutFromEnd = false;
mValid = false;
}
//...
}
LayoutState
在 onLayoutChildren 的源码中也多次使用了 LayoutState 中的变量,直接看一下源码:
static class LayoutState {
// ...
// 是否要回收
boolean mRecycle = true;
// 偏移量
int mOffset;
// 要填充的空间值(像素)
int mAvailable;
// 当前位置
int mCurrentPosition;
// 适配器遍历方向
int mItemDirection;
// 布局填充方向
int mLayoutDirection;
// 滚动的偏移量
int mScrollingOffset;
// 预布局需要额外填充的空间大小
int mExtraFillSpace = 0;
int mNoRecycleSpace = 0;
// 预布局标记
boolean mIsPreLayout = false;
// 上一次滚动的距离
int mLastScrollDelta;
// 我查看了他的赋值 其实就是 Relcycer 的 mAttachScrap
List<RecyclerView.ViewHolder> mScrapList = null;
boolean mInfinite;
// ...
}
可以看出 LayoutState 就是一个布局状态类,将一些关键的布局方向、偏移量等等作为成员变量。
滑动
RecyclerView 的滑动是交给 LayoutManager 的 scrollHorizontallyBy 和 scrollVerticallyBy 两个方法执行,分别处理水平、垂直方向的滑动,那么就直接进入到 LinearLayoutManager 的 scrollHorizontallyBy 方法(垂直方向的逻辑都一样就只看一个了)查看其源码:
LinearLayoutManager.java
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
RecyclerView.State state) {
if (mOrientation == VERTICAL) { // 判断了下方向
return 0;
}
// 调用了 scrollBy
return scrollBy(dx, recycler, state);
}
int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
// ...
// 滑动方向
final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
// 距离
final int absDelta = Math.abs(delta);
updateLayoutState(layoutDirection, absDelta, true, state);
// fill 方法很熟悉了 主要处理 itemView 的摆放以及回收复用
final int consumed = mLayoutState.mScrollingOffset
+ fill(recycler, mLayoutState, state, false);
// ...
final int scrolled = absDelta > consumed ? layoutDirection * consumed : delta;
// 交给 mOrientationHelper 的 offsetChildren 方法处理滑动
mOrientationHelper.offsetChildren(-scrolled);
// ...
return scrolled;
}
OrientationHelper 是一个抽象类,offsetChildren 也是一个抽象方法,那么直接在 LinearLayoutManager 中查看其初始化逻辑:
LinearLayoutManager.java
public LinearLayoutManager(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
Properties properties = getProperties(context, attrs, defStyleAttr, defStyleRes);
// 在构造方法中 设置了方向
setOrientation(properties.orientation);
setReverseLayout(properties.reverseLayout);
setStackFromEnd(properties.stackFromEnd);
}
public void setOrientation(@RecyclerView.Orientation int orientation) {
// ...
if (orientation != mOrientation || mOrientationHelper == null) {
// 通过 OrientationHelper 静态方法 createOrientationHelper 初始化
mOrientationHelper = OrientationHelper.createOrientationHelper(this, orientation);
// ...
}
}
OrientationHelper.java
public static OrientationHelper createOrientationHelper(
RecyclerView.LayoutManager layoutManager, @RecyclerView.Orientation int orientation) {
switch (orientation) {
case HORIZONTAL: // 水平方向
return createHorizontalHelper(layoutManager);
case VERTICAL: // 垂直
return createVerticalHelper(layoutManager);
}
throw new IllegalArgumentException("invalid orientation");
}
// 由于分析水平方向,所以只查看其水平方向的创建源码
public static OrientationHelper createHorizontalHelper(
RecyclerView.LayoutManager layoutManager) {
return new OrientationHelper(layoutManager) {
// ...
public void offsetChildren(int amount) {
// 又调用回了 LayoutManager
mLayoutManager.offsetChildrenHorizontal(amount);
}
// ...
};
}
LayoutManager.java
public void offsetChildrenHorizontal(@Px int dx) {
if (mRecyclerView != null) {
// 又调用到了 RecyclerView 中
mRecyclerView.offsetChildrenHorizontal(dx);
}
}
RecyclerView.java
public void offsetChildrenHorizontal(@Px int dx) {
final int childCount = mChildHelper.getChildCount();
for (int i = 0; i < childCount; i++) {
// 循环获取 View 调用 View 的 offsetLeftAndRight 方法进行偏移操作
mChildHelper.getChildAt(i).offsetLeftAndRight(dx);
}
}
经过这一系列的源码调用,最终滑动还是交给 RecyclerView 去遍历子 View 设置偏移来实现的,其实可以看出当我们自定义 LayoutManager 时需要处理滑动时,水平方向调用 offsetChildrenHorizontal 那么垂直方向自然是调用 offsetChildrenVertical。
回收复用
LinearLayoutManager fill 方法的回收调用流程:
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
//...
// 可用空间
int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
// 当 remainingSpace > 0 会继续循环
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
// ...
// 布局
layoutChunk(recycler, state, layoutState, layoutChunkResult);
// 计算可用空间
if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
|| !state.isPreLayout()) {
layoutState.mAvailable -= layoutChunkResult.mConsumed;
remainingSpace -= layoutChunkResult.mConsumed;
}
// 在上面布局、计算可用空间的操作完成后
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
// 通过 recycleByLayoutState 去回收移除屏幕的 View
recycleByLayoutState(recycler, layoutState);
}
// ...
}
}
private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
if (!layoutState.mRecycle || layoutState.mInfinite) {
return;
}
int scrollingOffset = layoutState.mScrollingOffset;
int noRecycleSpace = layoutState.mNoRecycleSpace;
// 和布局时一样 分两个方向
// 以水平滑动为例吧 左滑则左侧item移除屏幕需要回收,右滑同理
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
recycleViewsFromEnd(recycler, scrollingOffset, noRecycleSpace);
} else {
recycleViewsFromStart(recycler, scrollingOffset, noRecycleSpace);
}
}
private void recycleViewsFromEnd(RecyclerView.Recycler recycler, int scrollingOffset,
int noRecycleSpace) {
final int childCount = getChildCount();
// 计算出回收的触发距离
final int limit = mOrientationHelper.getEnd() - scrollingOffset + noRecycleSpace;
if (mShouldReverseLayout) {
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
// 遍历找出需要回收的子 View 的最大索引
if (mOrientationHelper.getDecoratedStart(child) < limit
|| mOrientationHelper.getTransformedStartWithDecoration(child) < limit) {
// 通过 recycleChildren 回收
recycleChildren(recycler, 0, i);
return;
}
}
}
// ...
}
private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) {
// 根据传入的索引 开始遍历回收
if (endIndex > startIndex) {
for (int i = endIndex - 1; i >= startIndex; i--) {
removeAndRecycleViewAt(i, recycler);
}
} else {
for (int i = startIndex; i > endIndex; i--) {
removeAndRecycleViewAt(i, recycler);
}
}
}
public void removeAndRecycleViewAt(int index, @NonNull Recycler recycler) {
final View view = getChildAt(index);
removeViewAt(index); // 移除 View
recycler.recycleView(view); // 最终调用到了 recycler.recycleView
}
自定义 LayoutManager
其实算一个比较简单的需求,和自定义 ViewGroup 很像只要计算好每个 item 的位置,在第几页、第几列、第几行按位置摆放即可,重点在于理解自定义 LayoutManager 的步骤,相比于自定义 ViewGroup 多了一个回收复用的步骤,并且滑动也不需要自己去实现。
布局逻辑就不说了,一页一页的排布,仅仅是根据 item 的 index 计算它的位置。
滑动和回收复用是关联在一起的,一般自定义 LayoutManager 要保证子 View 当数量,也就是 childCount 不超过屏幕可见的 item 数量,也就意味着当滑动结束后 item 的坐标不在屏幕可见范围内就应该回收,而新滑入屏幕的 View 应该优先从缓存池中获取达到复用的目的。
布局
首先要根据 index 能算出 item 在第几行第几列,接着再根据行列计算出 item 的位置:
// 在第几页
var page = ...
// 在分页的第几个位置
val pageIndex = ...
// 第几列
val colum = ...
// 第几行
val row = ...
// 位置
itemRect.left = page * 一页的宽度 + colum * itemView的宽度
itemRect.right = itemRect.left + itemView的宽度
itemRect.top = row * itemView的高度
itemRect.bottom = itemRect.top + itemView的高度
接着根据滑动的距离,计算出可见范围的 Rect,遍历 items 只要在可见范围内则添加到屏幕上即可:
// 可见范围
val outRect = Rect(滑动偏移量, 0, 滑动偏移量 + 一页的宽度, 一页的高度)
while (index < itemCount) {
val itemRect
if (Rect.intersects(outRect, itemRect)) { // 在范围内
// 添加 测量 布局 itemView 即可
}
index++
}
滑动
滑动 LayoutManager 给出了接口:
// 是否可以水平滑动
public boolean canScrollHorizontally() {
return false;
}
// 水平滑动触发 需要返回消费的滑动距离
public int scrollHorizontallyBy(int dx, Recycler recycler, State state) {
return 0;
}
// 是否可以垂直滑动
public boolean canScrollVertically() {
return false;
}
// 垂直滑动触发
public int scrollVerticallyBy(int dy, Recycler recycler, State state) {
return 0;
}
重写对应方法返回 true 即可,实现水平滑动可以重写 scrollHorizontallyBy 在其中利用 offsetChildrenHorizontal 方法进行滑动,但是要注意边界检测(如果做无限循环的 LayoutManager 就不需要检测边界了)。
回收
回收复用是重中之重,在合适的时机(当前Demo的场景当 item 不在可见范围内即可回收),LayoutManager 并不处理回收,而是都要交给 Recycler 去处理,LayoutManager 也给我们提供了一些 Api:
detachAndScrapAttachedViews // 回收所有可见的 View
detachAndScrapView // 回收指定 View
detachView // 轻量级回收,用于马上要 attach 回来的情况
复用
复用一般只需要调用 recycler.getViewForPosition 即可,会根据 RecyclerView 的缓存机制一层一层的获取缓存。
class NavigationGridLayoutManager : RecyclerView.LayoutManager() {
private var mItemViewWidth = 0 // itemView 宽度
private var mItemViewHeight = 0 // itemView 高度
private val mColumCount = 5 // 列数
private val mRowCount = 2 // 行数
private var mPageCount = 0 // 页面数
private var mPageItemSize = 0 // 一页能放多少个 item = mRowCount * mColumCount
private var mPageWidth = 0 // 一页的宽度
private var mPageHeight = 0 // 一页的高度
private var mOffsetHorizontal = 0 // 水平滑动偏移量 用于计算可见范围 布局子 View
private var mMaxOffsetHorizontal = 0 // 水平滑动最大偏移量 滑动边界
override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {
return RecyclerView.LayoutParams(
RecyclerView.LayoutParams.WRAP_CONTENT,
RecyclerView.LayoutParams.WRAP_CONTENT
)
}
override fun onMeasure(
recycler: RecyclerView.Recycler,
state: RecyclerView.State,
widthSpec: Int,
heightSpec: Int
) {
// 这里只考虑水平滑动的场景 因为需要均分高度 高度一定要是测量值 所以重写 onMeasure 检查 heightMode
val heightSize = MeasureSpec.getSize(heightSpec)
var heightMode = MeasureSpec.getMode(heightSpec)
if (heightMode != MeasureSpec.EXACTLY && heightSize > 0) {
heightMode = MeasureSpec.EXACTLY
}
super.onMeasure(
recycler,
state,
widthSpec,
MeasureSpec.makeMeasureSpec(heightSize, heightMode)
)
}
override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
if (state.isPreLayout) {
return
}
if (itemCount == 0) {
detachAndScrapAttachedViews(recycler)
return
}
// 获取一页的宽高、itemView均分后的宽高、一页的item数量、页面数量
mPageWidth = width - paddingLeft - paddingRight
mPageHeight = height - paddingTop - paddingBottom
mItemViewWidth = mPageWidth / mColumCount
mItemViewHeight = mPageHeight / mRowCount
mPageItemSize = mRowCount * mColumCount
mPageCount = itemCount / mPageItemSize
if (itemCount % mPageItemSize > 0) {
mPageCount++
}
// 最大滑动边界
mMaxOffsetHorizontal = (mPageCount - 1) * mPageWidth
// 模仿 LinearLayoutManager 在 fill 方法中进行填充布局的操作
fill(recycler, state, 0)
}
// 允许水平滑动
override fun canScrollHorizontally(): Boolean {
return true
}
// 水平滑动处理
override fun scrollHorizontallyBy(
dx: Int,
recycler: RecyclerView.Recycler,
state: RecyclerView.State
): Int {
// 因为有边界,所以需要获取实际滑动消费的距离
val newX: Int = mOffsetHorizontal + dx
var result = dx
if (newX > mMaxOffsetHorizontal) {
result = mMaxOffsetHorizontal - mOffsetHorizontal
} else if (newX < 0) {
result = 0 - mOffsetHorizontal
}
mOffsetHorizontal += result // 记录滑动的偏移量 用于计算可见范围
offsetChildrenHorizontal(-result) // 滑动子View
fill(recycler, state, result) // 填充布局
return result
}
private fun fill(recycler: RecyclerView.Recycler, state: RecyclerView.State, dx: Int) {
if (state.isPreLayout) {
return
}
// 先将屏幕上的 View 全部分离进缓存
detachAndScrapAttachedViews(recycler)
// 计算出可见范围
val outRect = Rect(
mOffsetHorizontal,
0,
mPageWidth + mOffsetHorizontal,
mPageHeight
)
// 遍历 item
// 注意:这么写性能很烂 如果有 itemCount 增大后 这个循环会导致严重的卡顿
// 篇幅原因就简单处理了 遍历了所有 View
// 正确做法:根据可见范围计算出 遍历的区间
var startPosition = 0
while (startPosition < itemCount) {
val itemRect = Rect()
// 在第几页
val page = startPosition / mPageItemSize
// 在分页的第几个位置
val pageIndex = (startPosition) % mPageItemSize
// 第几列
val colum = pageIndex % mColumCount
// 第几行
val row = pageIndex / mColumCount
// 位置
itemRect.left = page * mPageWidth + colum * mItemViewWidth
itemRect.right = itemRect.left + mItemViewWidth
itemRect.top = row * mItemViewHeight
itemRect.bottom = itemRect.top + mItemViewHeight
// 是否在可见范围内
if (Rect.intersects(outRect, itemRect)) {
val itemView = recycler.getViewForPosition(startPosition) // 获取 View
addView(itemView) // 添加 View
measureChildWithMargins(itemView, 0, 0) // 测量
layoutDecoratedWithMargins( // 对 View 进行布局
itemView,
itemRect.left - mOffsetHorizontal,
itemRect.top,
itemRect.right - mOffsetHorizontal,
itemRect.bottom
)
}
startPosition++
}
}
}
ItemDecoration
由于 ItemDecoration 的源码很简单,直接进入源码:
public abstract static class ItemDecoration {
public void onDraw(Canvas c, RecyclerView parent,State state) {
onDraw(c, parent);
}
public void onDrawOver(Canvas c, RecyclerView parent, State state) {
onDrawOver(c, parent);
}
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
getItemOffsets(outRect, ((LayoutParams)view.getLayoutParams()).getViewLayoutPosition(), parent);
}
// 方法标记删除 使用上面的 onDraw
@Deprecated
public void onDraw(Canvas c, RecyclerView parent) {
}
// 方法标记删除 使用上面的 onDrawOver
@Deprecated
public void onDrawOver(Canvas c, RecyclerView parent) {
}
// 方法标记删除 使用上面的 getItemOffsets
@Deprecated
public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
outRect.set(0, 0, 0, 0);
}
}
onDraw、onDrawOver
RecyclerView.java
public void draw(Canvas c) {
super.draw(c);
// draw 方法上来就循环 mItemDecorations 逐个调用了 onDrawOver 方法
// mItemDecorations 就不贴代码了 就是个 ArrayList 存储 ItemDecoration 对象
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDrawOver(c, this, mState);
}
// ...
}
View 的 draw 方法中会调用 onDraw,那么接着看一下 onDraw 方法:
RecyclerView.java
public void onDraw(Canvas c) {
super.onDraw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
// 同样是遍历 这次调用的是 ItemDecoration 的 onDraw 方法
mItemDecorations.get(i).onDraw(c, this, mState);
}
}
RecyclerView 的 draw 方法先调用了 super.draw(),在父类 View 的 draw 方法中先调用了 onDraw,然后再执行RecyclerView 重写后剩余 draw 方法中的代码,所以 ItemDecoration onDraw 方法是先执行,onDrawOver 则是后执行。
getItemOffsets
public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
outRect.set(0, 0, 0, 0);
}
先来个示意图来理解一下 getItemOffsets 中 outRect 的含义:
getItemOffsets 设置偏移前后的区别:
就是给 itemView 设置了一个内部偏移,说的再简单点就是 itemView 内容的绘制区域缩小了。
接着查看下其在 RecyclerView 的调用时机,在 RecyclerView 中搜索后发现仅有一处调用:
RecyclerView.java
Rect getItemDecorInsetsForChild(View child) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// ...
// 从 LayoutParams 中取出
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 进行累加
insets.left += mTempRect.left;
insets.top += mTempRect.top;
insets.right += mTempRect.right;
insets.bottom += mTempRect.bottom;
}
lp.mInsetsDirty = false;
return insets;
}
可以看出每个 ItemDecoration 设置的偏移量都进行累加存储在了 LayoutParams 的 mDecorInsets 中,继续查看下 getItemDecorInsetsForChild 的调用时机:
RecyclerView.java
// 测量 View
public void measureChild(@NonNull View child, int widthUsed, int heightUsed) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 获取 mDecorInsets 累加完成后的结果
final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
widthUsed += insets.left + insets.right;
heightUsed += insets.top + insets.bottom;
final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
getPaddingLeft() + getPaddingRight() + widthUsed, lp.width,
canScrollHorizontally());
final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
getPaddingTop() + getPaddingBottom() + heightUsed, lp.height,
canScrollVertically());
if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
child.measure(widthSpec, heightSpec);
}
}
// 和上面 measureChild 是一样的
public void measureChildWithMargins(@NonNull View child, int widthUsed, int heightUsed) {
//...
// 区别在于 widthSpec heightSpec 计算时增加了 Margin
final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
getPaddingLeft() + getPaddingRight()
+ lp.leftMargin + lp.rightMargin + widthUsed, lp.width,
canScrollHorizontally());
final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
getPaddingTop() + getPaddingBottom()
+ lp.topMargin + lp.bottomMargin + heightUsed, lp.height,
canScrollVertically());
//...
}
从上面的代码不难看出,在测量子 View(也就是 itemView)时,整体占用的宽高是包含设置的偏移量的。
示意图
最后再来个示意图将三个方法结合起来理解下:
-
onDraw 绘制的内容在最底层,itemView 绘制在其内容之上,而 onDrawOver 绘制的内容在最上层;
-
图中 onDrawOver 虽然绘制在了 itemView 之中,但其实可以超出 itemView,我怕会有歧义特别说明一下;
-
如果想让 onDraw 绘制的内容不被 itemView 遮挡就重写 getItemOffsets 方法设置合适的偏移;
自定义 ItemDecoration
进度条首先要保证不被 item 遮挡,那么绘制的逻辑要写在 onDrawOver 保证其绘制在最上层。其次也要保证进度条不遮挡 item,需要重写 getItemOffsets 对 item 设置底部偏移。
绘制逻辑比较简单用 drawLine 画进度条背景、进度条即可。进度等于 当滑动距离 / 最大滑动距离 ,获取这两个值可以通过 RecyclerView 的 重写 computeHorizontalScrollOffset 和 computeHorizontalScrollRange 两个方法,可以看下源码:
RecyclerView.java
public class RecyclerView {
// ...
public int computeHorizontalScrollOffset() {
if (mLayout == null) {
return 0;
}
return mLayout.canScrollHorizontally() ? mLayout.computeHorizontalScrollOffset(mState) : 0;
}
@Override
public int computeHorizontalScrollRange() {
if (mLayout == null) {
return 0;
}
return mLayout.canScrollHorizontally() ? mLayout.computeHorizontalScrollRange(mState) : 0;
}
// ...
}
可以看出 RecyclerView 又交给了 LayoutManager 去获取,而 LayoutManager 中两个方法默认返回 0,需要我们自己重写实现计算。
class NavigationGridLayoutManager : RecyclerView.LayoutManager() {
// ...
override fun computeHorizontalScrollOffset(state: RecyclerView.State): Int {
return mOffsetHorizontal
}
override fun computeHorizontalScrollRange(state: RecyclerView.State): Int {
return mMaxOffsetHorizontal
}
}
接着自定义 ItemDecoration:
class NavigationItemDecoration : RecyclerView.ItemDecoration() {
private val mPaint = Paint().apply {
strokeCap = Paint.Cap.ROUND
}
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDrawOver(c, parent, state)
// 滑动偏移量
val scrollOffset = parent.computeHorizontalScrollOffset().toFloat()
// 最大滑动距离
val scrollRange = parent.computeHorizontalScrollRange().toFloat()
// 进度条高度
val progressHeight = 4f.dp
// 进度条整体宽度
val progressBgWidth = 100f.dp
// 进度条宽度
val progressWidth = 10f.dp
// 先画进度条背景
mPaint.color = Color.LTGRAY
mPaint.strokeWidth = progressHeight
val progressBgStartX = parent.width / 2f - progressBgWidth / 2
val progressBgEndX = parent.width / 2f + progressBgWidth / 2
val progressBgStartY = parent.height - progressHeight
val progressBgEndY = parent.height - progressHeight
c.drawLine(progressBgStartX, progressBgStartY, progressBgEndX, progressBgEndY, mPaint)
// 画进度条
mPaint.color = Color.GREEN
val progressStartX = min(
progressBgEndX - progressWidth,
progressBgStartX + progressBgWidth * (scrollOffset / scrollRange)
)
val progressEndX = progressStartX + progressWidth
val progressStartY = progressBgStartY
val progressEndY = progressBgEndY
c.drawLine(progressStartX, progressStartY, progressEndX, progressEndY, mPaint)
}
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
// 设置底部偏移 防止被遮挡
outRect.set(0, 0, 0, 10.dp)
}
}
ItemAnimator
一般我们给 RecyclerView 设置动画会调用 setItemAnimator 方法,直接看一下源码:
RecyclerView.java
// 默认动画 DefaultItemAnimator
ItemAnimator mItemAnimator = new DefaultItemAnimator();
public void setItemAnimator(@Nullable ItemAnimator animator) {
if (mItemAnimator != null) { // 先结束动画,取消监听
mItemAnimator.endAnimations();
mItemAnimator.setListener(null);
}
mItemAnimator = animator; // 赋值
if (mItemAnimator != null) { // 重新设置监听
mItemAnimator.setListener(mItemAnimatorListener);
}
}
可以看出 RecyclerView 默认提供了 DefaultItemAnimator,先不着急分析它,首先我们要分析出动画的执行流程以及动画的信息是怎么处理的。先了解以下这么几个类作为基础。
ItemHolderInfo
RecylerView.java
public static class ItemHolderInfo {
public int left;
public int top;
public int right;
public int bottom;
@AdapterChanges
public int changeFlags;
public ItemHolderInfo() {
}
public ItemHolderInfo setFrom(@NonNull RecyclerView.ViewHolder holder) {
return setFrom(holder, 0);
}
@NonNull
public ItemHolderInfo setFrom(@NonNull RecyclerView.ViewHolder holder,
@AdapterChanges int flags) {
final View view = holder.itemView;
this.left = view.getLeft();
this.top = view.getTop();
this.right = view.getRight();
this.bottom = view.getBottom();
return this;
}
}
ItemHolderInfo 作为 RecyclerView 的内部类,代码非常简单,向外暴露 setFrom 方法,用于存储 ViewHolder 的位置信息;
InfoRecord
InfoRecord 是 ViewInfoStore 的内部类(下一小节分析),代码也非常简单:
ViewInfoStore.java
static class InfoRecord {
// 一些 Flag 定义
static final int FLAG_DISAPPEARED = 1; // 消失
static final int FLAG_APPEAR = 1 << 1; // 出现
static final int FLAG_PRE = 1 << 2; // 预布局
static final int FLAG_POST = 1 << 3; // 真正布局
static final int FLAG_APPEAR_AND_DISAPPEAR = FLAG_APPEAR | FLAG_DISAPPEARED;
static final int FLAG_PRE_AND_POST = FLAG_PRE | FLAG_POST;
static final int FLAG_APPEAR_PRE_AND_POST = FLAG_APPEAR | FLAG_PRE | FLAG_POST;
// 这个 flags 要记住!!
// 后面多次对其进行赋值,且执行动画时也根据 flags 来判断动画类型;
int flags;
// ViewHolder 坐标信息
RecyclerView.ItemAnimator.ItemHolderInfo preInfo; // 预布局阶段的
RecyclerView.ItemAnimator.ItemHolderInfo postInfo; // 真正布局阶段的
// 池化 提高效率
static Pools.Pool<InfoRecord> sPool = new Pools.SimplePool<>(20);
// ...
// 其内部的一些方法都是复用池相关 特别简单 就不贴了
}
不难看出,InfoRecord 功能和他的名字一样信息记录,主要记录了预布局、真正布局两个阶段的 ViewHodler 的位置信息(ItemHolderInfo)。
ViewInfoStore
class ViewInfoStore {
// 将 ViewHodler 和 InfoRecord 以键值对形式存储
final SimpleArrayMap<RecyclerView.ViewHolder, InfoRecord> mLayoutHolderMap =
new SimpleArrayMap<>();
// 根据坐标存储 ViewHodler 看名字也看得出是 旧的,旧是指:
// 1.viewHolder 被隐藏 但 未移除
// 2.隐藏item被更改
// 3.预布局跳过的 item
final LongSparseArray<RecyclerView.ViewHolder> mOldChangedHolders = new LongSparseArray<>();
// mLayoutHolderMap 中添加一项 如果有就改变 InfoRecord 的值
// 下面很多方法都是类似功能 下面的就不贴 if 里面那段了
void addToPreLayout(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) {
InfoRecord record = mLayoutHolderMap.get(holder);
if (record == null) { // 没有就构建一个 加入 map
record = InfoRecord.obtain();
mLayoutHolderMap.put(holder, record);
}
record.preInfo = info;
record.flags |= FLAG_PRE; // 跟方法名对应的 flag
}
// 调用 popFromLayoutStep 传递 FLAG_PRE
RecyclerView.ItemAnimator.ItemHolderInfo popFromPreLayout(RecyclerView.ViewHolder vh) {
return popFromLayoutStep(vh, FLAG_PRE);
}
// 调用 popFromLayoutStep 传递 FLAG_POST
RecyclerView.ItemAnimator.ItemHolderInfo popFromPostLayout(RecyclerView.ViewHolder vh) {
return popFromLayoutStep(vh, FLAG_POST);
}
// 上面两个方法都调用的这里 flag 传递不同
private RecyclerView.ItemAnimator.ItemHolderInfo popFromLayoutStep(RecyclerView.ViewHolder vh, int flag) {
int index = mLayoutHolderMap.indexOfKey(vh);
if (index < 0) {
return null;
}
// 从map中获取
final InfoRecord record = mLayoutHolderMap.valueAt(index);
if (record != null && (record.flags & flag) != 0) {
record.flags &= ~flag;
final RecyclerView.ItemAnimator.ItemHolderInfo info;
if (flag == FLAG_PRE) { // 根据 flag 获取对应的 ItemHolderInfo
info = record.preInfo;
} else if (flag == FLAG_POST) {
info = record.postInfo;
} else {
throw new IllegalArgumentException("Must provide flag PRE or POST");
}
// 如果没有包含两个阶段的flag 直接回收
if ((record.flags & (FLAG_PRE | FLAG_POST)) == 0) {
mLayoutHolderMap.removeAt(index);
InfoRecord.recycle(record);
}
return info;
}
return null;
}
// 向 mOldChangedHolders 添加一个 holder
void addToOldChangeHolders(long key, RecyclerView.ViewHolder holder) {
mOldChangedHolders.put(key, holder);
}
// 和 addToPreLayout 方法类似 flags 不同
void addToAppearedInPreLayoutHolders(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) {
InfoRecord record = mLayoutHolderMap.get(holder);
// ...
record.flags |= FLAG_APPEAR;
record.preInfo = info;
}
// 和 addToPreLayout 方法类似 flags 不
// 注意这里的方法名 是添加的 post-layout 真正布局阶段的信息
void addToPostLayout(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) {
InfoRecord record = mLayoutHolderMap.get(holder);
// ...
record.postInfo = info; // 这里赋值的是 postInfo
record.flags |= FLAG_POST;
}
// 这里直接拿到 InfoRecord 修改了 flag
void addToDisappearedInLayout(RecyclerView.ViewHolder holder) {
InfoRecord record = mLayoutHolderMap.get(holder);
// ...
record.flags |= FLAG_DISAPPEARED;
}
// 这里直接拿到 InfoRecord 修改了 flag
void removeFromDisappearedInLayout(RecyclerView.ViewHolder holder) {
InfoRecord record = mLayoutHolderMap.get(holder);
// ...
record.flags &= ~FLAG_DISAPPEARED;
}
// 移除 两个容器都移除
void removeViewHolder(RecyclerView.ViewHolder holder) {
//...
mOldChangedHolders.removeAt(i);
//...
final InfoRecord info = mLayoutHolderMap.remove(holder);
}
// 这里其实是 动画开始的入口
void process(ProcessCallback callback) {
// 倒着遍历 mLayoutHolderMap
for (int index = mLayoutHolderMap.size() - 1; index >= 0; index--) {
final RecyclerView.ViewHolder viewHolder = mLayoutHolderMap.keyAt(index);
final InfoRecord record = mLayoutHolderMap.removeAt(index);
// 取出 InfoRecord 根据 flag 和 两个阶段位置信息 进行判断 触发对应的 callback 回调方法
if ((record.flags & FLAG_APPEAR_AND_DISAPPEAR) == FLAG_APPEAR_AND_DISAPPEAR) {
callback.unused(viewHolder);
} // 一大堆判断就先省略了,后面会提到 ...
// ...
// 最后回收
InfoRecord.recycle(record);
}
}
// ...
}
ViewInfoStore,字面翻译为 View信息商店,类名就体现出了他的功能,主要提供了对 ViewHolder 的 InfoRecord 存储以及修改,并且提供了动画触发的入口。
ProcessCallback
还有最后一个类需要了解,也就是上面 ViewInfoStore 最后一个方法 process 中用到的 callback,直接看源码:
ViewInfoStore.java
//前三个需要做动画的方法传入了 viewHolder 以及其预布局、真正布局两个阶段的位置信息
interface ProcessCallback {
// 进行消失
void processDisappeared(RecyclerView.ViewHolder viewHolder, RecyclerView.ItemAnimator.ItemHolderInfo preInfo, RecyclerView.ItemAnimator.ItemHolderInfo postInfo);
// 进行出现
void processAppeared(RecyclerView.ViewHolder viewHolder, RecyclerView.ItemAnimator.ItemHolderInfo preInfo, RecyclerView.ItemAnimator.ItemHolderInfo postInfo);
// 持续 也就是 不变 或者 数据相同大小改变的情况
void processPersistent(RecyclerView.ViewHolder viewHolder, RecyclerView.ItemAnimator.ItemHolderInfo preInfo, RecyclerView.ItemAnimator.ItemHolderInfo postInfo);
// 未使用
void unused(RecyclerView.ViewHolder holder);
}
ProcessCallback 在 RecyclerView 有默认实现,这个待会再详细分析,看 callback 的方法名也能略知一二,分别对应 ViewHolder 做动画的几种情况;
那么从前三个方法的参数中也能推断出,ViewHolder 做动画时,动画的数据也是从 preInfo 和 postInfo 两个参数中做计算得出。
动画处理
前置基础有点多😂😂😂,不过通过对上面几个类有一些了解,下面在分析动画触发、信息处理时就不用反复解释一些变量的意义了。
dispatchLayoutStep3
在之前分析布局阶段的博客中提到 dispatchLayoutStep1、2、3 三个核心方法,分别对应三种状态 STEP_START、STEP_LAYOUT、STEP_ANIMATIONS;
很显然,STEP_ANIMATIONS 是执行动画的阶段,再来看一下 dispatchLayoutStep3 方法中对 item 动画进行了哪些操作:
RecyclerView.java
private void dispatchLayoutStep3() {
// ...
if (mState.mRunSimpleAnimations) { // 需要做动画
// 倒着循环 因为可能会发生移除
for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
// 获取到 holder
ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
if (holder.shouldIgnore()) { // 如果被标记忽略则跳过
continue;
}
// 获取 holder 的 key 一般情况获取的就是 position
long key = getChangedHolderKey(holder);
// 前置基础中 提到的 ItemHolderInfo
// recordPostLayoutInformation 内部构建了一个 ItemHolderInfo 并且调用了 setFrom 设置了 位置信息
final ItemHolderInfo animationInfo = mItemAnimator.recordPostLayoutInformation(mState, holder);
// 从 ViewInfoStore 的 mOldChangedHolders 中获取 vh
ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key);
if (oldChangeViewHolder != null && !oldChangeViewHolder.shouldIgnore()) {
// 是否正在执行消失动画
final boolean oldDisappearing = mViewInfoStore.isDisappearing(oldChangeViewHolder);
final boolean newDisappearing = mViewInfoStore.isDisappearing(holder);
// 这个 if 判断 将要发生更新动画的 vh 已经在执行消失动画
if (oldDisappearing && oldChangeViewHolder == holder) {
// 用消失动画代替
mViewInfoStore.addToPostLayout(holder, animationInfo);
} else {
// 获取 预布局阶段的 ItemHolderInfo
final ItemHolderInfo preInfo = mViewInfoStore.popFromPreLayout(oldChangeViewHolder);
// 设置 真正布局阶段的 ItemHolderInfo
mViewInfoStore.addToPostLayout(holder, animationInfo);
// 然后在取出来
ItemHolderInfo postInfo = mViewInfoStore.popFromPostLayout(holder);
if (preInfo == null) {
handleMissingPreInfoForChangeError(key, holder, oldChangeViewHolder);
} else { // 用上面取出来的 preInfo postInfo 做更新动画
animateChange(oldChangeViewHolder, holder, preInfo, postInfo,oldDisappearing, newDisappearing);
}
}
} else { // 没有获取到 直接设置 hodler 真正布局阶段的 位置信息 并且设置 flag
mViewInfoStore.addToPostLayout(holder, animationInfo);
}
}
// 开始执行动画
mViewInfoStore.process(mViewInfoProcessCallback);
}
// ...
}
代码中的注释写的比较详细,主要注意一下 mViewInfoStore.addToPostLayout 会给 ViewHolder 生成 InfoRecord 对象,并且设置 postInfo,并且给 flags 添加 FLAG_POST,然后以 <ViewHolder, InfoRecord> 键值对形式添加到 ViewInfoStore 的 mLayoutHolderMap 中;
dispatchLayoutStep1
其实上一小节 dispatchLayoutStep3 方法中也包含对动画信息的处理,也就是针对真正布局后的位置信息设置的相关代码。那么删除、新增的动画在哪里实现呢?首先,回顾一下之前分析的布局流程,真正的布局发生在 dispatchLayoutStep2 中,预布局发生在 dispatchLayoutStep1 中,结合之前对预布局的简单解释,不难理解出预布局时肯定也对动画信息进行了处理,那么直接看一下 dispatchLayoutStep1 的相关源码,这部分需要分成两段来分析,先看第一段:
RecyclerView.java
private void dispatchLayoutStep1() {
// ...
if (mState.mRunSimpleAnimations) {
// 遍历 child
int count = mChildHelper.getChildCount();
for (int i = 0; i < count; ++i) {
// 获取 vh
final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
// 忽略、无效的 跳过
if (holder.shouldIgnore() || (holder.isInvalid() && !mAdapter.hasStableIds())) {
continue;
}
// 构造出 ItemHolderInfo
final ItemHolderInfo animationInfo = mItemAnimator
.recordPreLayoutInformation(mState, holder,
ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
holder.getUnmodifiedPayloads());
// 注意这里 时 addToPreLayout. 表示预布局阶段
// 此时设置的是 InfoRecord 的 preInfo,flag 是 FLAG_PRE
mViewInfoStore.addToPreLayout(holder, animationInfo);
// 如果 holder 发生改变 添加到 ViewInfoStore 的 mOldChangedHolders 中
if (mState.mTrackOldChangeHolders && holder.isUpdated() && !holder.isRemoved()
&& !holder.shouldIgnore() && !holder.isInvalid()) {
long key = getChangedHolderKey(holder); // 获取 key 一般是 position
mViewInfoStore.addToOldChangeHolders(key, holder);
}
}
}
// ...
}
这一段也不复杂,记录当前 holder 预布局阶段的位置信息(InfoRecord 的 preInfo)到 ViewInfoStore 的 mLayoutHolderMap 中,且添加了 FLAG_PRE 到 flags 中;
并且如果 holder 发生改变就添加到 ViewInfoStore 的 mOldChangedHolders 中;
再看下面的代码:
RecyclerView.java
private void dispatchLayoutStep1() {
// ...
if (mState.mRunPredictiveAnimations) {
// ...
// 这次是预布局 计算可用空间时忽略了要删除的项目 所以如果发生删除 会有新的 item 添加进去
mLayout.onLayoutChildren(mRecycler, mState);
// ...
// 遍历 child
for (int i = 0; i < mChildHelper.getChildCount(); ++i) {
final View child = mChildHelper.getChildAt(i);
final ViewHolder viewHolder = getChildViewHolderInt(child);
if (viewHolder.shouldIgnore()) {
continue;
}
// 这个判断也就是没有经历过上一部分代码的 vh (onLayoutChildren 中新加入的 item)
// InfoRecord 为 null 或者 flags 不包含 FLAG_PRE
if (!mViewInfoStore.isInPreLayout(viewHolder)) {
int flags = ItemAnimator.buildAdapterChangeFlagsForAnimations(viewHolder);
// 判断是否是隐藏的
boolean wasHidden = viewHolder
.hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
if (!wasHidden) { // 没有隐藏 则标记在预布局阶段出现
flags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
}
// 构造出 ItemHolderInfo
final ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation(
mState, viewHolder, flags, viewHolder.getUnmodifiedPayloads());
if (wasHidden) {
// 隐藏的 如果发生更新 并且没有被移除 就添加到 mOldChangedHolders
// 设置 preInfo 设置 flag为 FLAG_PRE
recordAnimationInfoIfBouncedHiddenView(viewHolder, animationInfo);
} else { // 没有隐藏的 设置 flag FLAG_APPEAR, 并且设置 preInfo
mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);
}
}
}
clearOldPositions();
} else {
clearOldPositions();
}
// ...
}
这里结合之前解释预布局时的图来理解下:
第一部分执行时,item1、2、3 都会执行 addToPreLayout,addToPreLayout 会生成 InfoRecord 并且设置其 preInfo 存储 vh 的位置信息,然后以 <ViewHolder, InfoRecord> 键值对形式添加到 ViewInfoStore 的 mLayoutHolderMap 中;
然后第二部分执行了 onLayoutChildren 进行了预布局,以 LinearLayoutManager 为例,在计算可用空间时会忽略要删除的 item3,从而 item4 被添加到 RecyclerView 中,再次对 child 进行遍历时进行 mViewInfoStore.isInPreLayout(viewHolder) 判断时显然 item4 对应的 ViewHolder 在 mLayoutHolderMap 中获取为 null,那么就能知道 item4 属于新增出来的,就在最后调用 mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo); 生成 InfoRecord 设置位置信息,并且添加 flag 为 FLAG_APPEAR 添加到 mLayoutHolderMap 中。
总结
这部分源码是倒着来分析的(先看 dispatchLayoutStep3 在看 1 ),可能有点不好理解,先从这三个布局核心方法的角度来稍稍总结一下(均假设需要执行动画):
dispatchLayoutStep1
- 首先将当前屏幕中的 items 信息保存;(生成 ItemHolderInfo 赋值给 InfoRecord 的 preInfo 并且对其 flags 添加 FLAG_PRE ,再将 InfoRecord 添加到 ViewInfoStore 的 mLayoutHolderMap 中)
- 进行预布局;(调用 LayoutManager 的 onLayoutChildren)
- 预布局完成后和第 1 步中保存的信息对比,将新出现的 item 信息保存;(和第 1 步中不同的是 flags 设置的是 FLAG_APPEAR)
dispatchLayoutStep2
- 将预布局 boolean 值改为 flase;
- 进行真正布局;(调用 LayoutManager 的 onLayoutChildren)
dispatchLayoutStep3
- 将真正布局后屏幕上的 items 信息保存;(与 dispatchLayoutStep1 不同的是赋值给 InfoRecord 的 postInfo 并且 flags 添加 FLAG_POST)
- 执行动画,调用 ViewInfoStore.process;
- 布局完成回调,onLayoutCompleted;
动画执行
经过上面两个 dispatchLayoutStep1 和 3 方法的执行,ViewInfoStore 中已经有预布局时 item 的信息、真正布局后的 item 信息、以及对应的 flags。最终调用了 ViewInfoStore 的 process 执行动画:
这里面的代码不难,就是根据 flags 进行判断执行对应的动画,调用的是 ProcessCallback 中的方法进行执行,那么看一下 ProcessCallback 的具体实现:
RecyclerView.java
private final ViewInfoStore.ProcessCallback mViewInfoProcessCallback =
new ViewInfoStore.ProcessCallback() {
// ...
// 这里就以执行新增动画为例 其他的也都差不多
@Override
public void processAppeared(ViewHolder viewHolder,
ItemHolderInfo preInfo, ItemHolderInfo info) {
// 调用 animateAppearance
animateAppearance(viewHolder, preInfo, info);
}
// ...
};
void animateAppearance(@NonNull ViewHolder itemHolder, @Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) {
// 先标记 vh 不能被回收
itemHolder.setIsRecyclable(false);
// mItemAnimator 上面也提过了 又默认实现 待会再分析
if (mItemAnimator.animateAppearance(itemHolder, preLayoutInfo, postLayoutInfo)) {
// 这里看方法名也知道是 post 一个 runnable
postAnimationRunner();
}
}
void postAnimationRunner() {
if (!mPostedAnimatorRunner && mIsAttached) {
// 核心就是 post 的 mItemAnimatorRunner
ViewCompat.postOnAnimation(this, mItemAnimatorRunner);
mPostedAnimatorRunner = true;
}
private Runnable mItemAnimatorRunner = new Runnable() {
@Override
public void run() {
if (mItemAnimator != null) {
// 有调用到了 mItemAnimator 中
mItemAnimator.runPendingAnimations();
}
mPostedAnimatorRunner = false;
}
};
整个调用下来核心就在于 ItemAnimator 的两个方法调用(animateAppearance、runPendingAnimations),那么下面我们就来进入 ItemAnimator 的分析;
ItemAnimator
在最开始的前置基础小节提到 mItemAnimator 实际上是 DefaultItemAnimator;而 DefaultItemAnimator 继承自 SimpleItemAnimator,SimpleItemAnimator 又继承自 ItemAnimator。ItemAnimator 是 RecyclerView 的内部类,其内部大部分是抽象方法需要子类实现,就简单说说其主要功能不贴代码了:
- ItemHolderInfo 是 ItemAnimator 的内部类,用于保存位置信息;
- ItemAnimatorListener 是其内部动画完成时的回调接口;
- 提供设置动画时间、动画执行、动画开始结束回调、动画状态的方法,大部分是需要子类实现的;
而上述提供的 animateAppearance 和 runPendingAnimations 都是抽象方法,这里并没有实现;
SimpleItemAnimator
SimpleItemAnimator 继承自 ItemAnimator,乍一看方法很多,大部分都是空实现或抽象方法:
这一堆 dispatchXXX 方法和 onXXX 方法是一一对应的,dispatchXXX 中调用 onXXX,而 onXXX 都是空方法交给子类去实现,这部分代码很简单就不贴了;
SimpleItemAnimator 实现了 animateAppearance:
public boolean animateAppearance(RecyclerView.ViewHolder viewHolder,ItemHolderInfo preLayoutInfo, ItemHolderInfo postLayoutInfo) {
if (preLayoutInfo != null && (preLayoutInfo.left != postLayoutInfo.left
|| preLayoutInfo.top != postLayoutInfo.top)) {
return animateMove(viewHolder, preLayoutInfo.left, preLayoutInfo.top,
postLayoutInfo.left, postLayoutInfo.top);
} else {
return animateAdd(viewHolder);
}
}
逻辑很简单,如果 preLayoutInfo 不为空,并且 preLayoutInfo 和 postLayoutInfo 的 top、left 不同则调用 animateMove 否则调用 animateAdd;看名字也大致能理解是处理移除动画和添加动画;
对于 runPendingAnimations SimpleItemAnimator 还是没有实现;
DefaultItemAnimator
DefaultItemAnimator 继承自 SimpleItemAnimator,上述两个父类中都没有真正执行动画,那么执行动画一定在 DefaultItemAnimator 内部;在看其 runPendingAnimations 实现前先大概了解下类的结构;
// mPendingXXX 容器存放将要执行动画的 ViewHodler
private ArrayList<RecyclerView.ViewHolder> mPendingRemovals = new ArrayList<>();
private ArrayList<RecyclerView.ViewHolder> mPendingAdditions = new ArrayList<>();
// 这里的 MoveInfo,ChangeInfo 下面解释
private ArrayList<MoveInfo> mPendingMoves = new ArrayList<>();
private ArrayList<ChangeInfo> mPendingChanges = new ArrayList<>();
// mXXXAnimations 容器存放正在执行动画的 ViewHolder
ArrayList<RecyclerView.ViewHolder> mAddAnimations = new ArrayList<>();
ArrayList<RecyclerView.ViewHolder> mMoveAnimations = new ArrayList<>();
ArrayList<RecyclerView.ViewHolder> mRemoveAnimations = new ArrayList<>();
ArrayList<RecyclerView.ViewHolder> mChangeAnimations = new ArrayList<>();
// MoveInfo 额外存储了执行移除动画前后的坐标信息用于动画执行
private static class MoveInfo {
public RecyclerView.ViewHolder holder;
public int fromX, fromY, toX, toY;
// ...
}
// ChangeInfo 想比于 MoveInfo 额外存储了 oldHolder
private static class ChangeInfo {
public RecyclerView.ViewHolder oldHolder, newHolder;
public int fromX, fromY, toX, toY;
// ...
}
上面的代码逻辑很简单,注释都详细说明了,就不再解释了,最后来看看 animateRemoveImpl,animateMoveImpl 方法:
private void animateRemoveImpl(final RecyclerView.ViewHolder holder) {
// 拿到 itemView
final View view = holder.itemView;
// 构建动画
final ViewPropertyAnimator animation = view.animate();
// 添加到正在执行动画的容器
mRemoveAnimations.add(holder);
// 执行动画
animation.setDuration(getRemoveDuration()).alpha(0).setListener(
new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animator) {
// 开始执行动画回调
// SimpleItemAnimator 中默认空实现
dispatchRemoveStarting(holder);
}
@Override
public void onAnimationEnd(Animator animator) {
// 动画结束后的一些操作
animation.setListener(null);
view.setAlpha(1);
// 当前 item 动画执行结束回调
dispatchRemoveFinished(holder);
mRemoveAnimations.remove(holder);
// 所有动画执行完成后的回调
// 内部通过判断上述各个容器是否为空触发回调
dispatchFinishedWhenDone();
}
}).start(); // 执行动画
}
void animateMoveImpl(final RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) {
final View view = holder.itemView;
final int deltaX = toX - fromX;
final int deltaY = toY - fromY;
if (deltaX != 0) {
view.animate().translationX(0);
}
if (deltaY != 0) {
view.animate().translationY(0);
}
final ViewPropertyAnimator animation = view.animate();
// 添加进容器
mMoveAnimations.add(holder);
animation.setDuration(getMoveDuration()).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animator) {
// 动画开始回调
dispatchMoveStarting(holder);
}
// ...
@Override
public void onAnimationEnd(Animator animator) {
// 和 animateRemoveImpl 一样 就不重复说明了
animation.setListener(null);
dispatchMoveFinished(holder);
mMoveAnimations.remove(holder);
dispatchFinishedWhenDone();
}
}).start(); // 执行动画
}
DefaultItemAnimator 的代码也不难理解,这里仅仅贴出了重要部分代码进行解读,其余代码的阅读难度也不大,就不再细说了,对于自定义 ItemAnimator 仿照 DefaultItemAnimator 的逻辑实现即可。