精华内容
下载资源
问答
  • RecyclerView动画

    2017-04-27 15:56:42
    实现RecyclerView列表项的展示动画,丰富RecyclerView展示效果。
  • RecyclerView动画:属性动画

    千次阅读 2019-08-08 21:43:42
    通过RecyclerView的实现展开动画动画的时候,... 为了更好的实现动画的自定义,必须查看RecyclerView动画的实现机制。在此基础上才能够有机会hook动画机制,从而实现更好的自定义。目前实现了动画的差值器、展开距离...

            通过RecyclerView的实现展开动画动画的时候,首先想到的是通过原生的方法notifyItemRangeChanged来实现。但是android并没有提供很多Api,让使用者来自定义动画。

            为了更好的实现动画的自定义,必须查看RecyclerView动画的实现机制。在此基础上才能够有机会hook动画机制,从而实现更好的自定义。目前实现了动画的差值器、展开距离、动画时间的自定义。

            从postAnimationRunner方法开始,RecyclerView的Item动画就真正的开始了。

            通过postOnAnimation方法在主线程的handler中post了一个Runnable。这个Runnable中就是就是整个Item动画中心。所有Item的move、remove、add、change动画都是这里开始装载、开始的。

          在runPendingAnimations方法中,我们来关注下change的动画。change动画是通过DefaultItemAnimator.ChangeInfo类来封装的,并通过列表(ArrayList<DefaultItemAnimator.ChangeInfo> mPendingChanges)来管理所有待动画的item项。ChangeInfo的添加逻辑逻辑就不在这里展开了感兴趣的同学可以查看RecyclerView源码中的dispatchLayoutStep3()方法。这里必须提出一个RecyclerView的坑,当展开的item过多时(高度过高),就没有了展开动画。

           因为RecyclerView是自身height来决定加载Item数量的,所以当添加的item项的总高度大于RecyclerView高度时,这个时候之前的item项会从父View中移除。导致的结果是没item进行change动画。RecyclerView展开动画的实质就是对被展开item项下面的所有item进行translate动画。既然item已经被移除了界面,那动画肯定就没了。

          下面将会请出今天的主角ViewPropertyAnimator。在animateChangeImpl方法中对itemView进行了动画的设置,其中设置了动画的时间,translateX,translateY,alpha等属性的设置。(注意这里可能会涉及到连个View的动画,这里不做展开)

          重头戏终于要开始了。下面来看看属性动画是怎么来改变View属性从而实现动画的呢?是什么来驱动动画的呢?首先我们来看看ViewPropertyAnimator的startAnimation方法。通过源码分析可知,方法中主要是对ValueAnimator进行设置,可知真正的动画是通过ValueAnimator来实现的。

           ValueAnimator的start方法也是暗藏玄机。其中有一方法addAnimationCallback。方法中通过AnimationHandler对象实现了属性动画的注册。

            AnimationHandler可以说是来头不小,它可以说是管理者所有的属性动画。通过AnimationHandler的统一管理能够确保所有的属性动画在主线程中自信,同时确保了多个动画能够在相同的时间去修改属性,实现动画时间的统一性。

           那么驱动AnimationHandler的动力是什么呢?说道动力我们就不得不请出UI届扛把子Choreographer。对于这位老大哥,大家应该都有所耳闻。在android上层的UI绘制中,每次都是他响应UI绘制请求。大家都知道在其doFrame方法中有三部曲,分别是measureHierarchy、performLayout、performDraw。

          在演奏三部曲之前,Choreographer会先去执行动画,而动画的实质就是不同的时间点对View对想设置不同的属性,然后在绘制到屏幕上。实现就是在Choreographer通过AnimationHandler,在每次绘制请求到来的时候回调AnimationHandler中注册的ValueAnimator。

          首先看看回调AnimationHandler的内容。

    上述方法比较简单,主要是回调了doAnimationFrame方法。在doAnimationFrame中主要也是回调注册的ValueAnimator对象。

           在doAnimationFrame方法中主要是基于基于时间线计算对应的值,并回调注册的监听。本例中注册的是ViewPropertyAnimator对象。下面是截取的部分源码。感兴趣的同学可以看看源码。这里不再赘述。

    ValueAnimator中的回调:

    ViewPropertyAnimator中的回调:

     

     

     

     

     

     

     

    展开全文
  • InstaMaterial:正确处理RecyclerView动画.pdf
  • RecyclerView动画Enter-animation-demo,ecyclerView动画Enter-animation-demo,ecyclerView动画Enter-animation-demo,
  • 安卓利用RecyclerView并且借助动画实现列表Item的可展开收缩效果
  • 定制RecyclerView的itemAnimator,关注博客:http://blog.csdn.net/qibin0506
  • 屏幕快照开发:ruzhan-[受电子邮件保护]许可证版权所有2018 ruzhan根据Apache许可证2.0版(“许可证”)获得许可; 除非遵照屏幕截图,否则您不得使用此文件。开发者:ruzhan-[受电子邮件保护] License版权2018 ...
  • 本文是RecyclerView源码分析系列第四篇文章,内容主要是...本文主要分析RecyclerView删除动画的实现原理,不同类型动画的大体实现流程其实都是差不多的,所以对于添加、交换这种动画就不再做分析。本文主要目标是理解...

    本文是RecyclerView源码分析系列第四篇文章,内容主要是基于前三篇文章来叙述的,因此在阅读之前推荐看一下前3篇文章:

    RecylcerView的基本设计结构

    RecyclerView的刷新机制

    RecyclerView的复用机制

    本文主要分析RecyclerView删除动画的实现原理,不同类型动画的大体实现流程其实都是差不多的,所以对于添加、交换这种动画就不再做分析。本文主要目标是理解清楚RecyclerViewItem删除动画源码实现逻辑。文章比较长。

    可以通过下面这两个方法触发RecyclerView的删除动画:

        //一个item的删除动画
        dataSource.removeAt(1)
        recyclerView.adapter.notifyItemRemoved(1)
    
        //多个item的删除动画
        dataSource.removeAt(1)
        dataSource.removeAt(1)
        recyclerView.adapter.notifyItemRangeRemoved(1,2)
    复制代码

    下面这个图是设置10倍动画时长时删除动画的执行效果,可以先预想一下这个动画时大致可以怎么实现:

    接下来就结合前面几篇文章的内容并跟随源码来一块看一下RecyclerView是如何实现这个动画的:

    adapter.notifyItemRemoved(1)会回调到RecyclerViewDataObserver:

        public void onItemRangeRemoved(int positionStart, int itemCount) {
            if (mAdapterHelper.onItemRangeRemoved(positionStart, itemCount)) {
                triggerUpdateProcessor();
            }
        }
    复制代码

    其实按照onItemRangeRemoved()这个方法可以将Item删除动画分为两个部分:

    1. 添加一个UpdateOpAdapterHelper.mPendingUpdates中。
    2. triggerUpdateProcessor()调用了requestLayout, 即触发了RecyclerView的重新布局。

    先来看mAdapterHelper.onItemRangeRemoved(positionStart, itemCount):

    AdapterHelper

    这个类可以理解为是用来记录adapter.notifyXXX动作的,即每一个Operation(添加、删除)都会在这个类中有一个对应记录UpdateOpRecyclerView在布局时会检查这些UpdateOp,并做对应的操作。 mAdapterHelper.onItemRangeRemoved其实是添加一个Remove UpdateOp:

        mPendingUpdates.add(obtainUpdateOp(UpdateOp.REMOVE, positionStart, itemCount, null));
        mExistingUpdateTypes |= UpdateOp.REMOVE;
    复制代码

    即把一个Remove UpdateOp添加到了mPendingUpdates集合中。

    RecyclerView.layout

    RecyclerView的刷新机制中知道RecyclerView的布局一共分为3分步骤:dispatchLayoutStep1()、dispatchLayoutStep2()、dispatchLayoutStep3(),接下来我们就分析这3步中有关Item删除动画 的工作。

    dispatchLayoutStep1(保存动画现场)

    直接从dispatchLayoutStep1()开始看,这个方法是RecyclerView布局的第一步:

    dispatchLayoutStep1():

        private void dispatchLayoutStep1() {
            ...
            processAdapterUpdatesAndSetAnimationFlags();
            ...
            if (mState.mRunSimpleAnimations) {
                ...
            }
            ...
        }
    复制代码

    上面我只贴出了Item删除动画主要涉及到的部分, 先来看一下processAdapterUpdatesAndSetAnimationFlags()所触发的操作,整个操作链比较长,就不一一跟了,它最终其实是调用到AdapterHelper.postponeAndUpdateViewHolders():

    private void postponeAndUpdateViewHolders(UpdateOp op) {
        mPostponedList.add(op); //op其实是从mPendingUpdates中取出来的
        switch (op.cmd) {
            case UpdateOp.ADD:
                mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount); break;
            case UpdateOp.MOVE:
                mCallback.offsetPositionsForMove(op.positionStart, op.itemCount); break;
            case UpdateOp.REMOVE:
                mCallback.offsetPositionsForRemovingLaidOutOrNewView(op.positionStart, op.itemCount); break;  
            case UpdateOp.UPDATE:
                mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload); break;    
            ...
        }
    }
    复制代码

    即这个方法做的事情就是把mPendingUpdates中的UpdateOp添加到mPostponedList中,并回调根据op.cmd来回调mCallback,其实这个mCallback是回调到了RecyclerView中:

     void offsetPositionRecordsForRemove(int positionStart, int itemCount, boolean applyToPreLayout) {
            final int positionEnd = positionStart + itemCount;
            final int childCount = mChildHelper.getUnfilteredChildCount();
            for (int i = 0; i < childCount; i++) {
                final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
                ...
                if (holder.mPosition >= positionEnd) {
                    holder.offsetPosition(-itemCount, applyToPreLayout);
                    mState.mStructureChanged = true;
                }
                ...
            }
            ...
        }
    复制代码

    offsetPositionRecordsForRemove方法:主要是把当前显示在界面上的ViewHolder的位置做对应的改变,即如果item位于删除的item之后,那么它的位置应该减一,比如原来的位置是3现在变成了2。

    接下来继续看dispatchLayoutStep1()中的操作:

        if (mState.mRunSimpleAnimations) {
            int count = mChildHelper.getChildCount();
            for (int i = 0; i < count; ++i) {
                final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
                //根据当前的显示在界面上的ViewHolder的布局信息创建一个ItemHolderInfo
                final ItemHolderInfo animationInfo = mItemAnimator
                        .recordPreLayoutInformation(mState, holder,
                                ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
                                holder.getUnmodifiedPayloads());
                mViewInfoStore.addToPreLayout(holder, animationInfo); //把 holder对应的animationInfo保存到 mViewInfoStore中
                ...
            }
        }
    复制代码

    即就做了两件事:

    1. 为当前显示在界面上的每一个ViewHolder创建一个ItemHolderInfoItemHolderInfo其实就是保存了当前显示itemview的布局的top、left等信息
    2. 拿着ViewHolder和其对应的ItemHolderInfo调用mViewInfoStore.addToPreLayout(holder, animationInfo)

    mViewInfoStore.addToPreLayout()就是把这些信息保存起来:

    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;
    }
    复制代码

    即把holder 和 info保存到mLayoutHolderMap中。可以理解为它是用来保存动画执行前当前界面ViewHolder的信息一个集合。

    到这里大致理完了在执行Items删除动画AdapterHelperdispatchLayoutStep1()的执行逻辑,这里用一张图来总结一下:

    其实这些操作可以简单的理解为保存动画前View的现场 。其实这里有一次预布局,预布局也是为了保存动画前的View信息,不过这里就不讲了。

    dispatchLayoutStep2

    这一步就是摆放当前adapter中剩余的Item,在本文的例子中,就是依次摆放剩余的5个Item。在前面的文章RecyclerView的刷新机制中,我们知道LinearLayoutManager会向RecyclerView来填充RecyclerView,所以RecyclerView中填几个View,其实和Recycler有很大的关系,因为Recycler不给LinearLayoutManager的话,RecyclerView中就不会有View填充。那RecyclerLinearLayoutManager``View的边界条件是什么呢? 我们来看一下tryGetViewHolderForPositionByDeadline()方法:

    ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
            if (position < 0 || position >= mState.getItemCount()) {
                throw new IndexOutOfBoundsException("Invalid item position " + position
                        + "(" + position + "). Item count:" + mState.getItemCount()
                        + exceptionLabel());
            }
    }
    复制代码

    即如果位置大于mState.getItemCount(),那么就不会再向RecyclerView中填充子View。而这个mState.getItemCount()一般就是adapter中当前数据源的数量。所以经过这一步布局后,View的状态如下图:

    这时你可能就有疑问了? 动画呢? 怎么直接成最终的模样了?别急,这一步只不过是布局,至于动画是怎么执行的我们继续往下看:

    dispatchLayoutStep3(执行删除动画)

    在上一步中对删除操作已经完成了布局,接下来dispatchLayoutStep3()就会做删除动画:

    private void dispatchLayoutStep3() {
        ...
        if (mState.mRunSimpleAnimations) {
            ...
            mViewInfoStore.process(mViewInfoProcessCallback); //触发动画的执行
        }
        ...
    }
    复制代码

    可以看到主要涉及到动画的是mViewInfoStore.process(), 其实这一步可以分为两个操作:

    1. 先把Item View动画前的起始状态准备好
    2. 执行动画使Item View到目标布局位置

    下面我们来继续跟一下mViewInfoStore.process()这个方法

    Item View动画前的起始状态准备好

     void process(ProcessCallback callback) {
            for (int index = mLayoutHolderMap.size() - 1; index >= 0; index--) { //对mLayoutHolderMap中每一个Holder执行动画
                final RecyclerView.ViewHolder viewHolder = mLayoutHolderMap.keyAt(index);
                final InfoRecord record = mLayoutHolderMap.removeAt(index);
                if ((record.flags & FLAG_APPEAR_AND_DISAPPEAR) == FLAG_APPEAR_AND_DISAPPEAR) {
                    callback.unused(viewHolder);
                } else if ((record.flags & FLAG_DISAPPEARED) != 0) {
                    callback.processDisappeared(viewHolder, record.preInfo, record.postInfo);  //被删除的那个item会回调到这个地方
                }else if ((record.flags & FLAG_PRE_AND_POST) == FLAG_PRE_AND_POST) {
                    callback.processPersistent(viewHolder, record.preInfo, record.postInfo);   //需要上移的item会回调到这个地方
                }  
                ...
                InfoRecord.recycle(record);
            }
        }
    复制代码

    这一步就是遍历mLayoutHolderMap对其中的每一个ViewHolder做对应的动画。这里callback会调到了RecyclerView,RecyclerView会对每一个Item执行相应的动画:

    ViewInfoStore.ProcessCallback mViewInfoProcessCallback =
            new ViewInfoStore.ProcessCallback() {
                @Override
                public void processDisappeared(ViewHolder viewHolder, @NonNull ItemHolderInfo info,@Nullable ItemHolderInfo postInfo) {
                    mRecycler.unscrapView(viewHolder);   //从scrap集合中移除,
                    animateDisappearance(viewHolder, info, postInfo);
                }
    
                @Override
                public void processPersistent(ViewHolder viewHolder, @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
                    ...
                    if (mItemAnimator.animatePersistence(viewHolder, preInfo, postInfo)) {
                        postAnimationRunner();
                    }
                }
                ...
            }
    }
    复制代码

    先来分析被删除那那个Item的消失动画:

    将Item的动画消失动画放入到mPendingRemovals待执行队列

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

    先把Holderattch到RecyclerView上(这是因为在dispatchLayoutStep1dispatchLayoutStep2中已经对这个Holder做了Dettach)。即它又重新出现在了RecyclerView的布局中(位置当然还是未删除前的位置)。然后调用了mItemAnimator.animateDisappearance()其执行这个删除动画,mItemAnimatorRecyclerView的动画实现者,它对应的是DefaultItemAnimator。继续看animateDisappearance()它其实最终调用到了DefaultItemAnimator.animateRemove():

    public boolean animateRemove(final RecyclerView.ViewHolder holder) {
        resetAnimation(holder);
        mPendingRemovals.add(holder);
        return true;
    }
    复制代码

    即,其实并没有执行动画,而是把这个holder放入了mPendingRemovals集合中,看样是要等下执行。

    将未被删除的Item的移动动画放入到mPendingMoves待执行队列

    其实逻辑和上面差不多DefaultItemAnimator.animatePersistence():

    public boolean animatePersistence(@NonNull RecyclerView.ViewHolder viewHolder,@NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
        if (preInfo.left != postInfo.left || preInfo.top != postInfo.top) {  //和预布局的状态不同,则执行move动画
            return animateMove(viewHolder,preInfo.left, preInfo.top, postInfo.left, postInfo.top);
        }
        ...
    }
    复制代码

    animateMove的逻辑也很简单,就是根据偏移构造了一个MoveInfo然后添加到mPendingMoves中,也没有立刻执行:

    public boolean animateMove(final RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) {
        final View view = holder.itemView;
        fromX += (int) holder.itemView.getTranslationX();
        fromY += (int) holder.itemView.getTranslationY();
        resetAnimation(holder);
        int deltaX = toX - fromX;
        int deltaY = toY - fromY;
        if (deltaX == 0 && deltaY == 0) {
            dispatchMoveFinished(holder);
            return false;
        }
        if (deltaX != 0) {
            view.setTranslationX(-deltaX);  //设置他们的位置为负偏移!!!!!
        }
        if (deltaY != 0) {
            view.setTranslationY(-deltaY);  //设置他们的位置为负偏移!!!!!
        }
        mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
        return true;
    }
    复制代码

    但要注意这一步把要做滚动动画的View的TranslationXTranslationY都设置负的被删除的Item的高度,如下图

    即被删除的Item之后的Item都下移了

    postAnimationRunner()执行所有的pending动画

    上面一步操作已经把动画前的状态准备好了,postAnimationRunner()就是将上面pendding的动画开始执行:

    //DefaultItemAnimator.java

        public void runPendingAnimations() {
            boolean removalsPending = !mPendingRemovals.isEmpty();
            ...
            for (RecyclerView.ViewHolder holder : mPendingRemovals) {
                animateRemoveImpl(holder); //执行pending的删除动画
            }
            mPendingRemovals.clear();
    
            if (!mPendingMoves.isEmpty()) { //执行pending的move动画
                final ArrayList<MoveInfo> moves = new ArrayList<>();
                moves.addAll(mPendingMoves);
                mMovesList.add(moves);
                mPendingMoves.clear();
                Runnable mover = new Runnable() {
                    @Override
                    public void run() {
                        for (MoveInfo moveInfo : moves) {
                            animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY,
                                    moveInfo.toX, moveInfo.toY);
                        }
                        moves.clear();
                        mMovesList.remove(moves);
                    }
                };
                if (removalsPending) {
                    View view = moves.get(0).holder.itemView;
                    ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration());
                } else {
                    mover.run();
                }
            }
            ...
        }
    复制代码

    至于animateRemoveImplanimateMoveImpl的源码具体我就不贴了,直接说一下它们做了什么操作吧:

    1. animateRemoveImpl 把这个被Remove的Item做一个透明度由(1~0)的动画
    2. animateMoveImpl把它们的TranslationXTranslationY移动到0的位置。

    我再贴一下删除动画的gif, 你感受一下是不是这个执行步骤:

    欢迎关注我的Android进阶计划。看更多干货

    展开全文
  • RecyclerView动画集合

    2016-03-16 16:34:32
    https://github.com/wasabeef/recyclerview-animators
    展开全文
  • RecyclerViewAnimations ... 想进一步了解RecyclerView动画吗? 检查我有关在InstaMaterial项目中实现它们的博客文章: ://frogermcs.github.io/instamaterial-recyclerview-animations-done-right/
  • InstaMaterial:正确处理RecyclerView动画

    千次阅读 2016-03-06 18:53:09
    英文原文:InstaMaterial - RecyclerView animations done right (thanks to Android Dev Summit!)  我们生活在一个app不仅要能用还要流畅和好看的年代。不同与几年前,我们对ListView adapter唯一要做的事情就是...

    英文原文:InstaMaterial - RecyclerView animations done right (thanks to Android Dev Summit!) 

    我们生活在一个app不仅要能用还要流畅和好看的年代。不同与几年前,我们对ListView adapter唯一要做的事情就是调用notifyDataSetChanged(),屏幕一闪,显示新的数据,完事。

    今天,在RenderThread的时代,MaterialDesign动画以及过渡效果的app应该完全展示出所发生的事情。用户应该看到什么时候它的集合发生了改变或者什么时候新的元素出现(或者被移除)。


    几周前,我们看到了(现场或者线上)一个伟大的安卓大会 - Android Dev Summit。在这两天的深入技术会议中我们可以看到安卓工程师团队推出了的新东西 - Android Studio 2.0,新的Gradle 插件, Instant run功能,新的官方模拟器等等。

    顺便说一句,如果你错过了,我强烈建议去观看整个播放列表 - 这里面有很多关于安卓开发,工具以及解决方案的会议。

    其中的一个视频- RecyclerView Animations and Behind the Scenes 正是写本文的原因。Chet Haase 和 Yigit Boyar 过了一遍RecyclerView的item动画并演示了如何正确的做这件事。对于要学习如何让RecyclerView更有吸引力更好看来说,这个视频是个很好的开始。

    https://youtu.be/imsr8NrIAMs?list=PLWz5rJ2EKKc_Tt7q77qwyKRgytF1RzRx8 

    InstaMaterial 遇到 RecyclerView使用指南  

    今天我们将从特定的角度去看看RecyclerView动画(很快我将试试正式的去深入探讨整个RecyclerView)。

    我们想要整理的InstaMaterial源码在这个 commit 中(最新的前几个版本已经根据以下的描述进行了更新。)

    还有一点同样重要 - 从一个用户的角度来说,这个版本没有任何改变。但是从代码的角度来说,我们将更明智(至少更干净)。

    期望的效果

    我们想要重建的代码负责两个相似的操作:

    1. like操作:点击item主图片

    2. like欢操作:点击喜欢按钮

    这些动画应该在RecyclerView中被触发。

    1. 出场动画-当对象首次添加的时候,feed item从底部滑出。

    2. 大like动画 - 当用户点击主图片的时候,圆形背景的心形播放动画。

    3. like按钮动画 - 用户点击like按钮或者点击主图片(这样就是两个动画被播放)心形旋转并被填充。

    这里是上面描述的动画(从最近的app录制过来):

    Appearance animation

    add_anim.gif

    大的like动画

    big_like_anim.gif

    like按钮动画

    small_like_anim.gif

    代码

    以前我们的动画直接在 RecyclerView.Adapter的子类 FeedAdapter里实现的,一切运行正常,那这个方法到底有什么问题呢?

    1. RecyclerView.Adapter并不是为动画而设计的。根据 文档
      Adapters提供了app专用数据到视图的绑定。adapter已经有足够多的绑定代码,如果再加上动画代码将加倍。

    2. 在adapter中处理动画我们需要考虑如何结束它们,恰当的处理view的回收,确保它们在正确的时间准时播放以及更多的事情。所有的事情都靠我们自己。

    3. 虽然单个item动画好处理,但是对象间的互动(移动/交换item,当新的对象显示或者消失时更新item的位置)则是更复杂的事情。

    4. RecyclerView的发明者为我们提供了官方的解决方案:RecyclerView.ItemAnimator:
      这个类定义了当adapter变化时,发生在item上的动画。
      它处理了上面提到的所有情况。因此我们可以更多的去考虑动画的质量,而不是它们在滚动周期中该如何正确处理的逻辑。


    让我们再次看看 FeedAdapter 。

    这几行的代码是不应该在这里的:

    1
    2
    3
    4
    private static final DecelerateInterpolator DECCELERATE_INTERPOLATOR =  new  DecelerateInterpolator();    
    private static final AccelerateInterpolator ACCELERATE_INTERPOLATOR =  new  AccelerateInterpolator();    
    private static final OvershootInterpolator OVERSHOOT_INTERPOLATOR =  new  OvershootInterpolator(4);    
    private static final int ANIMATED_ITEMS_COUNT = 2;

    interpolators.java hosted with ❤ by GitHub

    1
    private boolean showLoadingView =  false ;

    showLoadingView.java hosted with ❤ by GitHub

    我们需要控制什么时候item动画什么时候不动画(item应该在第一次显示的时候动画,而不是在activity恢复的时候)。

    1
    private final Map<RecyclerView.ViewHolder, AnimatorSet> likeAnimations =  new  HashMap<>();

    likeAnimations.java hosted with ❤ by GitHub

    我们应该把动画保存在某个位置,以防我们需要在回收的时候检查它们是否还在运行或者等待被取消。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    private void runEnterAnimation(View view, int position) {
         if  (!animateItems || position >= ANIMATED_ITEMS_COUNT - 1) {
             return ;
         }
     
         if  (position > lastAnimatedPosition) {
             lastAnimatedPosition = position;
             view.setTranslationY(Utils.getScreenHeight(context));
             view.animate()
                     .translationY(0)
                     .setInterpolator( new  DecelerateInterpolator(3.f))
                     .setDuration(700)
                     .start();
         }
    }
     
    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
         runEnterAnimation(viewHolder.itemView, position);
         //...
    }

    runEnterAnimation.java hosted with ❤ by GitHub

    这里,我们在每次view被绑定的时候运行runEnterAnimation,并检查当前是否是应该这么做(item动画只能有一次)。鉴于我们描述的场景,方法的命名可能有些迷惑。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
         //...
         bindLoadingFeedItem(holder);
    }
     
    private void bindDefaultFeedItem(int position, CellFeedViewHolder holder) {
         //...
         updateLikesCounter(holder,  false );
         updateHeartButton(holder,  false );
     
         holder.btnComments.setTag(position);
         holder.btnMore.setTag(position);
         holder.ivFeedCenter.setTag(holder);
         holder.btnLike.setTag(holder);
     
         if  (likeAnimations.containsKey(holder)) {
             likeAnimations.get(holder).cancel();
         }
         resetLikeAnimationState(holder);
    }

    onBindViewHolder.java hosted with ❤ by GitHub

    在onBindViewHolder()的某个时刻,如果动画已经运行,那么我们取消它。这是因为view可能被回收而我们不知道它们是否已经完成。

    updateLikesCounter() 和 updateHeartButton()方法负责两种情况下(动画与静态)内容的设置。

    我们的代码也有一个问题。

    我们把position传递给按钮:

    1
    2
    holder.btnComments.setTag(position);    
    holder.btnMore.setTag(position);

    holder.java hosted with ❤ by GitHub

    让它在后面的onClick()方法中获得:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Override
    public void onClick(View view) {
    //...
         if  (viewId == R.id.btnComments) {
         //...
         else  if  (viewId == R.id.btnMore) {
             if  (onFeedItemClickListener !=  null ) {
                 onFeedItemClickListener.onMoreClick(view, (Integer) view.getTag());
             }
         }
    //...
    }

    onClick.java hosted with ❤ by GitHub

    这个position索引并非总是准确的。尤其是这个position用于这两个情形时:放置上下文菜单到屏幕的正确位置和把adapter的item传递给它的时候(好吧,理论上讲是这种情况)。

    因为RecyclerView可以通过异步的方式更新数据(item视图可以不用更新数据就被移除 - 比如notifyItemMoved()),所以有可能我们的position指向的是错误的数据。

    这非常类似于 Yigit Boyar所讨论的:

    Adapter position

    我们不能假设item position是final的(这张幻灯片中的代码就会导致问题)。

    所以我们应该转使用RecyclerView.ViewHolder的两个方法:

    新的实现

    让我们从头再来。我们的Feed将由这些部分组成:

    • FeedItemAnimator继承于DefaultItemAnimator(而它继承于RecyclerView.ItemAnimator)。它为我们提供了RecyclerView执行的默认动画(主要是淡入和淡出),并且这个动画可以在我们认为重要的地方做相关的继承(意思应该是重写相关方法吧)。

    • LinearLayoutManager - 跟前面一样,让feed看起来像一个标准的列表。

    • FeedAdapter - 绑定数据 (并且只做这件事).

    FeedItemAnimator

     FeedItemAnimator的完整代码。

    同时这里的FeedItemAnimator中我们有一个更有趣的代码:

    1
    2
    3
    4
    @Override    
    public boolean canReuseUpdatedViewHolder(RecyclerView.ViewHolder viewHolder) {    
         return  true ;    
    }

    canReuseUpdatedViewHolder.java hosted with ❤ by GitHub

    注:这个方法是Android Support Library 23.1中加入的,参见:Android Support Library 23.1的变化 一文。

    在我们播放RecyclerView item动画的时候,我们有一次让RecyclerView保持item的前一个ViewHolder 不变并提供新的ViewHolder来过渡的机会(我们的RecyclerView中只有新的ViewHolder才会可见)。但是我们为布局写了一个自定义的item animator ,所以我们应该使用一样的ViewHolder,并手动动画内容变化。这就是为什么我们的方法返回true的原因。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @NonNull
    @Override
    public ItemHolderInfo recordPreLayoutInformation(@NonNull RecyclerView.State state,
                                                      @NonNull RecyclerView.ViewHolder viewHolder,
                                                      int changeFlags, @NonNull List<Object> payloads) {
         if  (changeFlags == FLAG_CHANGED) {
             for  (Object payload : payloads) {
                 if  (payload  instanceof  String) {
                     return  new  FeedItemHolderInfo((String) payload);
                 }
             }
         }
     
         return  super .recordPreLayoutInformation(state, viewHolder, changeFlags, payloads);
    }

    recordPreLayoutInformation.java hosted with ❤ by GitHub

    当我们调用notifyItemChanged() 方法时,我们可以传入额外的参数帮助我们决定该执行什么什么动画。

    FeedAdapter的例子:

    • notifyItemChanged(adapterPosition, ACTION_LIKE_IMAGE_CLICKED);

    • notifyItemChanged(adapterPosition, ACTION_LIKE_BUTTON_CLICKED);

    recordPreLayoutInformation() 方法用于在数据改变之前缓存数据。RecyclerView调用onBindViewHolder()(adapter中)然后ItemAnimator调用recordPostLayoutInformation() 缓存数据。

    正因为这些操作我们才能得到item改变前后的状态。

    最后是调用animateChange()方法,并传入前后ItemHolderInfo对象。如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @Override
    public boolean animateChange(@NonNull RecyclerView.ViewHolder oldHolder,
                                  @NonNull RecyclerView.ViewHolder newHolder,
                                  @NonNull ItemHolderInfo preInfo,
                                  @NonNull ItemHolderInfo postInfo) {
         cancelCurrentAnimationIfExists(newHolder);
     
         if  (preInfo  instanceof  FeedItemHolderInfo) {
             FeedItemHolderInfo feedItemHolderInfo = (FeedItemHolderInfo) preInfo;
             FeedAdapter.CellFeedViewHolder holder = (FeedAdapter.CellFeedViewHolder) newHolder;
     
             animateHeartButton(holder);
             updateLikesCounter(holder, holder.getFeedItem().likesCount);
             if  (FeedAdapter.ACTION_LIKE_IMAGE_CLICKED.equals(feedItemHolderInfo.updateAction)) {
                 animatePhotoLike(holder);
             }
         }
     
         return  false ;
    }

    animateChange.java hosted with ❤ by GitHub

    我们已经清楚的看到心形按钮动画总是被触发,而大图片动画只在用户点击feed图片的时候被触发。这正是我们想要的效果。


    第二件事-入场动画。它应该在我们第一次看见列表的时候被触发。如何处理呢?如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Override
    public boolean animateAdd(RecyclerView.ViewHolder viewHolder) {
         if  (viewHolder.getItemViewType() == FeedAdapter.VIEW_TYPE_DEFAULT) {
             if  (viewHolder.getLayoutPosition() > lastAddAnimatedItem) {
                 lastAddAnimatedItem++;
                 runEnterAnimation((FeedAdapter.CellFeedViewHolder) viewHolder);
                 return  false ;
             }
         }
     
         dispatchAddFinished(viewHolder);
         return  false ;
    }

    animateAdd.java hosted with ❤ by GitHub

    当FeedAdapter触发 notifyItemRangeInserted()的时候, 这个 RecyclerView.ItemAnimator的方法将被调用。另一个方法就是调用notifyItemInserted()。

    还有什么?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Override
    public void endAnimation(RecyclerView.ViewHolder item) {
         super .endAnimation(item);
         cancelCurrentAnimationIfExists(item);
    }
     
    @Override
    public void endAnimations() {
         super .endAnimations();
         for  (AnimatorSet animatorSet : likeAnimationsMap.values()) {
             animatorSet.cancel();
         }
    }

    endAnimation.java hosted with ❤ by GitHub

    实现这两个方法是很有必要的。这样当item视图从屏幕上出现的时候,RecyclerView能够停止item视图上的动画(同时也将准备好回收)。

    另外这两个方法也值得一提:

     

    今天就是这么多。我们更新过的FeedAdapter比之前少了200行代码,同时只负责数据和视图的绑定。这里是它完整的代码

    源码

    最新版本的InstaMaterial源代码可在Github repository上得到。

    作者

    Miroslaw Stanek

    如果你喜欢,你可以分享给你的粉丝或者 关注我! 


    展开全文
  • 我们经常会遇到在一个list中删除一条数据,这时候一般会有一个飞出的动画效果,如下图:在RecyclerView中可以通过setItemAnimator函数设置一个ItemAnimator,实现item的add、remove、change等动作的动效。...
  • github地址: https://github.com/wasabeef/recyclerview-animators 感谢作者。
  • stackoverflow上看到这个问题,答主给了个demohttp://stackoverflow.com/questions/27446051/recyclerview-animate-item-resize 看懂了之后发个博,记录一下,刚开始看别人代码好难受,就这么3个文件看了一晚上。。 ...
  • 1,导入gradle.properties文件到项目 2,在项目下的build.gradle文件下引用依赖 ... 3,在app模块下的build.gradle文件下 引用依赖 compile project(':animators')  RecyclerView项目地址:https://github.com/
  • 我们生活在一个app不仅要能用还要流畅和...今天,在RenderThread的时代,MaterialDesign动画以及过渡效果的app应该完全展示出所发生的事情。用户应该看到什么时候它的集合发生了改变或者什么时候新的元素出现(或者被
  • recyclerView item动画

    2018-10-10 09:33:21
    recyclerview是一个强大的view组件,我往往不喜欢单一的recyclerview,今天我就上传一个我改装过的recyclerview动画组件
  • 很酷炫的一个recyclerview动画加载样式和点击水波纹的样式

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 16,784
精华内容 6,713
关键字:

recyclerview动画