RecyclerView 源码分析(四) - RecyclerView的动画机制

  距离上一篇RecyclerView源码分析的文章已经过去了10多天,今天我们将来看看RecyclerView的动画机制。不过,本文不会分析ItemAnimator相关的知识,而是理解RecyclerView怎么执行ItemAnimator的,有关ItemAniamtor的知识,后面我会写专门的文章来分析。
  本文参考资料:

  1. RecyclerView animations - AndroidDevSummit write-up
  2. RecyclerView.ItemAnimator终极解读(一)--RecyclerView源码解析

  注意,本文所有的代码都来自于27.1.1。

1. 概述

  RecyclerView之所以受欢迎,有一部分的原因得归功于它的动画机制。我们可以通过RecyclerViewsetItemAnimator方法来给每个Item设置在不同行为下,执行不同的动画,非常的简单。尽管我们知道怎么给RecyclerView设置动画,但是RecyclerView是怎么通过ItemAnimator来给每个Item实现动画,这里面的原理值得我们去研究和学习。
  在正式分析RecyclerView的动画机制之前,我们先对几个词语有一个概念,我们来看看:

词语 含义
Disappearance 表示在动画之前,ItemView是可见的,动画之后就可不见了。这里的操作包括,remove操作和普通的滑动导致ItemView划出屏幕
Appearance 表示动画之前,ItemView是不可见,动画之后就可见了。这里的操作包括,add操作和普通的滑动导致ItemView划入屏幕
Persistence 表示动画前后,状态是不变的。这里面的操作包括,无任何操作
change 表示动画前后,状态是不变的。这里面的操作包括,change操作。

  还有注意的一点就是,ViewHolder不是用来记录ItemView的位置信息,而是进行数据绑定的,所以在动画中,关于位置信息的记录不是依靠ViewHolder来实现的,而是依靠一个叫ItemHolderInfo的类实现的,在这个类里面,有四个成员变量,分别记录ItemView的left、top、right和bottom四个位置信息。
  最后还需要注意一点就是,我们从RecyclerView的三大流程中可以得到,在RecyclerView的内部,dispatchLayout分为三步,其中dispathchLayoutStep1被称为预布局,在这里主要是保存ItemViewOldViewHolder,同时还会记录下每个ItemView在动画之前的位置信息;与之对应的dispathchLayoutStep3被称为后布局,主要结合真正布局和预布局的相关信息来实现进行动画,当然前提是RecyclerView本身支持动画。


  本文打算从两个角度来分析RecyclerView的动画,一是从普通三大的流程来看,这是动画机制的核心所在;而是从Adapeter的角度上来看,看看我们每次在调用Adapter的notify相关方法之后,是怎么进行执行动画的(实际上也是回到三大流程里面)。

1. 再来看RecyclerView的三大流程

  取这个题目,我感觉有特别的含义。首先,本次分析动画机制就是重新来看看三大流程,当然本次分三大流程肯定没有之前的那么仔细,其次侧重点也不同;其次,本次再来看RecyclerView的三大流程,还可以填之前在分析RecyclerView的三大流程留下的坑。
  本次的分析重点在于dispathchLayoutStep1dispathchLayoutStep3,不会分析完整的三大流程,所以,还有不懂RecyclerView三大流程的同学,可以参考我的文章:RecyclerView 源码分析(一) - RecyclerView的三大流程
  我们先来看看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);
                }
            }
        }
        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;

            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;
    }

  我将dispatchLayoutStep1方法分为2步(实际上从谷歌爸爸的注释,我们也可以得出来)。

  1. 找到每个没有被remove 掉的ItemView,将它的ViewHolder(OldViewHolder)放在ViewInfoStore里面,同时还将它预布局的位置放在ViewInfoStore里面。这两个信息在后面做动画时都会用到。
  2. 如果当前RecyclerViewLayoutManager支持predictive item animations(supportsPredictiveItemAnimations方法返回true,我觉得用英语描述这种动画挺好的,因为我不知道怎么翻译),会真正的进行预布局。在这一步,会先调用LayoutManageronLayoutChildren进行一次布局,不过这次布局知识预布局,也就是说不是真正的布局,只是先确定每个ItemView的位置。预布局之后,此时取到的每个ItemViewViewHolderItemHolderInfo,便是每个ItemView的最终信息。

  第二步的信息与第一步的信息相互呼应,第一步是变化前的信息,第二步是变化后的信息。这些都是为dispatchLayout3阶段的动画做准备。其中,我们发现相对于第一步,第二步变得复杂了很多。不过,我们可以发现,不管怎么复杂,都是通过调用addToOldChangeHolders方法来保存当前ItemViewViewHolder(在LayoutManageronLayoutChildren方法前后,在同一个位置上,不一定是同一个ItemView,也不一定是同一个ViewHolder),然后调用addXXXLayout方法将位置信息(ItemHolderInfo)保存起来。
  然后,我们再来看看dispatchLayoutStep3阶段:

    private void dispatchLayoutStep3() {
        mState.assertLayoutStep(State.STEP_ANIMATIONS);
        startInterceptRequestLayout();
        onEnterLayoutOrScroll();
        mState.mLayoutStep = State.STEP_START;
        // 将相关信息取到,然后添加到ViewInfoStore
        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);
        }
        // 清理工作阶段
    }

  我将上面的代码分为3阶段。

  1. 获得相关的位置信息(ItemHolderInfo),然后通过addToPostLayout方法将位置保存在ViewInfoStore里面。
  2. 调用ViewInfoStoreprocess方法触发动画。
  3. 进行相关的清理工作。

  这里,我们重点关注前两步就行了。
  其中第一步非常容易理解,先是获到当前ItemView的位置信息,保存在ViewInfoStore里面。其中,我们在这里发现,如果OldViewHolder不为空的话,会特别处理,为什么会这样处理的呢?其实这里考虑到change操作,因为change操作会涉及到两个ItemView的动画变化,所以,我们发现,如果一个ItemView调用的是animateChange方法进行动画开始,而不是走通用的逻辑(将位置信息通过addToPostLayout方法保存起来,然后调用process方法进行统一的调用)。
  然后就是第二步。我们来看看ViewInfoStoreprocess方法,不过在我们在这方法之前,我们我们先看看ProcessCallback接口的几个方法。

方法 作用
processDisappeared 一个ItemView从可见到不可见会回调这个方法,主要是执行这种情况下的动画
processAppeared 一个ItemView从不可见到可见会回调这个方法。
processPersistent 一个ItemView动画前后状态为改变,这里面包括:本身未发生任何操作的ItemView、change操作的ItemView
unused 一个ItemView的变化不支持动画会回调此方法,这里包括比如一个ItemView先是Appeared然后disappeared,这种情况RecyclerView找不到合适的动画;还有当前ItemView缺少preInfo,也就是在预布局未记录位置信息,也会调用此方法,这种情况经常是ItemView进行remove操作,但是Adapter调用的是notifyDataSetChanged方法

  现在,我们正式的来看看process方法:

    void process(ProcessCallback callback) {
        for (int index = mLayoutHolderMap.size() - 1; index >= 0; index--) {
            final 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) {
                // Set as "disappeared" by the LayoutManager (addDisappearingView)
                if (record.preInfo == null) {
                    // similar to appear disappear but happened between different layout passes.
                    // this can happen when the layout manager is using auto-measure
                    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) {
                // Persistent in both passes. Animate persistence
                callback.processPersistent(viewHolder, record.preInfo, record.postInfo);
            } else if ((record.flags & FLAG_PRE) != 0) {
                // Was in pre-layout, never been added to post layout
                callback.processDisappeared(viewHolder, record.preInfo, null);
            } else if ((record.flags & FLAG_POST) != 0) {
                // Was not in pre-layout, been added to post layout
                callback.processAppeared(viewHolder, record.preInfo, record.postInfo);
            } else if ((record.flags & FLAG_APPEAR) != 0) {
                // Scrap view. RecyclerView will handle removing/recycling this.
            } else if (DEBUG) {
                throw new IllegalStateException("record without any reasonable flag combination:/");
            }
            InfoRecord.recycle(record);
        }
    }

  其实process方法非常简单,就是通过相关的flag来调用ProcessCallback相关的方法。我们现在来同一个看看ProcessCallback的每个方法都怎么实现的。

    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) {
                        // 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();
                    }
                }
                @Override
                public void unused(ViewHolder viewHolder) {
                    mLayout.removeAndRecycleView(viewHolder.itemView, mRecycler);
                }
            };

  其实说到底,就是调用了animateXXX方法来实现,而animateXXX方法里面做了啥?其实没啥,就是调用了ViewCompatpostOnAnimation方法往任务队列后面post一个Runnable。代码如下:

    void postAnimationRunner() {
        if (!mPostedAnimatorRunner && mIsAttached) {
            ViewCompat.postOnAnimation(this, mItemAnimatorRunner);
            mPostedAnimatorRunner = true;
        }
    }

  其中,上面的代码中,我们需要注意的是,postAnimationRunner每次只会被调用一次。那么如果在某一次操作中,会执行多个动画,怎么办呢?ProcessCallback每个回调方法都会调用animateXXX方法,而animateXXX方法会调用ItemAnimator对应的方法,在ItemAnimator里面,会将当前动画添加到一个数组里面,然后通过mItemAnimatorRunner调用ItemAnimatorrunPendingAnimations方法,runPendingAnimations方法就是所有动画开始的起点。这里,我们就不讨论ItemAnimator内部的实现,后面有专门的文章来分析它。

2. 从Adapter角度来看动画执行的机制

  我们知道,调用Adapter的notifyDataSetChanged方法,RecyclerView是不会执行动画的;而调用notifyItemRemoved之类的方法是有动画,这里我们从Adapter的角度来分析动画。跟ItemAnimator一样,这里我们也不会去分析Adapter,后面会有专门的文章分析它。
  在分析Adapter之前,我们先来看一个东西,就是RecyclerViewAdapter怎么进行通信。

(1).通过观察者模式来实现RecyclerView 和Adapter的通信

  我们思考这个问题之前,首先应该排除AddapterRecyclerView是强耦合的,也就是说,Adapter内部持有一个RecyclerView对象。RecyclerView本身就是插拔式设计,如果AdapterRecyclerView是强耦合,就违背了插拔式的设计思想。那么它俩究竟是怎么进行通信的呢?答案已经非常的明显了,两者是通过观察者模式来进行通信。
  这其中,Adapter作为被观察者,RecyclerView作为观察者,当Adapter的数据发生改变时,会通知它的每个观察者。
  RecyclerView本身设计又比较特殊,RecyclerView没有去实现Observer(这里暂且这么称呼)接口,而是内部持有一个Observer(RecyclerViewDataObserver)对象,进而监听Adapter的状态变化;当然Adapter也是如此,并没有去实现Observable接口,也是在内部持有一个Observable(AdapterDataObservable)对象。
  我们来看Adapternotify方法跟Observer的方法是怎么进行对应的。

Adapter的notify方法 与之对应的Observer的方法
notifyItemRemoved notifyItemRangeRemoved
notifyItemChanged notifyItemRangeChanged
notifyItemInserted notifyItemRangeInserted
notifyItemMoved notifyItemMoved

  调用到Observer的方法时,Observer会调用AdapterHelper相关的方法,在AdapterHelper内部会为每个操作创建一个UpdateOp对象,并且添加到一个PendingUpdate数组。我们来看看相关代码(以add为例):

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

  如果onItemRangeInserted返回为true,就调用triggerUpdateProcessor方法。为什么这里需要判断是否调用triggerUpdateProcessor方法,其实是为了避免多次调用,比如一个操作,可能会导致多种动画执行,所以这里保证triggerUpdateProcessor方法只会被调用一次。
  然后,我们来看看triggerUpdateProcessor方法:

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

  其实不管是if的执行语句,还是else里面,最终还是调用了requestLayout方法,重新走一遍三大流程。
  可见而知,RecyclerView的三大流程到底多么重要。这次,我们看三大流程中的dispatchLayoutStep2方法。我们知道,在Observer阶段,每个操作其实都创建了一个UpdateOp对象,添加到PendingUpdate数组。那么数组里面的操作都是什么时候执行的呢?其实就是在dispatchLayoutStep2方法阶段:

    private void dispatchLayoutStep2() {
        // ······
        mAdapterHelper.consumeUpdatesInOnePass();
        // ······
    }

  真正执行PendingUpdate的操作是在AdapterHelperconsumeUpdatesInOnePass方法里面,我们来瞧瞧:

    void consumeUpdatesInOnePass() {
        // we still consume postponed updates (if there is) in case there was a pre-process call
        // w/o a matching consumePostponedUpdates.
        consumePostponedUpdates();
        final int count = mPendingUpdates.size();
        for (int i = 0; i < count; i++) {
            UpdateOp op = mPendingUpdates.get(i);
            switch (op.cmd) {
                case UpdateOp.ADD:
                    mCallback.onDispatchSecondPass(op);
                    mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount);
                    break;
                case UpdateOp.REMOVE:
                    mCallback.onDispatchSecondPass(op);
                    mCallback.offsetPositionsForRemovingInvisible(op.positionStart, op.itemCount);
                    break;
                case UpdateOp.UPDATE:
                    mCallback.onDispatchSecondPass(op);
                    mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload);
                    break;
                case UpdateOp.MOVE:
                    mCallback.onDispatchSecondPass(op);
                    mCallback.offsetPositionsForMove(op.positionStart, op.itemCount);
                    break;
            }
            if (mOnItemProcessedCallback != null) {
                mOnItemProcessedCallback.run();
            }
        }
        recycleUpdateOpsAndClearList(mPendingUpdates);
        mExistingUpdateTypes = 0;
    }

  虽然代码不少,但是我们发现了,最终的操作都是调用到了Callback接口里面了。而Callback做了什么呢?主要是做了两件事:

  1. 可能会更新一些ViewHolder的position
  2. 会更新一些ViewHolder的flag,比如说,remove的flag或者update的flag。

  这部分的内容,我们后面分析Adapter会详细的分析,本文就不做过多的介绍了。
  到这里,每个ViewHolder的position都更新完毕,并且每个ViewHolder的flag也已经更新完毕。这样,到了dispatchLayoutStep3阶段,就知道每个ViewHolder应该做什么动画。
  然后,我们来看看为什么调用AdapternotifyDataSetChanged方法不执行动画呢?

(2). 为什么notifyDataSetChanged方法不会执行动画呢?

  notifyDataSetChanged方法会回调到ObservernotifyChanged方法里面,我们看看notifyChanged方法干什么:

        @Override
        public void onChanged() {
            assertNotInLayoutOrScroll(null);
            mState.mStructureChanged = true;

            processDataSetCompletelyChanged(true);
            if (!mAdapterHelper.hasPendingUpdates()) {
                requestLayout();
            }
        }

  在这个方法里面,我们需要特别关注processDataSetCompletelyChanged方法。我们来看看:

    void processDataSetCompletelyChanged(boolean dispatchItemsChanged) {
        mDispatchItemsChangedEvent |= dispatchItemsChanged;
        mDataSetHasChangedAfterLayout = true;
        markKnownViewsInvalid();
    }

  在processDataSetCompletelyChanged方法里面,调用了markKnownViewsInvalid方法所有的ViewHolder标记为了FLAG_INVALID。这个操作直接导致了,我们在预布局阶段不能正确获得每个ItemView的位置信息和OldViewHolder,进而导致在后布局阶段不能执行动画。这就是notifyDataSetChanged方法为什么不执行动画的原因。

3. 总结

  RecyclerView的动画机制还是比较简单的,这里我们对它做一个简单的总结。

  1. RecyclerView执行动画的机制在于,在预布局阶段将每个ItemView的位置信息和ViewHolder保存起来,在后布局阶段,根据每个ItemViewViewHolderflag状态来判断执行什么动画,根据位置信息来判断怎么做动画。
  2. Adapter的notify方法之所以能够执行动画,是因为他们在三大流程中给每个ViewHolder打了响应的flag,包括remove的flag或者update的flag等。而在后布局中,正是根据flag来执行不同的动画的。
  3. notifyDataSetChanged方法之所以不支持动画,那是因为notifyDataSetChanged方法会使每个ViewHolder失效(打了FLAG_INVALID标记),所以导致在预布局阶段,不能正确的获得每个ItemView的位置信息和ViewHolder,进而导致动画不能执行。

  如果不出意外的话,下一篇文章将分析Adapter

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,158评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,600评论 1 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,785评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,655评论 0 220
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,075评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,002评论 1 225
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,146评论 2 318
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,918评论 0 211
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,671评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,838评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,318评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,636评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,343评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,187评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,982评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,126评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,934评论 2 279

推荐阅读更多精彩内容