注册

RecyclerView 动画原理 | 如何存储并应用动画属性值?(2)

RecyclerView 动画原理 | 如何存储并应用动画属性值?(1)


存预布局动画属性值


InfoRecord中除了postInfo还有一个preInfo,分别表示后布局和预布局表项的动画信息。想必还有一个addToPreLayout()addToPostLayout()对应:


class ViewInfoStore {
// 存储预布局表项与其动画信息
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; // 将后布局表项动画信息存储在 preInfo 字段中
record.flags |= FLAG_PRE; // 追加 FLAG_PRE 到标志位
}
}
复制代码

addToPreLayout()在预布局阶段被调用:


public class RecyclerView {
private void dispatchLayoutStep1() {
...
// 遍历可见表项
int count = mChildHelper.getChildCount();
for (int i = 0; i < count; ++i) {
final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
...
// 构建表项动画信息
final ItemHolderInfo animationInfo = mItemAnimator
.recordPreLayoutInformation(mState, holder,
ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
holder.getUnmodifiedPayloads());
// 将表项动画信息保存到 mViewInfoStore
mViewInfoStore.addToPreLayout(holder, animationInfo);
...
}
...
// 预布局
mLayout.onLayoutChildren(mRecycler, mState);
}
}
复制代码

RecyclerView 布局的第一个阶段中,在第一次执行onLayoutChildren()之前,即预布局之前,遍历了所有的表项并逐个构建动画信息。以 Demo 为例,预布局之前,表项 1、2 的动画信息被构建并且标志位追加了FLAG_PRE,这些信息都被保存到mViewInfoStore实例中。


紧接着RecyclerView执行了onLayoutChildren(),即进行预布局。


public class RecyclerView {
private void dispatchLayoutStep1() {
// 遍历预布局前所有表项
int count = mChildHelper.getChildCount();
for (int i = 0; i < count; ++i) {
final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
...
final ItemHolderInfo animationInfo = mItemAnimator
.recordPreLayoutInformation(mState, holder,
ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
holder.getUnmodifiedPayloads());
mViewInfoStore.addToPreLayout(holder, animationInfo);
...
}
...
// 预布局
mLayout.onLayoutChildren(mRecycler, mState);
// 遍历预布局之后所有的表项
for (int i = 0; i < mChildHelper.getChildCount(); ++i) {
final View child = mChildHelper.getChildAt(i);
final ViewHolder viewHolder = getChildViewHolderInt(child);
...
// 如果 ViewInfoStore 中没有对应的 ViewHolder 信息
if (!mViewInfoStore.isInPreLayout(viewHolder)) {
...
// 构建表项动画信息
final ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation(mState, viewHolder, flags, viewHolder.getUnmodifiedPayloads());
...
// 将表项 ViewHolder 和其动画信息绑定并保存在 mViewInfoStore 中
mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);

}
}
}
}
复制代码

RecyclerView 在预布局之后再次遍历了所有表项。因为预布局会把表项 3 也填充到列表中,所以表项 3 的动画信息也会被存入mViewInfoStore,不过调用的是ViewInfoStore.addToAppearedInPreLayoutHolders()


class ViewInfoStore {
void addToAppearedInPreLayoutHolders(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) {
InfoRecord record = mLayoutHolderMap.get(holder);
if (record == null) {
record = InfoRecord.obtain();
mLayoutHolderMap.put(holder, record);
}
record.flags |= FLAG_APPEAR; // 追加 FLAG_APPEAR 到标志位
record.preInfo = info; // 将预布局表项动画信息存储在 preInfo 字段中
}
}
复制代码

addToAppearedInPreLayoutHolders()addToPreLayout()的实现几乎一摸一样,唯一的不同是,标志位追加了FLAG_APPEAR,用于标记表项 3 是即将出现在屏幕中的表项。


分析至此,可以得出下面的结论:



RecyclerView 经历了预布局、后布局及布局第三阶段后,ViewInfoStore中就记录了每一个参与动画表项的三重信息:预布局位置信息 + 后布局位置信息 + 经历过的布局阶段。



以 Demo 为例,表项 1、2、3 的预布局和后布局位置信息都被记录在ViewInfoStore中,其中表项 1 在预布局和后布局中均出现了,所以标志位中包含了FLAG_PRE | FLAG_POSTInfoRecord中用一个新的常量表示了这种状态FLAG_PRE_AND_POST


class ViewInfoStore {
static class InfoRecord {
static final int FLAG_PRE = 1 << 2;
static final int FLAG_POST = 1 << 3;
static final int FLAG_PRE_AND_POST = FLAG_PRE | FLAG_POST;
}
}
复制代码

而表项 2 只出现在预布局阶段,所以标志位仅包含了FLAG_PRE。表项 3 出现在预布局之后及后布局中,所以标志位中包含了FLAG_APPEAR | FLAG_POST


应用动画属性值


public class RecyclerView {
private void dispatchLayoutStep3() {
// 遍历后布局表项并构建动画信息再存储到 mViewInfoStore
for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
long key = getChangedHolderKey(holder);
final ItemHolderInfo animationInfo = mItemAnimator.recordPostLayoutInformation(mState, holder);
ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key);
mViewInfoStore.addToPostLayout(holder, animationInfo);
}
// 触发表项执行动画
mViewInfoStore.process(mViewInfoProcessCallback);
...
}
}
复制代码

RecyclerView 布局的第三个阶段中,在遍历完后布局表项后,调用了mViewInfoStore.process(mViewInfoProcessCallback)来触发表项执行动画:


class ViewInfoStore {
void process(ProcessCallback callback) {
// 遍历所有参与动画表项的位置信息
for (int index = mLayoutHolderMap.size() - 1; index >= 0; index--) {
// 获取表项 ViewHolder
final RecyclerView.ViewHolder viewHolder = mLayoutHolderMap.keyAt(index);
// 获取与 ViewHolder 对应的动画信息
final InfoRecord record = mLayoutHolderMap.removeAt(index);
// 根据动画信息的标志位确定动画类型以执行对应的 ProcessCallback 回调
if ((record.flags & FLAG_APPEAR_AND_DISAPPEAR) == FLAG_APPEAR_AND_DISAPPEAR) {
callback.unused(viewHolder);
} else if ((record.flags & FLAG_DISAPPEARED) != 0) {
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) {
callback.processAppeared(viewHolder, record.preInfo, record.postInfo);
} else if ((record.flags & FLAG_PRE_AND_POST) == FLAG_PRE_AND_POST) {
callback.processPersistent(viewHolder, record.preInfo, record.postInfo);// 保持
} else if ((record.flags & FLAG_PRE) != 0) {
callback.processDisappeared(viewHolder, record.preInfo, null); // 消失动画
} else if ((record.flags & FLAG_POST) != 0) {
callback.processAppeared(viewHolder, record.preInfo, record.postInfo);// 出现动画
} else if ((record.flags & FLAG_APPEAR) != 0) {
}
// 回收动画信息实例到池中
InfoRecord.recycle(record);
}
}
}
复制代码

ViewInfoStore.process()中遍历了包含所有表项动画信息的mLayoutHolderMap结构,并根据每个表项的标志位来确定执行的动画类型:




  • 表项 1 的标志位为FLAG_PRE_AND_POST所以会命中callback.processPersistent()




  • 表项 2 的标志位中只包含FLAG_PRE,所以(record.flags & FLAG_PRE) != 0成立,callback.processDisappeared()会命中。




  • 表项 3 的标志位中只包含FLAG_APPEAR | FLAG_POST,所以(record.flags & FLAG_APPEAR_PRE_AND_POST) == FLAG_APPEAR_PRE_AND_POST不成立,而(record.flags & FLAG_POST) != 0成立,callback.processAppeared()会命中。




作为参数传入ViewInfoStore.process()ProcessCallback是 RecyclerView 中预定义的动画回调:


class ViewInfoStore {
// 动画回调
interface ProcessCallback {
// 消失动画
void processDisappeared(RecyclerView.ViewHolder viewHolder, RecyclerView.ItemAnimator.ItemHolderInfo preInfo,RecyclerView.ItemAnimator.ItemHolderInfo postInfo);
// 出现动画
void processAppeared(RecyclerView.ViewHolder viewHolder, RecyclerView.ItemAnimator.ItemHolderInfo preInfo,RecyclerView.ItemAnimator.ItemHolderInfo postInfo);
...
}
}

public class RecyclerView {
// RecyclerView 动画回调默认实现
private final ViewInfoStore.ProcessCallback mViewInfoProcessCallback =
new ViewInfoStore.ProcessCallback() {
@Override
public void processDisappeared(ViewHolder viewHolder, ItemHolderInfo info, ItemHolderInfo postInfo) {
mRecycler.unscrapView(viewHolder);
animateDisappearance(viewHolder, info, postInfo);//消失动画
}
@Override
public void processAppeared(ViewHolder viewHolder,ItemHolderInfo preInfo, ItemHolderInfo info) {
animateAppearance(viewHolder, preInfo, info);//出现动画
}
...
};
// 表项动画执行器
ItemAnimator mItemAnimator = new DefaultItemAnimator();
// 出现动画
void animateAppearance(@NonNull ViewHolder itemHolder,ItemHolderInfo preLayoutInfo, ItemHolderInfo postLayoutInfo) {
itemHolder.setIsRecyclable(false);
if (mItemAnimator.animateAppearance(itemHolder, preLayoutInfo, postLayoutInfo)) {
postAnimationRunner();
}
}
// 消失动画
void animateDisappearance(@NonNull ViewHolder holder,ItemHolderInfo preLayoutInfo, ItemHolderInfo postLayoutInfo) {
addAnimatingView(holder);
holder.setIsRecyclable(false);
if (mItemAnimator.animateDisappearance(holder, preLayoutInfo, postLayoutInfo)) {
postAnimationRunner();
}
}
}
复制代码

RecyclerView 执行表项动画的代码结构如下:


if (mItemAnimator.animateXXX(holder, preLayoutInfo, postLayoutInfo)) {
postAnimationRunner();
}
复制代码

根据ItemAnimator.animateXXX()的返回值来决定是否要在下一帧执行动画,以 Demo 中表项 3 的出现动画为例:


public abstract class SimpleItemAnimator extends RecyclerView.ItemAnimator {
@Override
public boolean animateAppearance(RecyclerView.ViewHolder viewHolder,ItemHolderInfo preLayoutInfo, ItemHolderInfo postLayoutInfo) {
// 如果预布局和后布局中表项左上角的坐标有变化 则执行位移动画
if (preLayoutInfo != null
&& (preLayoutInfo.left != postLayoutInfo.left || preLayoutInfo.top != postLayoutInfo.top)) {
// 执行位移动画,并传入动画起点坐标(预布局表项左上角坐标)和终点坐标(后布局表项左上角坐标)
return animateMove(viewHolder,
preLayoutInfo.left,
preLayoutInfo.top,
postLayoutInfo.left,
postLayoutInfo.top);
} else {
return animateAdd(viewHolder);
}
}
}
复制代码

之前存储的表项位置信息,终于在这里被用上了,它作为参数传入animateMove(),这是一个定义在SimpleItemAnimator中的抽象方法,DefaultItemAnimator实现了它:


public class DefaultItemAnimator extends SimpleItemAnimator {
@Override
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);
}
// 将待移动的表项动画包装成 MoveInfo 并存入 mPendingMoves 列表
mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
// 表示在下一帧执行动画
return true;
}
}
复制代码

如果水平或垂直方向的位移增量不为 0,则将待移动的表项动画包装成MoveInfo并存入mPendingMoves列表,然后返回 true,表示在下一帧执行动画:


public class RecyclerView {  
// 出现动画
void animateAppearance(ViewHolder itemHolder,ItemHolderInfo preLayoutInfo, ItemHolderInfo postLayoutInfo) {
itemHolder.setIsRecyclable(false);
if (mItemAnimator.animateAppearance(itemHolder, preLayoutInfo, postLayoutInfo)) {
postAnimationRunner();// 触发动画执行
}
}

// 将动画执行代码抛到 Choreographer 中的动画队列中
void postAnimationRunner() {
if (!mPostedAnimatorRunner && mIsAttached) {
ViewCompat.postOnAnimation(this, mItemAnimatorRunner);
mPostedAnimatorRunner = true;
}
}
// 动画执行代码
private Runnable mItemAnimatorRunner = new Runnable() {
@Override
public void run() {
if (mItemAnimator != null) {
// 在下一帧执行动画
mItemAnimator.runPendingAnimations();
}
mPostedAnimatorRunner = false;
}
};
}
复制代码

通过将一个Runnable抛到Choreographer的动画队列中来触发动画执行,当下一个垂直同步信号到来时,Choreographer会从动画队列中获取待执行的Runnable实例,并将其抛到主线程执行(关于Choreographer的详细解析可以点击读源码长知识 | Android卡顿真的是因为”掉帧“?)。执行的内容定义在ItemAnimator.runPendingAnimations()中:


public class DefaultItemAnimator extends SimpleItemAnimator {
@Override
public void runPendingAnimations() {
// 如果位移动画列表不空,则表示有待执行的位移动画
boolean movesPending = !mPendingMoves.isEmpty();
// 是否有待执行的删除动画
boolean removalsPending = !mPendingRemovals.isEmpty();
...
// 处理位移动画
if (movesPending) {
final ArrayList 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();
}
}
...
}
}
复制代码

遍历mPendingMoves列表,为每一个待执行的位移动画调用animateMoveImpl()构建动画:


public class DefaultItemAnimator extends SimpleItemAnimator {
void animateMoveImpl(final RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) {
final View view = holder.itemView;
final int deltaX = toX - fromX;
final int deltaY = toY - fromY;
if (deltaX != 0) {
view.animate().translationX(0);
}
if (deltaY != 0) {
view.animate().translationY(0);
}

// 获取动画实例
final ViewPropertyAnimator animation = view.animate();
mMoveAnimations.add(holder);
// 设置动画参数并启动
animation.setDuration(getMoveDuration()).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animator) {
dispatchMoveStarting(holder);
}

@Override
public void onAnimationCancel(Animator animator) {
if (deltaX != 0) {
view.setTranslationX(0);
}
if (deltaY != 0) {
view.setTranslationY(0);
}
}

@Override
public void onAnimationEnd(Animator animator) {
animation.setListener(null);
dispatchMoveFinished(holder);
mMoveAnimations.remove(holder);
dispatchFinishedWhenDone();
}
}).start();
}
}
复制代码

原来默认的表项动画是通过ViewPropertyAnimator实现的。


总结



  1. RecyclerView 将表项动画数据封装了两层,依次是ItemHolderInfoInfoRecord,它们记录了列表预布局和后布局表项的位置信息,即表项矩形区域与列表左上角的相对位置,它还用一个int类型的标志位来记录表项经历了哪些布局阶段,以判断表项应该做的动画类型(出现,消失,保持)。
  2. InfoRecord被集中存放在一个商店类ViewInfoStore中。所有参与动画的表项的ViewHolderInfoRecord都会以键值对的形式存储其中。
  3. RecyclerView 在布局的第三阶段会遍历商店类中所有的键值对,以InfoRecord中的标志位为依据,判断执行哪种动画。表项预布局和后布局的位置信息会一并传递给RecyclerView.ItemAnimator,以触发动画。
  4. RecyclerView.ItemAnimator收到动画指令和数据后,又将他们封装为MoveInfo,不同类型的动画被存储在不同的MoveInfo列表中。然后将执行动画的逻辑抛到 Choreographer 的动画队列中,当下一个垂直同步信号到来时,Choreographer 从动画队列中取出并执行表项动画,执行动画即遍历所有的MoveInfo列表,为每一个MoveInfo构建 ViewPropertyAnimator 实例并启动动画。

作者:唐子玄
链接:https://juejin.cn/post/6895523568025600014
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册