RecyclerView 表项动画的属性值是怎么获取的,又存储在哪里?这一篇继续通过 走查源码 的方式解答这个疑问。
通过上两篇的分析得知,为了做动画 RecyclerView 会布局两次:预布局+后布局,依次将动画前与动画后的表项填充到列表。表项被填充后,就确定了它相对于 RecyclerView 左上角的位置,在两次布局过程中,这些位置信息是如何被保存的?
这是 RecyclerView 动画原理的第三篇,系列文章目录如下:
引子
这一篇源码分析还是基于下面这个 Demo 场景:
列表中有两个表项(1、2),删除 2,此时 3 会从屏幕底部平滑地移入并占据原来 2 的位置。
为了实现该效果,RecyclerView
的策略是:为动画前的表项先执行一次pre-layout
,将不可见的表项 3 也加载到布局中,形成一张布局快照(1、2、3)。再为动画后的表项执行一次post-layout
,同样形成一张布局快照(1、3)。比对两张快照中表项 3 的位置,就知道它该如何做动画了。
在此援引上一篇已经得出的结论:
RecyclerView
为了实现表项动画,进行了 2 次布局(预布局 + 后布局),在源码上表现为LayoutManager.onLayoutChildren()
被调用 2 次。State.mInPreLayout
用于标识是否在预布局阶段。预布局的生命周期始于RecyclerView.dispatchLayoutStep1()
,终于RecyclerView.dispatchLayoutStep2()
。- 在预布局阶段,循环填充表项时,若遇到被移除的表项,则会忽略它占用的空间,多余空间被用来加载额外的表项,这些表项在屏幕之外,本来不会被加载。
其中第三点表现在源码上,是这样的:
public class LinearLayoutManager {
// 布局表项
public void onLayoutChildren() {
// 不断填充表项
fill() {
while(列表有剩余空间){
// 填充单个表项
layoutChunk(){
// 让表项成为子视图
addView(view)
}
if (表项没有被移除) {
剩余空间 -= 表项占用空间
}
...
}
}
}
}
这是RecyclerView
填充表项的伪码。以 Demo 为例,预布局阶段,第一次执行onLayoutChildren()
,因表项 2 被删除,所以它占用的空间不会被扣除,导致while
循环多执行一次,这样表项 3 就被填充进列表。
存后布局动画属性值
RecyclerView
用一个 Int 值mLayoutStep
标记布局阶段,它有三种可能的取值。
public class RecyclerView {
public static class State {
static final int STEP_START = 1;
static final int STEP_LAYOUT = 1 << 1;
static final int STEP_ANIMATIONS = 1 << 2; // 布局的动画阶段
int mLayoutStep = STEP_START; // 当前布局阶段
}
}
只要全局查找下mLayoutStep
什么时候被赋值为STEP_ANIMATIONS
,就可以知道表项动画什么时候开始:
public class RecyclerView {
final State mState = new State();
private void dispatchLayoutStep2() {// 布局子表项第二阶段
mState.mInPreLayout = false; // 预布局结束
mLayout.onLayoutChildren(mRecycler, mState); // 开始后布局
mState.mLayoutStep = State.STEP_ANIMATIONS; // 标记为布局的动画阶段
...
}
}
RecyclerView 在后布局结束后,将mState.mLayoutStep
置为State.STEP_ANIMATIONS
,表示表项动画即将开始。
在紧接着的“布局子表项第三阶段”的开头,就断言:
public class RecyclerView {
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
...
dispatchLayout();// 开始布局 RecyclerView 的子表项
...
}
void dispatchLayout() {
...
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();// 布局子表项第一阶段
...
dispatchLayoutStep2(); // 布局子表项第二阶段
}
...
dispatchLayoutStep3(); // 布局子表项第三阶段
}
private void dispatchLayoutStep3() {
// 断言“在布局的动画阶段”
mState.assertLayoutStep(State.STEP_ANIMATIONS);
...
}
public static class State {
// 断言 mLayoutStep 是否为 accepted,否则抛异常
void assertLayoutStep(int accepted) {
if ((accepted & mLayoutStep) == 0) {
throw new IllegalStateException("Layout state should be one of "
+ Integer.toBinaryString(accepted) + " but it is "
+ Integer.toBinaryString(mLayoutStep));
}
}
}
}
由此可以断定,触发动画的逻辑将会出现在RecyclerView.dispatchLayoutStep3()
中,继续往下走读源码:
public class RecyclerView {
private void dispatchLayoutStep3() {
// 遍历表项
for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
// 获取表项对应 ViewHolder
ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
// 获取表项动画信息
final ItemHolderInfo animationInfo = mItemAnimator.recordPostLayoutInformation(mState, holder);
...
}
}
}
RecyclerView 在布局子表项的第三阶段中遍历了当前所有的表项(对于 Demo 场景,会遍历表项 1、3),调用ItemAnimator.recordPostLayoutInformation()
逐个构建表项动画信息ItemHolderInfo
:
public class RecyclerView {
public abstract static class ItemAnimator {
// 记录后布局信息
public ItemHolderInfo recordPostLayoutInformation(State state,ViewHolder viewHolder) {
return obtainHolderInfo().setFrom(viewHolder);
}
// 构建表项信息
public ItemHolderInfo obtainHolderInfo() {
return new ItemHolderInfo();
}
// 表项信息实体类
public static class ItemHolderInfo {
// 上下左右相对于列表的距离
public int left;
public int top;
public int right;
public int bottom;
public ItemHolderInfo setFrom(RecyclerView.ViewHolder holder) {
return setFrom(holder, 0);
}
// 记录表项位置
public ItemHolderInfo setFrom(RecyclerView.ViewHolder holder,int flags) {
final View view = holder.itemView;
this.left = view.getLeft();
this.top = view.getTop();
this.right = view.getRight();
this.bottom = view.getBottom();
return this;
}
}
}
}
构建的ItemHolderInfo
实例记录了表项相对于列表左上角的位置(上下左右),然后调用addToPostLayout()
将其添加到ViewInfoStore
:
public class RecyclerView {
// 用于存放表项动画信息
final ViewInfoStore mViewInfoStore = new ViewInfoStore();
private void dispatchLayoutStep3() {
// 遍历表项
for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
final ItemHolderInfo animationInfo = mItemAnimator.recordPostLayoutInformation(mState, holder);
// 将后布局表项动画信息保存到 mViewInfoStore
mViewInfoStore.addToPostLayout(holder, animationInfo);
...
}
}
}
ViewInfoStore
专门用于存放表项动画信息:
class ViewInfoStore {
// 存放 ViewHolder 与其对应动画信息 的 ArrayMap 结构
final ArrayMap<RecyclerView.ViewHolder, InfoRecord> mLayoutHolderMap = new ArrayMap<>();
// 存储后布局表项与其动画信息
void addToPostLayout(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) {
InfoRecord record = mLayoutHolderMap.get(holder);
if (record == null) {
// 从池中获取 InfoRecord 实例
record = InfoRecord.obtain();
// 将 ViewHolder 和 InfoRecord 绑定
mLayoutHolderMap.put(holder, record);
}
record.postInfo = info; // 将后布局表项动画信息存储在 postInfo 字段中
record.flags |= FLAG_POST; // 追加 FLAG_POST 到标志位
}
static class InfoRecord {
int flags; // 标记位
static final int FLAG_PRE = 1 << 2; // pre-layout 标记
static final int FLAG_POST = 1 << 3; // post-layout 标记
static final int FLAG_APPEAR = 1 << 1; // 表项出现标志
RecyclerView.ItemAnimator.ItemHolderInfo preInfo;// pre-layout 表项位置信息
RecyclerView.ItemAnimator.ItemHolderInfo postInfo;// post-layout 表项位置信息
// 池:为避免内存抖动
static Pools.Pool<InfoRecord> sPool = new Pools.SimplePool<>(20);
// 从池中获取 InfoRecord 实例
static InfoRecord obtain() {
InfoRecord record = sPool.acquire();
return record == null ? new InfoRecord() : record;
}
...
}
}
表项动画信息被包装成InfoRecord
实例并用一个int
类型的标志位来标识表项经历过哪些布局阶段。若表项动画信息是在 post-layout 阶段被添加的,其标志位会追加FLAG_POST
(该标记位用于判断做什么类型的动画)。最后将表项动画信息和对应的 ViewHolder 相互绑定并存储到 ArrayMap 结构中。
至此可以得出如下结论:
RecyclerView 在布局的第三个阶段会遍历后布局中填充的所有表项,为每个表项构建动画信息实例,该实例不仅保存了表项与列表的相对位置,还用一个标记位记录了表项经历过的布局阶段,并将表项与其动画信息的对应关系存储在ViewInfoStore
中的mLayoutHolderMap
结构中。
将结论应用到 Demo 的场景中:列表在布局第三阶段,会遍历表项 1、3,为它们构建动画信息实例,该实例的标志位被追加了FLAG_POST
标志。这些信息都被存储在ViewInfoStore
中的mLayoutHolderMap
结构中。
存预布局动画属性值
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_POST
,InfoRecord
中用一个新的常量表示了这种状态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<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();
}
}
...
}
}
遍历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
实现的。
总结
- RecyclerView 将表项动画数据封装了两层,依次是
ItemHolderInfo
和InfoRecord
,它们记录了列表预布局和后布局表项的位置信息,即表项矩形区域与列表左上角的相对位置,它还用一个int
类型的标志位来记录表项经历了哪些布局阶段,以判断表项应该做的动画类型(出现,消失,保持)。 InfoRecord
被集中存放在一个商店类ViewInfoStore
中。所有参与动画的表项的ViewHolder
与InfoRecord
都会以键值对的形式存储其中。- RecyclerView 在布局的第三阶段会遍历商店类中所有的键值对,以
InfoRecord
中的标志位为依据,判断执行哪种动画。表项预布局和后布局的位置信息会一并传递给RecyclerView.ItemAnimator
,以触发动画。 RecyclerView.ItemAnimator
收到动画指令和数据后,又将他们封装为MoveInfo
,不同类型的动画被存储在不同的MoveInfo
列表中。然后将执行动画的逻辑抛到 Choreographer 的动画队列中,当下一个垂直同步信号到来时,Choreographer 从动画队列中取出并执行表项动画,执行动画即遍历所有的MoveInfo
列表,为每一个MoveInfo
构建 ViewPropertyAnimator 实例并启动动画。
推荐阅读
RecyclerView 系列文章目录如下:
- RecyclerView 缓存机制 | 如何复用表项?
- RecyclerView 缓存机制 | 回收些什么?
- RecyclerView 缓存机制 | 回收到哪去?
- RecyclerView缓存机制 | scrap view 的生命周期
- 读源码长知识 | 更好的RecyclerView点击监听器
- 代理模式应用 | 每当为 RecyclerView 新增类型时就很抓狂
- 更好的 RecyclerView 表项子控件点击监听器
- 更高效地刷新 RecyclerView | DiffUtil二次封装
- 换一个思路,超简单的RecyclerView预加载
- RecyclerView 动画原理 | 换个姿势看源码(pre-layout)
- RecyclerView 动画原理 | pre-layout,post-layout 与 scrap 缓存的关系
- RecyclerView 动画原理 | 如何存储并应用动画属性值?
- RecyclerView 面试题 | 列表滚动时,表项是如何被填充或回收的?
- RecyclerView 面试题 | 哪些情况下表项会被回收到缓存池?
- RecyclerView 性能优化 | 把加载表项耗时减半 (一)
- RecyclerView 性能优化 | 把加载表项耗时减半 (二)
- RecyclerView 性能优化 | 把加载表项耗时减半 (三)
- RecyclerView 的滚动是怎么实现的?(一)| 解锁阅读源码新姿势
- RecyclerView 的滚动时怎么实现的?(二)| Fling
- RecyclerView 刷新列表数据的 notifyDataSetChanged() 为什么是昂贵的?