美文网首页recycleviewRecycleView
Android - RecyclerView进阶(3)—Item

Android - RecyclerView进阶(3)—Item

作者: 东方未曦 | 来源:发表于2020-03-26 00:30 被阅读0次

    我的CSDN: ListerCi
    我的简书: 东方未曦

    俗话说,好看的皮囊千篇一律,有趣的灵魂万里挑一。但是对于我们这些俗人来说,肯定是选择好看的皮囊,咱们的用户也是如此。你看看应用市场上那些花枝招展的APP,哪个不是用上了五花八门的动画效果,就算你的内在安全省电性能好,没点儿花招可留不住花心的用户。所以我们今天就来看看怎么实现让用户眼前一亮的动画,当然原理也很重要,因此源码分析必不可少,本文的源码分析主要聚焦于动画是怎么触发的,以及动画是怎么实现的。

    一、动画的触发与实现

    当Adapter中的数据发生变化时,我们通过notifyItemXXX()等方法通知RecyclerView来改变数据的展示,这个过程必然伴随新的layout()。如果在layout()后直接显示新数据,效果比较僵硬,因此需要通过动画来制造良好的用户体验。
    那么,为了实现动画,RecyclerView又额外做了哪些工作呢?抽象上来讲,RecyclerView实现动画的步骤如下。
    ① 数据发生改变时,保存当前的item信息为preInfo
    ② 根据新的数据Layout
    ③ Layout完毕,保存当前的item信息为postInfo
    ④ 根据preInfo和postInfo判断动画类型并交给ItemAnimator执行
    可以发现,前3步保存了执行动画所需要的信息,最后整体交给ItemAnimator来执行动画。前3步涉及到内容较为复杂,我们先从简单的开始分析,来看ItemAnimator是怎么实现动画的。

    1.1 动画的实现

    由于RecyclerView设计时的低耦合性,ItemAnimator只需要关注怎么执行动画即可,其逻辑并不复杂。RecyclerView为我们实现了DefaultItemAnimator,在不设置动画的情况下默认使用它,其中实现了4个针对item的动画,分别为Remove、Move、Add和Change。以Remove动画为例,当一个item被移出RecyclerView时,DefaultItemAnimator中的animateRemove(holder)方法就会被调用,但是并没有马上开始执行动画,而是将动画添加到了mPendingRemovals中,这是一个待执行的Romove动画List。

        @Override
        public boolean animateRemove(final RecyclerView.ViewHolder holder) {
            resetAnimation(holder);
            mPendingRemovals.add(holder);
            return true;
        }
    

    看一下DefaultItemAnimator的成员变量,原来有4个List分别存储4种动画。

    private ArrayList<RecyclerView.ViewHolder> mPendingRemovals = new ArrayList<>();
    private ArrayList<RecyclerView.ViewHolder> mPendingAdditions = new ArrayList<>();
    private ArrayList<MoveInfo> mPendingMoves = new ArrayList<>();
    private ArrayList<ChangeInfo> mPendingChanges = new ArrayList<>();
    

    当RecyclerView要执行动画时,ItemAnimator的runPendingAnimations()方法会被调用,DefaultItemAnimator重写后的方法如下,为了便于阅读,添加了注释并省略了部分代码。

        @Override
        public void runPendingAnimations() {
            boolean removalsPending = !mPendingRemovals.isEmpty();
            boolean movesPending = !mPendingMoves.isEmpty();
            boolean changesPending = !mPendingChanges.isEmpty();
            boolean additionsPending = !mPendingAdditions.isEmpty();
            // 判断是否有动画需要执行
            if (!removalsPending && !movesPending && !additionsPending && !changesPending) {
                return;
            }
            // 执行Remove动画
            for (RecyclerView.ViewHolder holder : mPendingRemovals) {
                animateRemoveImpl(holder); // 实际执行Remove动画的方法
            }
            mPendingRemovals.clear();
            // Move动画
            if (movesPending) {
                // 注意: Move动画并不是马上执行,会放入一个Runnable
                Runnable mover = new Runnable() {
                    @Override
                    public void run() {
                        for (MoveInfo moveInfo : moves) {
                            animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY,
                                    moveInfo.toX, moveInfo.toY);
                        }
                    }
                };
                // 如果有Remove动画,就在Remove动画结束之后执行Move动画
                // 如果没有Remove动画就马上执行
                if (removalsPending) {
                    View view = moves.get(0).holder.itemView;
                    ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration());
                } else {
                    mover.run();
                }
            }
            // Change动画,与Move动画一起执行
            if (changesPending) {
                // ......
                Runnable changer = new Runnable() {
                    @Override
                    public void run() {
                        for (ChangeInfo change : changes) {
                            animateChangeImpl(change);
                        }
                    }
                };
                if (removalsPending) {
                    RecyclerView.ViewHolder holder = changes.get(0).oldHolder;
                    ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration());
                } else {
                    changer.run();
                }
            }
            // 最后执行Add动画
            if (additionsPending) {
                // ......
                Runnable adder = new Runnable() {
                    @Override
                    public void run() {
                        for (RecyclerView.ViewHolder holder : additions) {
                            animateAddImpl(holder);
                        }
                        additions.clear();
                        mAdditionsList.remove(additions);
                    }
                };
                // 在Remove、Move、Change动画都完成之后开始执行Add动画
                if (removalsPending || movesPending || changesPending) {
                    long removeDuration = removalsPending ? getRemoveDuration() : 0;
                    long moveDuration = movesPending ? getMoveDuration() : 0;
                    long changeDuration = changesPending ? getChangeDuration() : 0;
                    long totalDelay = removeDuration + Math.max(moveDuration, changeDuration);
                    View view = additions.get(0).itemView;
                    ViewCompat.postOnAnimationDelayed(view, adder, totalDelay);
                } else {
                    adder.run();
                }
            }
        }
    

    我们发现动画的执行是有顺序的,Remove动画首先执行,之后Move和Change动画同时开始,等这3个动画全部结束之后开始执行Add动画。以RecyclerView删除一个item为例,动画如下。

    gif-Remove动画.gif

    我们发现动画有2段,首先是被删除item的Remove动画,等到完全不可见之后,下方的多个item同时执行向上的Move动画。相对的,如果向RecyclerView中添加一个item,会先执行item向下的Move动画,再执行插入item的Add动画。

    runPendingAnimations()中真正执行动画的是animateRemoveImpl()这样的方法,来看一下它是怎么实现的,这也是我们今后自定义动画的关键。

        private void animateRemoveImpl(final RecyclerView.ViewHolder holder) {
            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) {
                            dispatchRemoveStarting(holder);
                        }
    
                        @Override
                        public void onAnimationEnd(Animator animator) {
                            animation.setListener(null);
                            view.setAlpha(1);
                            dispatchRemoveFinished(holder);
                            mRemoveAnimations.remove(holder);
                            dispatchFinishedWhenDone();
                        }
                    }).start();
        }
    

    Remove动画很简单,通过ViewPropertyAnimator实现一个透明度到0的属性动画,不过要记得在动画开始和结束时调用dispatchRemoveStarting()dispatchRemoveFinished()方法。相对的,Add动画就是透明度从0到1的属性动画,而Remove动画就是修改itemView的translation进行移动。
    以上3个动画都只涉及1个item,而Change动画会涉及到2个item,DefaultItemAnimator中的实现是让原来的item执行透明度从1到0的动画,让新item执行透明度从0到1的动画。

        void animateChangeImpl(final ChangeInfo changeInfo) {
            final RecyclerView.ViewHolder holder = changeInfo.oldHolder;
            final View view = holder == null ? null : holder.itemView;
            final RecyclerView.ViewHolder newHolder = changeInfo.newHolder;
            final View newView = newHolder != null ? newHolder.itemView : null;
            if (view != null) {
                final ViewPropertyAnimator oldViewAnim = view.animate().setDuration(
                        getChangeDuration());
                mChangeAnimations.add(changeInfo.oldHolder);
                oldViewAnim.alpha(0).setListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationStart(Animator animator) {
                        dispatchChangeStarting(changeInfo.oldHolder, true);
                    }
    
                    @Override
                    public void onAnimationEnd(Animator animator) {
                        oldViewAnim.setListener(null);
                        view.setAlpha(1);
                        dispatchChangeFinished(changeInfo.oldHolder, true);
                        mChangeAnimations.remove(changeInfo.oldHolder);
                        dispatchFinishedWhenDone();
                    }
                }).start();
            }
            if (newView != null) {
                final ViewPropertyAnimator newViewAnimation = newView.animate();
                mChangeAnimations.add(changeInfo.newHolder);
                newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration())
                        .alpha(1).setListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationStart(Animator animator) {
                        dispatchChangeStarting(changeInfo.newHolder, false);
                    }
                    @Override
                    public void onAnimationEnd(Animator animator) {
                        newViewAnimation.setListener(null);
                        newView.setAlpha(1);
                        dispatchChangeFinished(changeInfo.newHolder, false);
                        mChangeAnimations.remove(changeInfo.newHolder);
                        dispatchFinishedWhenDone();
                    }
                }).start();
            }
        }
    

    以上就是DefaultItemAnimator的基本逻辑,它主要做了两件事:
    ① 统一安排动画的执行顺序
    ② 对4种item动画具体实现
    自定义ItemAnimator时,我们可以直接使用DefaultItemAnimator的runPendingAnimations()方法来安排动画的执行顺序,只需要修改item的动画即可。

    1.2 动画的触发

    接下来我们来看动画的触发流程,其关键就在于RecyclerView在执行动画前已经计算出了每个item在动画前后的位置等信息,随后将这些信息传给ItemAnimator统一执行。现在我们从notifyItemRemoved(int position)开始分析动画触发的流程。

        /**
         * 通知注册的观察者,position上的item从数据集中移除了
         * 之前在oldPosition上的item可能会出现在oldPosition - 1的位置上
         */
        public final void notifyItemRemoved(int position) {
            mObservable.notifyItemRangeRemoved(position, 1);
        }
    

    mObservable是一个AdapterDataObservable对象,是RecyclerView数据集的被观察者。mObservable会通知所有的Observer数据发生了改变,RecyclerView有个默认的观察者RecyclerViewDataObserver,来看一下在item被Remove后它做了什么。

        @Override
        public void onItemRangeRemoved(int positionStart, int itemCount) {
            assertNotInLayoutOrScroll(null);
                if (mAdapterHelper.onItemRangeRemoved(positionStart, itemCount)) {
                    triggerUpdateProcessor();
                }
        }
    

    直接执行了triggerUpdateProcessor()方法。

        void triggerUpdateProcessor() {
            if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
                ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
            } else {
                mAdapterUpdateDuringMeasure = true;
                requestLayout();
            }
        }
    

    这里有3个判断条件,POST_UPDATES_ON_ANIMATION在SDK>=16时为true;mHasFixedSize默认为false,为true时有优化性能的作用,可以减少requestLayout()方法的调用,这里先不展开,之后性能优化会提到它。
    当mHasFixedSize为false时进入else代码块,执行requestLayout()方法,最终进入RecyclerView的onLayout()方法。(PS: mHasFixedSize为true时最终也会执行到onLayout()方法)

        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
            dispatchLayout();
            TraceCompat.endSection();
            mFirstLayoutComplete = true;
        }
    

    onLayout()中调用了dispatchLayout()方法,在看它的代码前不妨先看下方法介绍。介绍里第一句就表示,这个方法用于处理由Layout产生的动画,并且将动画分为了5种类型。很显然这个方法就是今天的主角,下面来重点分析。

        /**
         * Wrapper around layoutChildren() that handles animating changes caused by layout.
         * Animations work on the assumption that there are five different kinds of items
         * in play:
         * PERSISTENT: items are visible before and after layout
         * REMOVED: items were visible before layout and were removed by the app
         * ADDED: items did not exist before layout and were added by the app
         * DISAPPEARING: items exist in the data set before/after, but changed from
         * visible to non-visible in the process of layout (they were moved off
         * screen as a side-effect of other changes)
         * APPEARING: items exist in the data set before/after, but changed from
         * non-visible to visible in the process of layout (they were moved on
         * screen as a side-effect of other changes)
         */
    

    dispatchLayout()代码如下,它依次调用dispatchLayoutStep1()dispatchLayoutStep2()dispatchLayoutStep3(),我们来逐个分析。

        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();
        }
    
    dispatchLayoutStep1()

    先来讲dispatchLayoutStep1(),它的主要工作有:处理Adapter的数据更新、决定应该运行哪种动画、记录当前所有ItemView的信息、进行预布局pre-layout并保存其信息。简化后的代码如下,只保留了动画相关的部分。

        private void dispatchLayoutStep1() {
            // ......
            if (mState.mRunSimpleAnimations) {
                // 找到所有没有被Remove的Item
                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());
                    // 将Item信息保存到mViewInfoStore
                    mViewInfoStore.addToPreLayout(holder, animationInfo);
                    if (mState.mTrackOldChangeHolders && holder.isUpdated() && !holder.isRemoved()
                            && !holder.shouldIgnore() && !holder.isInvalid()) {
                        // ......
                    }
                }
            }
            if (mState.mRunPredictiveAnimations) {
                // 开始pre-layout,此时使用的是oldPositions
                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;
    
                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)) {
                        // 这里保存原本在屏幕外的Item的信息,代码省略......
                    }
                }
                // 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;
        }
    

    首先找到没有被Remove的Item并保存信息,保存时调用的是mViewInfoStore.addToPreLayout(),可以理解为保存的是执行动画前的信息。
    如果mState.mRunPredictiveAnimations为true就开始执行pre-layout,pre-layout使用的是Item的oldPosition,它会对所有的Item(包括被Remove的Item)进行布局,并且为动画后显示在屏幕上的Item提供位置。什么意思呢?如果当前某个Item会Remove,原本屏幕外的Item就可能Move到屏幕上,这个Item的信息也需要被记录,pre-layout就是为这类Item提供了显示动画的能力。

    来看下mViewInfoStore.addToPreLayout(holder, animationInfo)做了什么。可以发现它通过键值对<ViewHolder, InfoRecord>保存Item的信息,并且由于当前保存的是动画前的Item信息,为InfoRecord添加FLAG_PRE标识。

        void addToPreLayout(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) {
            InfoRecord record = mLayoutHolderMap.get(holder);
            if (record == null) {
                record = InfoRecord.obtain();
                mLayoutHolderMap.put(holder, record);
            }
            record.preInfo = info;
            record.flags |= FLAG_PRE;
        }
    

    InfoRecord的数据结构如下,在dispatchLayoutStep1()中保存的是preInfo

        static class InfoRecord {
            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;
            int flags;
            @Nullable
            RecyclerView.ItemAnimator.ItemHolderInfo preInfo;
            @Nullable
            RecyclerView.ItemAnimator.ItemHolderInfo postInfo;
        }
    
    dispatchLayoutStep2()

    再来看dispatchLayoutStep2(),它主要通过mLayout.onLayoutChildren(mRecycler, mState)对新的数据进行了布局,随后对是否支持动画进行检查并赋值。

    private void dispatchLayoutStep2() {
            // ......
            // Step 2: Run layout
            mState.mInPreLayout = false;
            mLayout.onLayoutChildren(mRecycler, mState);
    
            mState.mStructureChanged = false;
            mPendingSavedState = null;
    
            mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
            mState.mLayoutStep = State.STEP_ANIMATIONS;
            onExitLayoutOrScroll();
            stopInterceptRequestLayout(false);
        }
    
    dispatchLayoutStep3()

    dispatchLayoutStep3()对Change动画进行了特殊处理,如果是Change动画会直接执行。对于其余动画来说,会先记录动画后的Item信息,记录完毕后触发动画。

        private void dispatchLayoutStep3() {
            // 初始化...
            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()) {
                        // 执行CHANGE动画
                        final boolean oldDisappearing = mViewInfoStore.isDisappearing(
                                oldChangeViewHolder);
                        final boolean newDisappearing = mViewInfoStore.isDisappearing(holder);
                        if (oldDisappearing && oldChangeViewHolder == holder) {
                            // 如果1个Item CHANGED,但是更新后会消失,则执行disappear动画
                            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 {
                        // 将Layout后的信息保存到mViewInfoStore中
                        mViewInfoStore.addToPostLayout(holder, animationInfo);
                    }
                }
                // 触发动画
                mViewInfoStore.process(mViewInfoProcessCallback);
            }
            // clean up...
        }
    

    这里通过mViewInfoStore.addToPostLayout(...)将Layout后的信息保存,再执行mViewInfoStore.process(mViewInfoProcessCallback)触发动画,主要逻辑为根据之前保存的item信息执行对应的回调。

        void process(ProcessCallback callback) {
            for (int index = mLayoutHolderMap.size() - 1; index >= 0; index--) {
                final RecyclerView.ViewHolder viewHolder = mLayoutHolderMap.keyAt(index);
                final InfoRecord record = mLayoutHolderMap.removeAt(index);
                if ((record.flags & FLAG_APPEAR_AND_DISAPPEAR) == FLAG_APPEAR_AND_DISAPPEAR) {
                    // Appeared then disappeared. Not useful for animations.
                    callback.unused(viewHolder);
                } else if ((record.flags & FLAG_DISAPPEARED) != 0) {
                    // 被LayoutManager设置为消失
                    if (record.preInfo == null) {
                        callback.unused(viewHolder);
                    } else {
                        callback.processDisappeared(viewHolder, record.preInfo, record.postInfo);
                    }
                } else if ((record.flags & FLAG_APPEAR_PRE_AND_POST) == FLAG_APPEAR_PRE_AND_POST) {
                    // Appeared in the layout but not in the adapter (e.g. entered the viewport)
                    callback.processAppeared(viewHolder, record.preInfo, record.postInfo);
                } else if ((record.flags & FLAG_PRE_AND_POST) == FLAG_PRE_AND_POST) {
                    // preLayout和postLayout都在,执行callback.processPersistent
                    callback.processPersistent(viewHolder, record.preInfo, record.postInfo);
                } else if ((record.flags & FLAG_PRE) != 0) {
                    // 在pre-layout中,但是不在post-layout中,因此item消失了
                    callback.processDisappeared(viewHolder, record.preInfo, null);
                } else if ((record.flags & FLAG_POST) != 0) {
                    // 不在pre-layout,出现在了post-layout
                    callback.processAppeared(viewHolder, record.preInfo, record.postInfo);
                } else if ((record.flags & FLAG_APPEAR) != 0) {
                    // Scrap view. RecyclerView will handle removing/recycling this.
                }
                InfoRecord.recycle(record);
            }
        }
    

    回调mViewInfoProcessCallback如下所示,基本就是执行对应的方法。

        private final ViewInfoStore.ProcessCallback mViewInfoProcessCallback =
                new ViewInfoStore.ProcessCallback() {
                    @Override
                    public void processDisappeared(ViewHolder viewHolder, @NonNull ItemHolderInfo info, @Nullable ItemHolderInfo postInfo) {
                        mRecycler.unscrapView(viewHolder);
                        animateDisappearance(viewHolder, info, postInfo);
                    }
    
                    @Override
                    public void processAppeared(ViewHolder viewHolder, ItemHolderInfo preInfo, ItemHolderInfo info) {
                        animateAppearance(viewHolder, preInfo, info);
                    }
    
                    @Override
                    public void processPersistent(ViewHolder viewHolder, @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
                        viewHolder.setIsRecyclable(false);
                        if (mDataSetHasChangedAfterLayout) {
                            if (mItemAnimator.animateChange(viewHolder, viewHolder, preInfo, postInfo)) {
                                postAnimationRunner();
                            }
                        } else if (mItemAnimator.animatePersistence(viewHolder, preInfo, postInfo)) {
                            postAnimationRunner();
                        }
                    }
    
                    @Override
                    public void unused(ViewHolder viewHolder) {
                        mLayout.removeAndRecycleView(viewHolder.itemView, mRecycler);
                    }
                };
    

    我们以animateDisappearance(...)为例来分析,如果mItemAnimator对应的方法返回true的话就执行postAnimationRunner(),该方法就是将mItemAnimatorRunner放到下一帧执行,而mItemAnimatorRunner实际调用了mItemAnimator.runPendingAnimations()执行了一段时间内触发的所有动画。它们的代码如下所示。

        void animateDisappearance(@NonNull ViewHolder holder,
                                  @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) {
            addAnimatingView(holder);
            holder.setIsRecyclable(false);
            if (mItemAnimator.animateDisappearance(holder, preLayoutInfo, postLayoutInfo)) {
                postAnimationRunner();
            }
        }
    
        void postAnimationRunner() {
            if (!mPostedAnimatorRunner && mIsAttached) {
                // 将mItemAnimatorRunner放到下一帧执行
                ViewCompat.postOnAnimation(this, mItemAnimatorRunner);
                mPostedAnimatorRunner = true;
            }
        }
    
        private Runnable mItemAnimatorRunner = new Runnable() {
            @Override
            public void run() {
                if (mItemAnimator != null) {
                    mItemAnimator.runPendingAnimations();
                }
                mPostedAnimatorRunner = false;
            }
        };
    

    动画触发的大体逻辑就是这样,不过为了加深印象,我们再举个栗子详细描述下。还是以REMOVE动画为例,来逐步分析下方动画执行时保存了哪些信息,又是怎么判断动画类型的。

    gif-例子.gif

    上面提到,dispatchLayoutStep1()方法会将动画执行前的ItemHolderInfo保存至ViewInfoStore,那么上方的例子在动画前会保存5个ItemHolderInfo,由于是VERTICAL布局,只关注ItemHolderInfo的top和bottom即可。
    下方的ViewHolder(0)等表示保存时的Key,括号中的数字为该ViewHolder对应ItemView显示的数据

    ViewHolder(0) : InfoRecord->preInfo(top: 0,   bottom: 131)
    ViewHolder(1) : InfoRecord->preInfo(top: 131, bottom: 262)
    ViewHolder(2) : InfoRecord->preInfo(top: 262, bottom: 393)
    ViewHolder(3) : InfoRecord->preInfo(top: 393, bottom: 524)
    ViewHolder(4) : InfoRecord->preInfo(top: 524, bottom: 655)
    

    随后dispatchLayoutStep2()调用mLayout.onLayoutChildren(mRecycler, mState)进行布局。布局完毕后执行dispatchLayoutStep3(),开始保存Layout之后的ItemHolderInfo,此时有4个Item,它们的信息会被保存至InfoRecord的postInfo中,最终ViewInfoStore中mLayoutHolderMap的信息如下所示。

    ViewHolder0 : InfoRecord->preInfo(top: 0,   bottom: 131), postInfo(top: 0, bottom: 131)
    ViewHolder1 : InfoRecord->preInfo(top: 131, bottom: 262), postInfo(null)
    ViewHolder2 : InfoRecord->preInfo(top: 262, bottom: 393), postInfo(top: 131, bottom: 262)
    ViewHolder3 : InfoRecord->preInfo(top: 393, bottom: 524), postInfo(top: 262, bottom: 393)
    ViewHolder4 : InfoRecord->preInfo(top: 524, bottom: 655), postInfo(top: 393, bottom: 524)
    

    随后执行mViewInfoStore.process(mViewInfoProcessCallback)开始动画,这里通过判断preInfo和postInfo是否存在去执行对应的回调,下面的代码只保留了本次例子相关的部分。

        void process(ProcessCallback callback) {
            for (int index = mLayoutHolderMap.size() - 1; index >= 0; index--) {
                final RecyclerView.ViewHolder viewHolder = mLayoutHolderMap.keyAt(index);
                final InfoRecord record = mLayoutHolderMap.removeAt(index);
                if ((record.flags & FLAG_APPEAR_AND_DISAPPEAR) == FLAG_APPEAR_AND_DISAPPEAR) {
                    // ......
                } else if ((record.flags & FLAG_DISAPPEARED) != 0) {
                    // ......
                } else if ((record.flags & FLAG_APPEAR_PRE_AND_POST) == FLAG_APPEAR_PRE_AND_POST) {
                    // ......
                } else if ((record.flags & FLAG_PRE_AND_POST) == FLAG_PRE_AND_POST) {
                    // preInfo和postInfo都有,执行callback.processPersistent(...)
                    callback.processPersistent(viewHolder, record.preInfo, record.postInfo);
                } else if ((record.flags & FLAG_PRE) != 0) {
                    // 有preInfo没有postInfo,说明Item被移除了,执行callback.processDisappeared()
                    callback.processDisappeared(viewHolder, record.preInfo, null);
                } else if ((record.flags & FLAG_POST) != 0) {
                    // ......
                } else if ((record.flags & FLAG_APPEAR) != 0) {
                    // ......
                }
                InfoRecord.recycle(record);
            }
        }
    

    先来看callback.processPersistent(),由于整个dataSet并未改变,因此进入else代码块,根据mItemAnimator.animatePersistence(...)的返回值决定是否执行postAnimationRunner()

        @Override
        public void processPersistent(ViewHolder viewHolder, @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
            viewHolder.setIsRecyclable(false);
            if (mDataSetHasChangedAfterLayout) {
                // since it was rebound, use change instead as we'll be mapping them from
                // stable ids. If stable ids were false, we would not be running any animations
                if (mItemAnimator.animateChange(viewHolder, viewHolder, preInfo, postInfo)) {
                    postAnimationRunner();
                }
            } else if (mItemAnimator.animatePersistence(viewHolder, preInfo, postInfo)) {
                postAnimationRunner();
            }
        }
    

    mItemAnimator.animatePersistence(...)执行的是SimpleItemAnimator中重写的方法,如下所示。根据上面总结的ViewInfoStore中保存的值,对于ViewHolder(2)、ViewHolder(3)和ViewHolder(4)来说,preInfo.top和postInfo.top不相等,执行animateMove(...),最终会执行DefaultItemAnimator中重写的方法,将MOVE动画添加到待执行动画列表中。而对于ViewHolder(0)来说,preInfo和postInfo中的值相等,就不用执行动画。

        @Override
        public boolean animatePersistence(@NonNull RecyclerView.ViewHolder viewHolder,
                                          @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
            if (preInfo.left != postInfo.left || preInfo.top != postInfo.top) {
                return animateMove(viewHolder,
                        preInfo.left, preInfo.top, postInfo.left, postInfo.top);
            }
            dispatchMoveFinished(viewHolder);
            return false;
        }
    

    再来看callback.processDisappeared(),直接执行了animateDisappearance(...)

        @Override
        public void processDisappeared(ViewHolder viewHolder, @NonNull ItemHolderInfo info,
                                @Nullable ItemHolderInfo postInfo) {
            mRecycler.unscrapView(viewHolder);
            animateDisappearance(viewHolder, info, postInfo);
        }
    

    也是根据mItemAnimator.animateDisappearance()的返回值决定是否执行postAnimationRunner()

        void animateDisappearance(@NonNull ViewHolder holder,
                                  @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) {
            addAnimatingView(holder);
            holder.setIsRecyclable(false);
            if (mItemAnimator.animateDisappearance(holder, preLayoutInfo, postLayoutInfo)) {
                postAnimationRunner();
            }
        }
    

    mItemAnimator.animateDisappearance()执行了SimpleItemAnimator中重写的方法。

        @Override
        public boolean animateDisappearance(@NonNull RecyclerView.ViewHolder viewHolder,
                                            @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) {
            int oldLeft = preLayoutInfo.left;
            int oldTop = preLayoutInfo.top;
            View disappearingItemView = viewHolder.itemView;
            int newLeft = postLayoutInfo == null ? disappearingItemView.getLeft() : postLayoutInfo.left;
            int newTop = postLayoutInfo == null ? disappearingItemView.getTop() : postLayoutInfo.top;
            if (!viewHolder.isRemoved() && (oldLeft != newLeft || oldTop != newTop)) {
                disappearingItemView.layout(newLeft, newTop,
                        newLeft + disappearingItemView.getWidth(),
                        newTop + disappearingItemView.getHeight());
                }
                return animateMove(viewHolder, oldLeft, oldTop, newLeft, newTop);
            } else {
                return animateRemove(viewHolder);
            }
        }
    

    由于viewHolder.isRemoved()返回true,因此执行animateRemove(viewHolder),最终执行DefaultItemAnimator中重写的方法,将动画添加到了执行队列中。
    到此,我们的例子也就讲完了。

    二、自定义动画

    DefaultItemAnimator提供的动画效果还是比较完善的,如果还有其他需求的话,在animateRemoveImpl(...)animateAddImpl(...)这样的方法中修改动画效果即可。方法内部通过ViewPropertyAnimator实现具体的动画效果,修改起来比较简单,例如我们可以将DefaultItemAnimator的Remove动画修改为:Item缩小至消失。

        private void animateRemoveImpl(final RecyclerView.ViewHolder holder) {
            // ......
            animation.setDuration(getRemoveDuration()).scaleX(0).scaleY(0).setListener(
                    new AnimatorListenerAdapter() {
                        @Override
                        public void onAnimationStart(Animator animator) {
                            dispatchRemoveStarting(holder);
                        }
    
                        @Override
                        public void onAnimationEnd(Animator animator) {
                            animation.setListener(null);
                            view.setScaleX(1);
                            view.setScaleY(1);
                            dispatchRemoveFinished(holder);
                            mRemoveAnimations.remove(holder);
                            dispatchFinishedWhenDone();
                        }
                    }).start();
        }
    

    真的超简单啊有没有!!!来看下效果。

    gif-自定义.gif

    emmmm...效果不怎么好看,不过只要能设计出优美的动画,自定义ItemAnimator是一件很简单的事情。

    三、参考

    1. RecyclerView.ItemAnimator终极解读(一)--RecyclerView源码解析
    2. RecyclerView机制解析: ChildHelper
    3. RecyclerView剖析

    相关文章

      网友评论

        本文标题:Android - RecyclerView进阶(3)—Item

        本文链接:https://www.haomeiwen.com/subject/xjrofhtx.html