RecyclerView源码解析 (一):RecyclerView之View的工作流程

RecyclerView源码解析 (一):RecyclerView之View的工作流程

一只猫在东京的公交车站等待,动漫卡通

导言

在Android中如果想要实现一个可滑动的列表,你会怎么做呢?对于我来说,我接触到的第一个列表类的控件就是ListView,其次就是RecylerView。目前来说,ListView已经很少用了,作为JetPack库中一部分,RecylerView显然是主流。可是对于我来说,我只知道实现RecyclerView的最简单的使用,而不了解其原理,在想要实现一些进阶效果时就有心而无力。

本文是源码解析RecyclerView的第一篇文章,主要介绍的是一些前置概念和RecyclerView的视图的工作流程,即onMeasure,onLayout,onDraw三个过程。

术语表

首先我们来看看源码中最开始提供的注释,主要是一张术语表:

  • Adapter(适配器):RecyclerView.Adapter的子类,用来提供视图所需要显示的数据。
  • Position(位置): 数据项在Adapter中的位置。
  • Binding: 准备子视图以显示与其在适配器中相对应的数据的过程。
  • Recycle(View): 之前用于显示特定适配器位置的视图,可能被放入缓存中以供稍后再次使用,以显示相同类型的数据。这可以通过跳过初始布局膨胀(inflation)或构建来大幅提高性能。
  • Scrap(View):在布局期间进入了临时分离状态的子视图。Scrap 视图可以在不完全分离父 RecyclerView 的情况下被重复使用,这些视图可以不经修改直接重用,如果需要重新绑定,则可以由适配器(Adapter)进行修改,前提是视图被视为脏视图(dirty)。
  • Scrapped视图:“Scrapped” 视图是指仍然附加到其父 RecyclerView 的视图,但已经被标记为待删除或重用的状态。
  • Dirty(View): 指的是一个子视图,在被显示之前必须由适配器重新绑定的视图。这通常表示子视图的数据或状态已经发生了变化,需要通过适配器重新绑定以反映最新的数据或状态。

这些术语在一定程度上说明了RecycleView的特点,主要是通过一层抽象把视图项和数据项进行分离,以此来达到高效显示和动态显示的效果。

和RecyclerView配合使用的一些API

除了介绍了RecyclerView的基本信息之外,注释中还介绍了一些和RecyclerView一起使用的一些API,他们分别适用于不同的使用场景。

更新列表项

使用RecyclerView中永恒的主题应该就是关于列表项的更新了,大部分的初级使用者可能只会使用notifyDataSetChanged方法来更新整个RecyclerView。但是实际上,Google官方提供了许多工具来帮助我们正确地更新列表项。关于更新列表项主要提供了四个工具

  • DiffUtil:计算两组数据项最小差值的工具,它可以帮助我们计算出前后数据项的最小更改集,这样每次刷新RecyclerView时就不必调用notifyDataSetChanged方法来触发整个重绘,只需要触发局部刷新即可。
  • ListAdapter:推荐的方法,是一种适配器,可以用最少的代码实现局部刷新。
  • AsyncListDiffer:和ListAdapter的行为类似,但是我们不需要实现ListAdapter的子类。
  • SortedList:用于列表项插入和删除的List,可以自动发送更新信号,这个信号可以被RecycleView监听到。

分页加载

分页加载主要就是一个Paging库,使用这个库可以帮助我们实现分页加载的效果。

源码解析

部分成员类

首先我们来看源码中的一些成员变量的类型:

  • Recycler类(回收池):Recycler 负责管理被回收或分离的项目视图,以便后续重用。通常情况下,RecyclerView.LayoutManager 使用 Recycler 来获取适配器的数据集中表示给定位置或项目 ID 的视图。如果要重用的视图被视为 “dirty”(需要重新绑定),则适配器将被要求重新绑定它。如果不需要重新绑定,LayoutManager 可以立即重用该视图,无需进一步处理。未请求布局的干净视图可能会被 LayoutManager 重新定位,而无需重新测量。
  • SavedState类:用于保存状态。
  • AdapterHelper类:一个辅助类,用于排队和处理适配器更新操作。为了支持动画效果,RecyclerView 在布局的过程中向 Adapter 提供一个旧版本,以最好地表示布局的先前状态。AdapterHelper 为每个适配器数据更改创建一个 UpdateOp,然后对它们进行预处理。在预处理过程中,AdapterHelper 确定哪些 UpdateOp 可以推迟到第二次布局传递,哪些不能。对于无法推迟的 UpdateOp,AdapterHelper 将根据先前推迟的操作更改它们,并在第一次布局传递之前将它们分派出去。它还负责更新推迟的 UpdateOp,因为此过程改变了操作的顺序。尽管操作可能以不同的顺序转发给 LayoutManager,但最终的数据集保证是一致的。这确保了布局和数据的一致性,从而实现了适配器更新操作的正确处理。
  • ChildHelper类:也是一个辅助类,用于管理子项视图。它包装了一个 RecyclerView,并添加了隐藏某些子项的功能。这个类提供了两组方法。常规方法是复制 ViewGroup 方法的方法,比如 getChildAt、getChildCount 等。这些方法会忽略已隐藏的子项。当 RecyclerView 需要直接访问视图组的子项时,它可以调用未经筛选的方法,如 getUnfilteredChildCount 或 getUnfilteredChildAt。这些方法不会忽略隐藏的子项,可以让 RecyclerView 直接访问所有子项,包括已隐藏的子项。
  • ViewInfoStore类:保存关于用于动画的视图的数据。
  • RecyclerListener类:可以在RecyclerView上设置一个RecyclerListener,以便在视图被回收时接收消息。
  • ItemDecorations类:允许应用程序为适配器数据集中的特定项目视图添加特殊的绘制和布局偏移。这对于在项目之间绘制分隔线、突出显示、视觉分组边界等非常有用。所有的ItemDecorations 都按它们被添加的顺序进行绘制。它们会在绘制项目视图之前(在 onDraw() 中)和在项目之上(在 onDrawOver(Canvas, RecyclerView, RecyclerView.State) 中)进行绘制。
  • ItemAnimator类:定义了在对适配器进行更改时项目上发生的动画。ItemAnimator 的子类可以用于为 ViewHolder 项的操作实现自定义动画。
  • RecyclerViewAccessibilityDelegate类:处理基本的辅助功能操作并将它们委托给布局管理器(LayoutManager)。AccessibilityDelegate 用于增强 RecyclerView 的辅助功能支持,以使其更易于访问和使用,特别是对于有特殊需求的用户。此类通常用于处理与辅助功能相关的事件和行为,以确保 RecyclerView 中的项目可以被正确地识别和交互。
  • ViewHolder类:用来描述 RecyclerView 中的一个条目视图(item view)以及有关其在 RecyclerView 中位置的元数据的类。在 RecyclerView.Adapter 中,通常会创建 ViewHolder 的子类,并在其中添加字段来缓存可能昂贵的 View.findViewById(int) 结果。需要注意的是,RecyclerView.LayoutParams 属于 RecyclerView.LayoutManager,而 ViewHolder 属于适配器(Adapter)。适配器可以自由使用自定义的 ViewHolder 实现来存储使绑定视图内容更容易的数据。实现时应假设每个条目视图会对 ViewHolder 对象保持强引用,并且 RecyclerView 实例可能会对屏幕外的额外条目视图保持强引用,以便进行缓存。这样有助于提高 RecyclerView 的性能。

视图绘制的三大流程

Measure流程

接下来就需要分析的就是RecyclerView视图从测量,放置到绘制的这三大流程了,首先查看它的测量流程:

protected void onMeasure(int widthSpec, int heightSpec) {
    if (mLayout == null) {
        defaultOnMeasure(widthSpec, heightSpec);
        return;
    }
    ......
}

当我们没有设置LayoutManager时,就会调用defaultOnMeasure来测量尺寸,这个方法将会帮助我们测量出视图最小需要多大的尺寸然后设置尺寸,这个方法稍微看一看就好:

void defaultOnMeasure(int widthSpec, int heightSpec) {
    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后就且允许自动测量标志位为true时就会调用该LayoutManager的onMeasure方法来测量尺寸:

protected void onMeasure(int widthSpec, int heightSpec) {
	......
    if (mLayout.isAutoMeasureEnabled()) {
        final int widthMode = MeasureSpec.getMode(widthSpec);
        final int heightMode = MeasureSpec.getMode(heightSpec);

        /**
         * This specific call should be considered deprecated and replaced with
         * {@link #defaultOnMeasure(int, int)}. It can't actually be replaced as it could
         * break existing third party code but all documentation directs developers to not
         * override {@link LayoutManager#onMeasure(int, int)} when
         * {@link LayoutManager#isAutoMeasureEnabled()} returns true.
         */
        mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);

        final boolean measureSpecModeIsExactly =
                widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
        if (measureSpecModeIsExactly || mAdapter == null) {
            return;
        }
	......
    }
}

这个自动测量模式是RecyclerView中的一种测量机制,它简化了布局的测量过程,包括根据子项的尺寸和位置计算所需的尺寸。它还支持 RecyclerView 的所有现有动画功能。我们常用的LinearLayoutManager就启用了这个自动测量模式。

可以看到在这里有一段注释,大概内容是:

  1. 方法弃用: 此注释指出,onMeasure(int, int) 方法在特定情况下应被视为已弃用。虽然该方法仍然可用,但它被认为是过时的。
  2. 替代方法: 开发者应该使用 defaultOnMeasure(int, int) 方法来代替 onMeasure(int, int) 方法。这是因为布局管理器的自动测量(AutoMeasure)被启用时,onMeasure(int, int) 方法的行为可能不稳定,因此建议使用默认方法。
  3. 兼容性考虑: 尽管建议不要覆盖 onMeasure(int, int) 方法,但也提到了一些现有的第三方代码可能仍然在使用该方法。因此,即使建议使用新方法,但不建议删除或更改现有代码中的 onMeasure(int, int) 方法,以兼容旧版本的代码。

所以说,如果启用了这个自动测量模式的话,就不应该再实现LayoutManager的onMeasure方法了。

继续往下走,就会触发mLayoutManager的onMeasure,但是由于启动了自动测量就不应该再重写mLayoutManager的onMeasure方法,所以就会触发RecyclerView的defaultOnMeasure方法,和一开始没有设置mLayoutManager时是一样地测量。如果宽和高的测量模式都是EXACTLY模式或者没有设置适配器的话就会退出测量。

protected void onMeasure(int widthSpec, int heightSpec) {
   .......
    }
    if (mLayout.isAutoMeasureEnabled()) {
	......
        if (mState.mLayoutStep == State.STEP_START) {
            dispatchLayoutStep1();
        }
        .....
        dispatchLayoutStep2();
        // now we can get the width and height from the children.
        mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
    } else {
        if (mHasFixedSize) {
                mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
                return;
            }
       ........
    }
}

如果其中有一个测量模式不是EXACTLY模式的话,接下来就会开始进一步的测量。首先将自身状态设置为START状态,然后开始进行分发Layout过程,进入分发的第一步进入dispatchLayoutStep1方法中。完成dispatchLayoutStep1之后就会接着进行dispatchLayoutStep2的方法中。接着step2也被完成后就会调用setMeasureDimensionFromChildren方法,这个方法的作用是计算子项的边界框,然后根据这个边界框来设置 RecyclerView 的测量尺寸。如果不启动自动测量的话就会进入到else块中进行自定义测量。不过官方的LayoutManager都是启用的自动测量,所以我们就不关注else块中了。

Layout流程

dispatchLayoutStep1

首先这个方法有一段简介,大致内容就是这个方法主要进行了这几个步骤:处理适配器的更新,决定需要运行哪些动画,存储当前视图的信息,如果有必要的话将会进行布局预测。来看这个方法:

private void dispatchLayoutStep1() {
	.......
	if (mState.mRunSimpleAnimations) {
        // Step 0: Find out where all non-removed items are, pre-layout
        int count = mChildHelper.getChildCount();
        for (int i = 0; i < count; ++i) {
            final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
            if (holder.shouldIgnore() || (holder.isInvalid() && !mAdapter.hasStableIds())) {
                continue;
            }
            final ItemHolderInfo animationInfo = mItemAnimator
                    .recordPreLayoutInformation(mState, holder,
                            ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
                            holder.getUnmodifiedPayloads());
            mViewInfoStore.addToPreLayout(holder, animationInfo);
            if (mState.mTrackOldChangeHolders && holder.isUpdated() && !holder.isRemoved()
                    && !holder.shouldIgnore() && !holder.isInvalid()) {
                long key = getChangedHolderKey(holder);
                // This is NOT the only place where a ViewHolder is added to old change holders
                // list. There is another case where:
                //    * A VH is currently hidden but not deleted
                //    * The hidden item is changed in the adapter
                //    * Layout manager decides to layout the item in the pre-Layout pass (step1)
                // When this case is detected, RV will un-hide that view and add to the old
                // change holders list.
                mViewInfoStore.addToOldChangeHolders(key, holder);
            }
        }
    }
   .........
}

首先来看这个方法的第一个分支,如果需要进行简单动画的话,那么就会进入到这个分支里去。首先获取当前列表中的列表项数目,然后依次获取对应的ViewHolder。如果这个ViewHolder被设置为可忽略的 或者 holder失效且其没有稳定的Id的话 就会跳过这次循环。这里出现了StableIds(稳定的Id),这个Id主要用于支持有关项目的动画效果以及在数据集更改时正确更新项目的位置。如果适配器的数据集中的项目具有稳定的 ID,RecyclerView 将能够更好地管理动画和视图项的位置。

如果还没有退出本次循环的话就会进行到下一步,也就是记录预布局的相关信息(recordPreLayoutInformation),然后将其视图信息的存储库中mViewInfoStore.addToPreLayout(holder, animationInfo)。接下来如果目前的这个ViewHolder被更新了且仍然有效且需要追踪旧的ViewHolder(mState.mTrackOldChangeHolders为true)的话那么就会将老的ViewHolder的信息也加入到ViewInfoStore中。这样第一个分支就结束了,接下来再看第二个分支:

private void dispatchLayoutStep1() {
    ......
    if (mState.mRunPredictiveAnimations) {
        // Step 1: run prelayout: This will use the old positions of items. The layout manager
        // is expected to layout everything, even removed items (though not to add removed
        // items back to the container). This gives the pre-layout position of APPEARING views
        // which come into existence as part of the real layout.

        // Save old positions so that LayoutManager can run its mapping logic.
        saveOldPositions();
        final boolean didStructureChange = mState.mStructureChanged;
        mState.mStructureChanged = false;
        // temporarily disable flag because we are asking for previous layout
        mLayout.onLayoutChildren(mRecycler, mState);
        mState.mStructureChanged = didStructureChange;
       	.......
        clearOldPositions();
    } else {
        clearOldPositions();
    }
    ......
}

如果需要运行预加载的动画的话就会进入到这个分支,首先会存储旧的Position信息。然后存储旧的结构是否改变的标志位,然后暂时将这个标志位置为false。然后会调用到mLayoutManager的onLayoutChildren方法来布局各个子项。布局完毕之后再讲结构改变标志位置为原来的状态。

private void dispatchLayoutStep1() {
    .....
    if (mState.mRunPredictiveAnimations) {
        .....
        for (int i = 0; i < mChildHelper.getChildCount(); ++i) {
            final View child = mChildHelper.getChildAt(i);
            final ViewHolder viewHolder = getChildViewHolderInt(child);
            if (viewHolder.shouldIgnore()) {
                continue;
            }
            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;
                }
                final ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation(
                        mState, viewHolder, flags, viewHolder.getUnmodifiedPayloads());
                if (wasHidden) {
                    recordAnimationInfoIfBouncedHiddenView(viewHolder, animationInfo);
                } else {
                    mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);
                }
            }
        }
        // we don't process disappearing list because they may re-appear in post layout pass.
        clearOldPositions();
    } else {
        clearOldPositions();
    }
    onExitLayoutOrScroll();
    stopInterceptRequestLayout(false);
    mState.mLayoutStep = State.STEP_LAYOUT;
}

接下来仍然会获取到列表项,然后获得其ViewHolder,当当前ViewHolder不是正在预布局列表中的话就会进入到if块中。这个预布局列表在之前的分支中提到过(mViewInfoStore.addToPreLayout(holder, animationInfo))。在这个语句块中首先会通过ItemAnimator来为当前ViewHolder构建出动画标志位,以确定应该如何处理该项目的动画。然后调用 mItemAnimator.recordPreLayoutInformation() 方法来记录列表项的预布局信息。这个方法将 ViewHolder、动画标志和列表项的未修改负载(payloads)作为参数传递,并返回一个包含动画信息的 ItemHolderInfo 对象。最后,根据列表项是否曾经隐藏过,将动画信息添加到 mViewInfoStore 中的适当位置。如果曾经隐藏过,使用 recordAnimationInfoIfBouncedHiddenView() 方法进行记录,否则,添加到 mViewInfoStore 的 “AppearedInPreLayoutHolders” 中,以便稍后的动画处理。

最后会通过clearOldPosition方法来清除所有ViewHolder项的旧位置信息。我们也可以稍微看一看这个方法:

void clearOldPositions() {
    final int childCount = mChildHelper.getUnfilteredChildCount();
    for (int i = 0; i < childCount; i++) {
        final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
        if (!holder.shouldIgnore()) {
            holder.clearOldPosition();
        }
    }
    mRecycler.clearOldPositions();
}                           

可以看到这个方法就是对RecyclerView中的所有未被忽略的holder进行旧数据的清除,最后再调用回收池的clearOldPositions方法来清除回收池中的旧数据。关于RecyclerView中的回收池等缓存机制我们将在下篇文章介绍。

接着回到dispatchLayoutStep1方法中,最后的三个方法:

onExitLayoutOrScroll();
stopInterceptRequestLayout(false);
mState.mLayoutStep = State.STEP_LAYOUT;

onExitLayoutOrScroll()方法确保在布局或滚动操作完成后执行一些必要的清理和事件分发操作,以维护 RecyclerView 的一致性和正确性。然后调用stopInterceptRequestLayout()方法停止拦截layout过程。然后把当前状态位置为State.STEP_LAYOUT。到这里就完成了dispatchLayoutStep1。

紧接着回到onMeasure方法中我们就可以发现完成了dispatchLayoutStep1之后就是dispatchLayoutStep2方法的执行了。

dispatchLayoutStep2

这个方法也有一段摘要:

The second layout step where we do the actual layout of the views for the final state. This step might be run multiple times if necessary (e.g. measure).

主要就是说这个步骤中RecyclerView 将根据最终的状态,安排和布局所有的视图。这可能包括视图的测量、定位、分配大小和排列等操作,以确保它们正确地显示在 RecyclerView 中。在必要的情况下这个步骤将会多次被执行。

接下来看这个方法:

private void dispatchLayoutStep2() {
	....
    mAdapterHelper.consumeUpdatesInOnePass();
    mState.mItemCount = mAdapter.getItemCount();
    mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;

    // Step 2: Run layout
    mState.mInPreLayout = false;
    mLayout.onLayoutChildren(mRecycler, mState);

    mState.mStructureChanged = false;
    mPendingSavedState = null;

    // onLayoutChildren may have caused client code to disable item animations; re-check
    mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
    mState.mLayoutStep = State.STEP_ANIMATIONS;
	......
}

第一个重要方法是mAdapterHelper.consumeUpdatesInOnePass(),在这个方法中将消费掉,RecyclerView 通过 mAdapterHelper 对象一次性处理所有的更新操作,以确保布局过程中不会受到更新的干扰。消费完之后再更新目前列表项的数目。接着把自身的的InPreLayout标志位设置为false,这个标志位和RecyclerView的布局生命周期有关:

在 RecyclerView 的生命周期中,有两个主要的布局步骤:预布局(pre-layout)和主布局(main layout)

  • 预布局(pre-layout):这是在 RecyclerView 的布局刚开始时执行的步骤,此时 RecyclerView 正在为即将发生的动画准备视图的位置和状态。这通常用于执行预测性的动画,例如,当你添加或移除项目时,RecyclerView 可能会在预布局步骤中预测新项目的位置。如果条件为 true,表示 RecyclerView 正处于这个步骤中。
  • 主布局(main layout):这是正常的布局步骤,RecyclerView 正在布置项目以呈现最终的布局状态。在这个步骤中,RecyclerView 会根据数据集和预测布局的结果来进行真正的布局。如果条件为 false,表示 RecyclerView 不在预布局步骤中,而是在主布局步骤中。

所以说将这个标志位置为false代表这个RecyclerView不运行在了预布局,而是在进行主布局。之后会调用mLayoutManager的onLayoutChildren来布局列表项。最后标记结构未改变,清除保存状态然后标记当前状态为State.STEP_ANIMATIONS。

dispatchLayoutStep3

你可能会好奇dispatchLayoutStep3方法在哪里,它实际上是在dispatchLayout方法中,而dispatchLayout方法是在onLayout方法中:

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() {
	......
    mState.mIsMeasuring = false;
    if (mState.mLayoutStep == State.STEP_START) {
        dispatchLayoutStep1();
        mLayout.setExactMeasureSpecsFrom(this);
        dispatchLayoutStep2();
    } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
            || mLayout.getHeight() != getHeight()) {
        // First 2 steps are done in onMeasure but looks like we have to run again due to
        // changed size.
        mLayout.setExactMeasureSpecsFrom(this);
        dispatchLayoutStep2();
    } else {
        // always make sure we sync them (to ensure mode is exact)
        mLayout.setExactMeasureSpecsFrom(this);
    }
    dispatchLayoutStep3();
}

dispatchLayout 方法是 RecyclerView 布局过程的入口点,它负责执行布局的三个主要步骤,并根据需要重新运行布局以确保布局的正确性。这个方法来说:

  1. 如果有适配器和布局管理器,方法会将布局状态标志 mState.mIsMeasuring 设置为 false,表示不处于测量状态。接着,根据当前布局步骤(mState.mLayoutStep)的状态,执行以下两种情况之一:
    • 如果当前布局步骤是 State.STEP_START,表示布局的起始阶段,那么它将执行 dispatchLayoutStep1(),这是布局的第一步。接着,它会通过 mLayout.setExactMeasureSpecsFrom(this) 设置布局管理器的测量规格(MeasureSpecs),然后执行 dispatchLayoutStep2(),这是布局的第二步。
    • 如果不是第一步,那么它会检查是否需要重新运行布局。这是通过检查适配器是否有更新(mAdapterHelper.hasUpdates())或者 RecyclerView 的尺寸是否发生了变化来完成的。如果有更新或尺寸变化,它会再次通过 mLayout.setExactMeasureSpecsFrom(this) 设置布局管理器的测量规格,然后执行 dispatchLayoutStep2()
  2. 完成布局过程: 最后,无论是第一步还是后续的步骤,都会执行 dispatchLayoutStep3(),这是布局的第三步。这一步用于完成布局过程,包括执行可能的动画和其他必要的布局操作。

所以我们接下来看最后一步方法dispatchLayoutStep3:

private void dispatchLayoutStep3() {
    .......
    mState.mLayoutStep = State.STEP_START;
    if (mState.mRunSimpleAnimations) {
        // Step 3: Find out where things are now, and process change animations.
        // traverse list in reverse because we may call animateChange in the loop which may
        // remove the target view holder.
        for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
            ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
            if (holder.shouldIgnore()) {
                continue;
            }
            long key = getChangedHolderKey(holder);
            final ItemHolderInfo animationInfo = mItemAnimator
                    .recordPostLayoutInformation(mState, holder);
            ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key);
            if (oldChangeViewHolder != null && !oldChangeViewHolder.shouldIgnore()) {
                // run a change animation

                // If an Item is CHANGED but the updated version is disappearing, it creates
                // a conflicting case.
                // Since a view that is marked as disappearing is likely to be going out of
                // bounds, we run a change animation. Both views will be cleaned automatically
                // once their animations finish.
                // On the other hand, if it is the same view holder instance, we run a
                // disappearing animation instead because we are not going to rebind the updated
                // VH unless it is enforced by the layout manager.
                final boolean oldDisappearing = mViewInfoStore.isDisappearing(
                        oldChangeViewHolder);
                final boolean newDisappearing = mViewInfoStore.isDisappearing(holder);
                if (oldDisappearing && oldChangeViewHolder == holder) {
                    // run disappear animation instead of change
                    mViewInfoStore.addToPostLayout(holder, animationInfo);
                } else {
                    final ItemHolderInfo preInfo = mViewInfoStore.popFromPreLayout(
                            oldChangeViewHolder);
                    // we add and remove so that any post info is merged.
                    mViewInfoStore.addToPostLayout(holder, animationInfo);
                    ItemHolderInfo postInfo = mViewInfoStore.popFromPostLayout(holder);
                    if (preInfo == null) {
                        handleMissingPreInfoForChangeError(key, holder, oldChangeViewHolder);
                    } else {
                        animateChange(oldChangeViewHolder, holder, preInfo, postInfo,
                                oldDisappearing, newDisappearing);
                    }
                }
            } else {
                mViewInfoStore.addToPostLayout(holder, animationInfo);
            }
        }

        // Step 4: Process view info lists and trigger animations
        mViewInfoStore.process(mViewInfoProcessCallback);
    }

   .........
}

这一步方法也有摘要,主要就是说这个方法是最后的一步方法,在这个方法里我们将会将视图的动画信息存储下来然后触发这些动画,在必要的情况下还会做一些清理工作。

在这层循环一开始是和dispatchLayoutStep1方法很像的。最开始会把自身的状态设置为start,这样就可以保证接下来再次进行layout时也会触发dispatchLayoutStep1dispatchLayoutStep2方法。然后就是获取到相关联的ViewHolder。接着获取到它的更改Key,再用ItemAnimator将当前信息记录下来。接下来还会额外获取到与当前ViewHolder对应的旧版本的ViewHolder,这主要是为了判断ViewHolder是否发生了变化,根据这个ViewHolder的变化情况接下来会进行不同的处理。实际上上面的大段注释也告诉了我们它是如何处理不同情况的, 根据不同情况处理变化。具体情况如下:

  • 旧版本为消失(DISAPPEARING)且新旧版本一致: 这种情况下会运行disappear动画,动画完成后就会自动释放资源。
  • 其他情况: 对于其他情况,首先从 mViewInfoStore 中弹出前布局信息(preInfo),然后将当前 ViewHolder 添加到后布局信息中。接着,再次从 mViewInfoStore 中弹出后布局信息(postInfo)。然后,通过调用 animateChange 方法处理变化,包括旧版本和新版本的 ViewHolder、前布局信息和后布局信息以及是否被标记为消失的状态。

进行完这一系列处理之后,调用mViewInfoStore.process(mViewInfoProcessCallback)来处理视图信息列表并且触发动画。

在做完这一大段操作之后实际上这个方法的核心逻辑就已经结束了,之后这个方法还会进行一些后续处理,主要是更新各种标志位,表明dispatchLayoutStep过程已经结束了,最后调用mLayoutManager的onLayoutCompleted方法做一些清理工作。

Draw流程

最后就是Draw流程了,来到onDraw流程也是最简单的一个流程:

public void onDraw(Canvas c) {
    super.onDraw(c);

    final int count = mItemDecorations.size();
    for (int i = 0; i < count; i++) {
        mItemDecorations.get(i).onDraw(c, this, mState);
    }
}

ess(mViewInfoProcessCallback)`来处理视图信息列表并且触发动画。

在做完这一大段操作之后实际上这个方法的核心逻辑就已经结束了,之后这个方法还会进行一些后续处理,主要是更新各种标志位,表明dispatchLayoutStep过程已经结束了,最后调用mLayoutManager的onLayoutCompleted方法做一些清理工作。

Draw流程

最后就是Draw流程了,来到onDraw流程也是最简单的一个流程:

public void onDraw(Canvas c) {
    super.onDraw(c);

    final int count = mItemDecorations.size();
    for (int i = 0; i < count; i++) {
        mItemDecorations.get(i).onDraw(c, this, mState);
    }
}

很简单,调用到了超类的onDraw,也就是ViewGroup的onDraw方法,它会将onDraw来分发给各个子View进行绘制。然后会根据ItemDecorations中的信息来绘制一些列表项的额外装饰,比如说分割线等。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
当出现RecyclerView溢出异常(uncaught exception: inconsistency detected. invalid view holder adapter position)时,通常是由于RecyclerView的数据源与视图不同步引起的。 通过检查以下几个方面,我们可以解决这个异常: 1. 数据源的变化:确保数据源在进行任何更改后,通过调用适配器的notifyDataSetChanged()或相关的notifyItemInserted()、notifyItemRemoved()等方法进行刷新。这将通知RecyclerView的数据源已更改,需要重新绘制视图。 2. ViewHolder的复用:RecyclerView使用ViewHolder来缓存已创建的视图,以便在滚动时能够快速重用。确保你正确地在Adapter中使用 onCreateViewHolder() 和 onBindViewHolder() 方法来创建和绑定ViewHolder,避免出现不同的视图或数据绑定在同一个ViewHolder上。 3. 数据源和视图的位置:检查重新绘制视图时,数据源和视图的位置是否一致。确保你在 onCreateViewHolder() 和 onBindViewHolder() 方法中正确地使用传入的position参数,以获取正确的数据并将其绑定到相应的视图。 如果上述方法不能解决问题,你可以尝试以下措施: 1. 删除RecyclerView中所有的数据源,然后重新加载数据。 2. 检查RecyclerView是否与其他组件发生冲突,例如布局、滚动监听器等。你可以尝试删除一些可能引起冲突的组件,逐步排除问题。 3. 检查RecyclerView的布局参数是否正确设置,确保它在容器中正确显示。 最后,如果以上方法仍然不能解决问题,可能涉及到其他更深层次的问题。你可以尝试查阅相关的技术文档、调试或寻求优秀的开发者社区的帮助来解决该问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值