Android开发Android开发经验谈Android技术知识

RecyclerView ItemAnimator 学习

2020-04-13  本文已影响0人  Ugly_K

RecyclerView ItemAnimator 学习

概述

RecyclerView中,通过设置ItemAnimator来给条目的增删改添加动画效果。

//默认的动画效果
rvCard.setItemAnimator(new DefaultItemAnimator());

1. SimpleItemAnimator

既然官方提供了默认的动画,所以就从默认动画入手:DefaultItemAnimator继承自SimpleItemAnimator

这里新建一个ItemAnimator也继承SimpleItemAnimator,并实现其中的方法,加上日志打印:

public class CustomItemAnimator extends SimpleItemAnimator {
    private static final String TAG = "CustomItemAnimator";

    @Override
    public boolean animateRemove(RecyclerView.ViewHolder holder) {
        Log.d(TAG, "animateRemove() called  holder.pos= ["+holder.getBindingAdapterPosition()+"]");
        return false;
    }

    @Override
    public boolean animateAdd(RecyclerView.ViewHolder holder) {
        Log.d(TAG, "animateAdd() called  holder.pos= ["+holder.getBindingAdapterPosition()+"]");
        return false;
    }

    @Override
    public boolean animateMove(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) {
        Log.d(TAG, "animateMove() called, holder.pos= [" + holder.getBindingAdapterPosition() + "] fromX = [" + fromX + "], fromY = [" + fromY + "], toX = [" + toX + "], toY = [" + toY + "]");
        return false;
    }

    @Override
    public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop) {
        Log.d(TAG, "animateChange() called, holder.pos= [" + newHolder.getBindingAdapterPosition() + "],  holder.pos= [" + oldHolder.getBindingAdapterPosition() + "] fromLeft = [" + fromLeft + "], fromTop = [" + fromTop + "], toLeft = [" + toLeft + "], toTop = [" + toTop + "]");
        return false;
    }

    @Override
    public void runPendingAnimations() {
        Log.d(TAG, "runPendingAnimations() called");
    }

    @Override
    public void endAnimation(@NonNull RecyclerView.ViewHolder item) {
        Log.d(TAG, "endAnimation() called with: item = [" + item + "]");
    }

    @Override
    public void endAnimations() {
        Log.d(TAG, "endAnimations() called");
    }

    @Override
    public boolean isRunning() {
//        Log.d(TAG, "isRunning() called");
        return false;
    }
}

设置给RecyclerView,运行,分别执行增删改动作,查看日志。

首先查看主界面

rv_itemanimator_1.png rv_itemanimator_2.png

可以看到,在新增条目的时候,调用了animateAdd()方法,同时调用了两次animateMove()方法。

通过打印的坐标信息,结合界面的显示可以发现,条目2条目3分别调用了animateMove()方法,而且他们的动作都是向下移动788高度,这也就是条目的高度,即界面上可见的条目2条目3向下腾出一个条目的位置出来,供新增的条目1来显示。

animateAdd()返回值设置为true,查看日志

rv_itemanimator_3.png

可以发现多调用了runPendingAnimations()方法,通过查看SimpleItemAnimator中的animateAdd()方法描述可以知晓,这里的机制是当返回值为true的时候就会调用runPendingAnimations()方法,在这个方法中进行相应的动画操作。

不仅如此,animateAdd animateMove animateRemove animateChange都遵循这个机制,当产生对应的动作的时候,可以在runPendingAnimations()中依次处理动画操作。

rv_itemanimator_5.png

根据方法调用打印的日志可以发现,删除的时候调用的animateMove()方法中的移动就是条目1删除之后,下面的两个条目向上挪动的移动。

2. DefaultItemAnimator

现在分析一下官方提供的DefaultItemAnimator

首先看一下resetAnimation方法

private void resetAnimation(RecyclerView.ViewHolder holder) {
    if (sDefaultInterpolator == null) {
        sDefaultInterpolator = new ValueAnimator().getInterpolator();
    }
    holder.itemView.animate().setInterpolator(sDefaultInterpolator);
    endAnimation(holder);
}

可以看到,在这里给所有条目都添加了一个相同的插值器,然后调用endAnimation方法,顾名思义,就是结束条目的动画;

继续看一下endAnimation方法

/**
 * 判断当前关闭item先关的change动画是否是必要的
 * @param changeInfo change的待执行动画信息
 * @param item 当前的条目
 * @return 
 */
private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, RecyclerView.ViewHolder item) {
    boolean oldItem = false;
    /**
     * 如果这个条目是change中的新条目
     * 那么就直接把这个条目设置为最终形态
     * 并且作为最终的显示
     * 
     * 如果这个条目是change中的旧条目
     * 那么也要将这个条目是指为最终形态
     * 因为后续的复用中需要使用
     */
    if (changeInfo.newHolder == item) {
        changeInfo.newHolder = null;
    } else if (changeInfo.oldHolder == item) {
        changeInfo.oldHolder = null;
        oldItem = true;
    } else {
        return false;
    }
    item.itemView.setAlpha(1);
    item.itemView.setTranslationX(0);
    item.itemView.setTranslationY(0);
    //结束change中旧条目的调用
    dispatchChangeFinished(item, oldItem);
    return true;
}
@Override
public void endAnimation(RecyclerView.ViewHolder item) {
    final View view = item.itemView;
    /**
     * 调用cancel的时候,会触发对动画设置的监听
     * 从这里可以推测,对于view的动画,都使用ViewPropertyAnimator补间动画实现,
     * 否则调用其cancel无法触发回调;
     *
     * 而在view的cancel回调中,均设置其状态为最终完成状态
     */
    view.animate().cancel();
    /**
     * mPendingMoves 中保存的是需要移动的条目动画
     * 譬如remove动画时本已经上滑但是被强制下滑到原始位置的条目动画
     * 通常在{@link StudyDefaultItemAnimator#runPendingAnimations()} 中执行动画后就会清除
     * 但是如果动画还没有执行,mPendingMoves 还没有清空,那么就直接将这些需要复原的条目进行复原
     */
    for (int i = mPendingMoves.size() - 1; i >= 0; i--) {
        MoveInfo moveInfo = mPendingMoves.get(i);
        if (moveInfo.holder == item) {
            view.setTranslationY(0);
            view.setTranslationX(0);
            dispatchMoveFinished(item);
            mPendingMoves.remove(i);
        }
    }
    /**
     * 清除change方法中产生的需要执行的动画集合
     */
    endChangeAnimation(mPendingChanges, item);
    /**
     *  mPendingRemovals这个集合是待执行的删除的条目的集合
     *  删除之后还需要将其状态设置为初始状态
     *  而初始状态和设置的删除动画有关
     *  譬如删除动画是将alpha设置为0,那么这里就要还原为1
     */
    if (mPendingRemovals.remove(item)) {
        view.setAlpha(1);
        dispatchRemoveFinished(item);
    }
    /**
     * 清除待执行的新增动画
     */
    if (mPendingAdditions.remove(item)) {
        view.setAlpha(1);
        dispatchAddFinished(item);
    }
      /**
       * 清除待执行的change动画
       */
    for (int i = mChangesList.size() - 1; i >= 0; i--) {
        ArrayList<ChangeInfo> changes = mChangesList.get(i);
        endChangeAnimation(changes, item);
        if (changes.isEmpty()) {
            mChangesList.remove(i);
        }
    }
    /**
     * 清除待执行的移动动画
     */
    for (int i = mMovesList.size() - 1; i >= 0; i--) {
        ArrayList<MoveInfo> moves = mMovesList.get(i);
        for (int j = moves.size() - 1; j >= 0; j--) {
            MoveInfo moveInfo = moves.get(j);
            if (moveInfo.holder == item) {
                view.setTranslationY(0);
                view.setTranslationX(0);
                dispatchMoveFinished(item);
                moves.remove(j);
                if (moves.isEmpty()) {
                    mMovesList.remove(i);
                }
                break;
            }
        }
    }
    /**
     * 清除待执行的新增动画
     */
    for (int i = mAdditionsList.size() - 1; i >= 0; i--) {
        ArrayList<RecyclerView.ViewHolder> additions = mAdditionsList.get(i);
        if (additions.remove(item)) {
            view.setAlpha(1);
            dispatchAddFinished(item);
            if (additions.isEmpty()) {
                mAdditionsList.remove(i);
            }
        }
    }
    
    ...
        
    dispatchFinishedWhenDone();
}

其实内容很简单就是清除那些待执行的动画。

接下来看一下animateMove方法

这个方法表示的是当每一个动画执行的时候,相关的条目发生移动的时候(譬如add时条目下移腾出新条目的控件,remove时删除条目下方的条目上移补足空缺):

@Override
public boolean animateMove(final RecyclerView.ViewHolder holder, int fromX, int fromY,
                           int toX, int toY) {
    Log.d(TAG, "animateMove() called holder pos: " + holder.getBindingAdapterPosition());
    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.setTranslationX(-deltaX)view.setTranslationY(-deltaY)将这些本该要移动的条目强制回退到原位,然后把这些移动的信息保存在mPendingMoves列表中,留待runPendingAnimations方法中执行;

接下来看一下runPendingAnimations方法

/**
 * 具体执行动画的方法
 */
@Override
public void runPendingAnimations() {
    Log.d(TAG, "runPendingAnimations() called");
    /**
     * 判断当前是否有延迟执行的动画
     */
    boolean removalsPending = !mPendingRemovals.isEmpty();
    /**
     * 当前是否有待执行的条目移动动画
     */
    boolean movesPending = !mPendingMoves.isEmpty();
    boolean changesPending = !mPendingChanges.isEmpty();
    //通过待执行动画是否为空来判断当前是否需要执行add相关的动画
    boolean additionsPending = !mPendingAdditions.isEmpty();
    if (!removalsPending && !movesPending && !additionsPending && !changesPending) {
        // nothing to animate
        return;
    }
    
    ...
    
    //执行条目移动动画
    if (movesPending) {
        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);
            }
        };
        //如果这个移动是删除相关的,那么就先等删除动画结束后再执行(这里的动画是alpha由1到0)
        if (removalsPending) {
            View view = moves.get(0).holder.itemView;
            ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration());
        } else {
            mover.run();
        }
    }
    
  ...
        
    // 处理新增动画
    if (additionsPending) {
        final ArrayList<RecyclerView.ViewHolder> additions = new ArrayList<>();
        additions.addAll(mPendingAdditions);
        mAdditionsList.add(additions);
        mPendingAdditions.clear();
        Runnable adder = new Runnable() {
            @Override
            public void run() {
                for (RecyclerView.ViewHolder holder : additions) {
                    animateAddImpl(holder);
                }
                additions.clear();
                mAdditionsList.remove(additions);
            }
        };
        //如果当前还有诸如删除更新相关的动画,那么就等待其执行完了再执行新增动画
        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();
        }
    }
}

这里仅仅保留了和add相关的部分,首先判断当前是否有待执行的动画,这里首先处理的move动画,也就是add过程中,那些需要向下移动的条目动画,然后执行add动画,也就是透明度由0到1。可以发现,在所有的的动画中add动画的优先级是最低的,如果有删除移动更新动画,就先delay,然后在执行;

代码就看到这里,后面就可以仿照DefaultItemAnimator进行自定义了

3. 自定义条目动画

add.gif
  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.translationX(changeInfo.toX - changeInfo.fromX - view.getMeasuredWidth());
          oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY);
          oldViewAnim.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);
                  view.setTranslationX(0);
                  view.setTranslationY(0);
                  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);
                  newView.setTranslationX(0);
                  newView.setTranslationY(0);
                  dispatchChangeFinished(changeInfo.newHolder, false);
                  mChangeAnimations.remove(changeInfo.newHolder);
                  dispatchFinishedWhenDone();
              }
          }).start();
      }
  }

实现效果如下:

change.gif

4. 轻松愉快,源码在这里!

上一篇 下一篇

猜你喜欢

热点阅读