Android属性动画源码解析(二)
今天来分析组合动画。1. 创建组合动画
private void doAnim() {
AnimatorSet set = new AnimatorSet();
// 动画1
ValueAnimator floatAnim = ObjectAnimator.ofFloat(0, 1);
floatAnim.setDuration(300);
floatAnim.addUpdateListener(animation -> {
log.dMKV("floatAnim", "val", animation.getAnimatedValue());
});
//动画2
ValueAnimator argb = ObjectAnimator.ofArgb(Color.TRANSPARENT, Color.WHITE);
argb.setDuration(200);
argb.addUpdateListener(animation -> {
log.dMKV("argb", "val", Integer.toHexString((Integer) animation.getAnimatedValue()));
});
//图1
set.playTogether(floatAnim, argb);
//图2
//set.playTogether(argb, floatAnim);
//图3
//set.playSequentially(argb, floatAnim);
set.start();
}
执行结果如下(如下三图执行结果和上面代码一致):
交换playTogether传入值的顺序时,回调顺序也会改变,多个动画可以有不同的duration,playTogether的动画同时开始,但不一定同时结束,从log可以看出argb动画在结束前最后一次回调反倒执行到了floatAnim之前,如果动画调换顺序,argb动画回调不会执行到floatAnim之后。playSequentially按传入动画的顺序一个个执行动画。
AnimatorSet还可以设置Interpolator和duration,但个人感觉不好用,它会在执行后覆盖它管理的所有动画的值,这不利于管理各个动画,但是setTarget很好用,各个动画创建时不用再传入target,可以由AnimatorSet统一设置。AnimatorSet还可以设置设置动画延时执行(setStartDelay)。
组合动画提供了playTogether和playSequentially两种关联方法,AnimatorSet本身也是一个动画,加上setStartDelay已经足以实现任何组合。
注:如果playTogether、playSequentially都只传入一个参数,且该参数之前没有传入过(传入过后,可能已经存在关联关系了,这个较为复杂),那它们都会成为根节点,其父节点都会是Root节点,最后它们都会在Root执行完后开始执行,有点像playTogether传入多个参数。play、playTogether和playSequentially方法传入的第一个参数都会成为Root节点下的子节点(playTogether传入的所有参数都会成为Root子节点)。忽略Root节点,相当于它们都会创建至少一个根节点。
set.playTogether(argb);
set.playSequentially(floatAnim);
2. 组合动画关联关系
public Builder play(Animator anim) {
if (anim != null) {
return new Builder(anim);
}
return null;
}
public void playTogether(Animator... items) {
if (items != null) {
Builder builder = play(items[0]);
for (int i = 1; i < items.length; ++i) {
builder.with(items[i]);
}
}
}
public void playSequentially(Animator... items) {
if (items != null) {
if (items.length == 1) {
play(items[0]);
} else {
for (int i = 0; i < items.length - 1; ++i) {
play(items[i]).before(items[i + 1]);
}
}
}
}
play方法创建了Builder(该建造者不是一个一般意义上的建造者,只是方便链式调用,操作都直接作用到组合动画),传入的第一个动画是当前组合动画的关联核心,关联方法关联关系都是以其为视角,比如before,上一个item.before下一个item,表示item1要执行在item2的前面。每个动画会被封装到一个Node中从名称就可以猜到,这是一个链式结构。
private static class Node implements Cloneable {
//存储直接执行在该动画之后的动画
ArrayList<Node> mChildNodes = null;
//存储Together
ArrayList<Node> mSiblings;
//存储直接执行在当前动画之前的动画
ArrayList<Node> mParents;
//动画开始时间(相对时间)
long mStartTime = 0;
//动画结束时间
long mEndTime = 0;
//动画执行时长
long mTotalDuration = 0;
}
组合动画的链式结构并不是在动画执行过程中遍历链表找到下一个执行动画,而是利用链表生成了一个顺序执行的event表,比遍历要方便的多(这种思路值得学习)。
3. 组合动画执行流程
private void start(boolean inReverse, boolean selfPulse) {
...
initAnimation();
...
if (!isEmptySet) {
startAnimation();
}
notifyStartListeners(inReverse);
...
}
private void initAnimation() {
if (mInterpolator != null) {
for (int i = 0; i < mNodes.size(); i++) {
Node node = mNodes.get(i);
node.mAnimation.setInterpolator(mInterpolator);
}
}
updateAnimatorsDuration();
createDependencyGraph();
}
private void updateAnimatorsDuration() {
if (mDuration >= 0) {
// If the duration was set on this AnimatorSet, pass it along to all child animations
int size = mNodes.size();
for (int i = 0; i < size; i++) {
Node node = mNodes.get(i);
// TODO: don't set the duration of the timing-only nodes created by AnimatorSet to
// insert "play-after" delays
node.mAnimation.setDuration(mDuration);
}
}
mDelayAnim.setDuration(mStartDelay);
}
initAnimation方法首先就遍历所有节点,覆盖插值器,接下来是updateAnimatorsDuration覆盖duration,这里有一个mDelayAnim,这个动画是组合动画第一个执行的动画,是mRootNode,如果没有设置delay,由于duration是0,动画直接就结束。
private void createDependencyGraph() {
...
int size = mNodes.size();
for (int i = 0; i < size; i++) {
mNodes.get(i).mParentsAdded = false;
}
for (int i = 0; i < size; i++) {
Node node = mNodes.get(i);
if (node.mParentsAdded) {
continue;
}
node.mParentsAdded = true;
if (node.mSiblings == null) {
continue;
}
findSiblings(node, node.mSiblings);
node.mSiblings.remove(node);
int siblingSize = node.mSiblings.size();
for (int j = 0; j < siblingSize; j++) {
node.addParents(node.mSiblings.get(j).mParents);
}
for (int j = 0; j < siblingSize; j++) {
Node sibling = node.mSiblings.get(j);
sibling.addParents(node.mParents);
sibling.mParentsAdded = true;
}
}
for (int i = 0; i < size; i++) {
Node node = mNodes.get(i);
if (node != mRootNode && node.mParents == null) {
node.addParent(mRootNode);
}
}
ArrayList<Node> visited = new ArrayList<Node>(mNodes.size());
mRootNode.mStartTime = 0;
mRootNode.mEndTime = mDelayAnim.getDuration();
updatePlayTime(mRootNode, visited);
sortAnimationEvents();
mTotalDuration = mEvents.get(mEvents.size() - 1).getTime();
}
createDependencyGraph相当重要,它生成了mEvents(事件表),让后续动画的执行变得极其丝滑,不用去遍历node表,苦逼的找执行节点,逻辑变得非常简单,而且减少了计算量。
createDependencyGraph前两个循环就干了一件事,将node表重新关联,将其织成一个图,以执行开始且存在with关系的节点为层级分层(每一个play生成的根节点所在的关系网都相互独立),任何一个node都存储了同级所有节点,父层级所有节点,子层级所有节点。第三个循环就是找到没有父节点的节点(调用方期望的第一批执行的动画,play方法传入的根节点动画),将root变成它们的父节点。
updatePlayTime提前计算出各个动画的起止时间并保存到Node中,这里还会将关联关系不合理的动画极其调用链变得不可执行。
private void updatePlayTime(Node parent, ArrayList<Node> visited) {
if (parent.mChildNodes == null) {
if (parent == mRootNode) {
// All the animators are in a cycle
for (int i = 0; i < mNodes.size(); i++) {
Node node = mNodes.get(i);
if (node != mRootNode) {
node.mStartTime = DURATION_INFINITE;
node.mEndTime = DURATION_INFINITE;
}
}
}
return;
}
visited.add(parent);
int childrenSize = parent.mChildNodes.size();
for (int i = 0; i < childrenSize; i++) {
Node child = parent.mChildNodes.get(i);
child.mTotalDuration = child.mAnimation.getTotalDuration();
int index = visited.indexOf(child);
if (index >= 0) {
for (int j = index; j < visited.size(); j++) {
visited.get(j).mLatestParent = null;
visited.get(j).mStartTime = DURATION_INFINITE;
visited.get(j).mEndTime = DURATION_INFINITE;
}
child.mStartTime = DURATION_INFINITE;
child.mEndTime = DURATION_INFINITE;
child.mLatestParent = null;
Log.w(TAG, "Cycle found in AnimatorSet: " + this);
continue;
}
if (child.mStartTime != DURATION_INFINITE) {
if (parent.mEndTime == DURATION_INFINITE) {
child.mLatestParent = parent;
child.mStartTime = DURATION_INFINITE;
child.mEndTime = DURATION_INFINITE;
} else {
if (parent.mEndTime >= child.mStartTime) {
child.mLatestParent = parent;
child.mStartTime = parent.mEndTime;
}
child.mEndTime = child.mTotalDuration == DURATION_INFINITE
? DURATION_INFINITE : child.mStartTime + child.mTotalDuration;
}
}
updatePlayTime(child, visited);
}
visited.remove(parent);
}
上面visited可以理解为一个栈,updatePlayTime是递归调用,parent是后进先出的逻辑。先看第一个if,如果parent没有子节点,那就不用进栈出栈,直接return,如果这个是root,那就有问题了,意味着在createDependencyGraph方法的关系网绑定时,所有root以外的节点都找到了父节点,root无法成为其它动画的父节点,root是动画起点,结束后没有下一个动画继续执行。假设如下图,4的父节点是1,7的父节点是4,1的父节点是7,3的父节点是1,147成了一个环,3也有父节点,直接在if拦截,所有动画时间赋值DURATION_INFINITE(-1,意思是无穷大),动画无法执行。1347代码如下:
// 1 -> 4
set.playSequentially(intAnim, argb);
// 4 -> 7
set.playSequentially(argb, floatAnim);
// 7 -> 1
set.playSequentially(floatAnim, intAnim);
// 1 -> 3
set.playSequentially(intAnim, fourAnim);
if下面的for循环下的第一个if是查找循环结构(定义:查环逻辑),比如如下一个例子,7和3同级,所以它们需要交换父节点,3的父节点是7,所以7将有一个父节点7,7和7产生了环,7是7的子类,当7进栈时,遍历其子节点,遍历到了7,先遍历子节点3(7后进父节点表),由于3没有子节点,它不进栈,再遍历到子节点7,7在栈里面了,所以7将被赋值DURATION_INFINITE。前面7将自己的父节点4给了3,所以3会存在一个调用链,3能正常执行但是执行时间会出现问题,遍历7的子节点时,由于3正常的节点(实际上是它在搞事,它反倒是正常的,哈哈哈),7节点还没有遍历其子节点7,其起止时间是正常的,3也就正常的计算了时间,3的时间是将自己当做7的子节点计算,后面4遍历子节点3时,由于时间已经赋好了(其start大于4的end),不会再次计算,所以最终4和3之间会间隔一个7的时间,7被隐藏了,但是时间却没有变。
// 1 4 7 3
set.playSequentially(intAnim, argb, floatAnim, fourAnim);
// 7 3
set.playTogether(floatAnim, fourAnim);
还可能出现1、2、4的情况,因为1遍历以后会退栈,所以2、4最后也会正常遍历赋值,时间的计算看end更大的那个(if (parent.mEndTime >= child.mStartTime) )。
再来看sortAnimationEvents方法,这个方法将每个动画分为了3个事件,start、delay和end,root Node在AnimatorSet创建阶段就已经放入,所以遍历是从第二个Node开始,最后将root插入,root一定是第一个执行。
这里对event进行了排序,event顺序的期望其实很简单,编舞者回调后,算出执行经过多少时间(unscaledPlayTime),每次计算经过这个时间后,哪些event需要执行,最后一个event的index被记下来,下次计算只计算这个index之后的event,就这样一段又一段的执行直到结束。
这个排序主逻辑是按时间先后顺序,时间先后顺序比较简单,越大的越靠后,DURATION_INFINITE是无穷大,直接往后面扔,它们并不会执行,它们的顺序无所谓。但是这里会有一个问题。sortAnimationEvents之后计算total时间时,直接是取的最后一个event的时间,是-1,所以组合动画尽量不要搞得太花哨,可能会出现问题。
event一定会存在时间相同的event,如果没有delay,start和delay时间就是一样的,不同动画,start可能是一样的,start和end也可能一样。sort将没有执行过程也就是end等于start的动画的三个事件按照end、start、delay的顺序排列,如果直接按照时间升序,action升序的逻辑排列,当存在两个动画的起止时间都一样,时间也都一样时,a0 a1 a2 b0 b1 b2会排序成a0 b0 a1 b1 a2 b2,谷歌工程师可能是觉得不太合适,直接将这种没有执行过程的动画的三个事件集中在一起(主要是start和end集中在一起,delay不一定),且顺序是0,1,2,动画从开始到结束中间不会有间隔。
排序整体上是按【时间升序,事件升序,无执行过程动画集中事件】的逻辑排序。
private void sortAnimationEvents() {
mEvents.clear();
for (int i = 1; i < mNodes.size(); i++) {
Node node = mNodes.get(i);
mEvents.add(new AnimationEvent(node, AnimationEvent.ANIMATION_START));
mEvents.add(new AnimationEvent(node, AnimationEvent.ANIMATION_DELAY_ENDED));
mEvents.add(new AnimationEvent(node, AnimationEvent.ANIMATION_END));
}
mEvents.sort(new Comparator<AnimationEvent>() {
@Override
public int compare(AnimationEvent e1, AnimationEvent e2) {
long t1 = e1.getTime();
long t2 = e2.getTime();
if (t1 == t2) {
if (e2.mEvent + e1.mEvent == AnimationEvent.ANIMATION_START
+ AnimationEvent.ANIMATION_DELAY_ENDED) {
return e1.mEvent - e2.mEvent;
} else {
return e2.mEvent - e1.mEvent;
}
}
if (t2 == DURATION_INFINITE) {
return -1;
}
if (t1 == DURATION_INFINITE) {
return 1;
}
return (int) (t1 - t2);
}
});
int eventSize = mEvents.size();
for (int i = 0; i < eventSize;) {
AnimationEvent event = mEvents.get(i);
if (event.mEvent == AnimationEvent.ANIMATION_END) {
boolean needToSwapStart;
if (event.mNode.mStartTime == event.mNode.mEndTime) {
needToSwapStart = true;
} else if (event.mNode.mEndTime == event.mNode.mStartTime
+ event.mNode.mAnimation.getStartDelay()) {
needToSwapStart = false;
} else {
i++;
continue;
}
int startEventId = eventSize;
int startDelayEndId = eventSize;
for (int j = i + 1; j < eventSize; j++) {
if (startEventId < eventSize && startDelayEndId < eventSize) {
break;
}
if (mEvents.get(j).mNode == event.mNode) {
if (mEvents.get(j).mEvent == AnimationEvent.ANIMATION_START) {
// Found start event
startEventId = j;
} else if (mEvents.get(j).mEvent == AnimationEvent.ANIMATION_DELAY_ENDED) {
startDelayEndId = j;
}
}
}
if (needToSwapStart && startEventId == mEvents.size()) {
throw new UnsupportedOperationException("Something went wrong, no start is"
+ "found after stop for an animation that has the same start and end"
+ "time.");
}
if (startDelayEndId == mEvents.size()) {
throw new UnsupportedOperationException("Something went wrong, no start"
+ "delay end is found after stop for an animation");
}
if (needToSwapStart) {
AnimationEvent startEvent = mEvents.remove(startEventId);
mEvents.add(i, startEvent);
i++;
}
AnimationEvent startDelayEndEvent = mEvents.remove(startDelayEndId);
mEvents.add(i, startDelayEndEvent);
i += 2;
} else {
i++;
}
}
if (!mEvents.isEmpty() && mEvents.get(0).mEvent != AnimationEvent.ANIMATION_START) {
throw new UnsupportedOperationException(
"Sorting went bad, the start event should always be at index 0");
}
// Add AnimatorSet's start delay node to the beginning
mEvents.add(0, new AnimationEvent(mRootNode, AnimationEvent.ANIMATION_START));
mEvents.add(1, new AnimationEvent(mRootNode, AnimationEvent.ANIMATION_DELAY_ENDED));
mEvents.add(2, new AnimationEvent(mRootNode, AnimationEvent.ANIMATION_END));
if (mEvents.get(mEvents.size() - 1).mEvent == AnimationEvent.ANIMATION_START
|| mEvents.get(mEvents.size() - 1).mEvent == AnimationEvent.ANIMATION_DELAY_ENDED) {
throw new UnsupportedOperationException(
"Something went wrong, the last event is not an end event");
}
}
在这里initAnimation再获取了当前动画的totalduration就结束了,之后流程和ValueAnimtor差不多,startAnimation省略绝大部分代码,直接看其遍历event的逻辑
private void startAnimation() {
addAnimationCallback(0);
if (mReversing || mStartDelay == 0 || mSeekState.isActive()) {
int toId = findLatestEventIdForTime(playTime);
handleAnimationEvents(-1, toId, playTime);
for (int i = mPlayingSet.size() - 1; i >= 0; i--) {
if (mPlayingSet.get(i).mEnded) {
mPlayingSet.remove(i);
}
}
mLastEventId = toId;
}
}
private int findLatestEventIdForTime(long currentPlayTime) {
int size = mEvents.size();
int latestId = mLastEventId;
if (mReversing) {
...
} else {
for (int i = mLastEventId + 1; i < size; i++) {
AnimationEvent event = mEvents.get(i);
// TODO: need a function that accounts for infinite duration to compare time
if (event.getTime() != DURATION_INFINITE && event.getTime() <= currentPlayTime) {
latestId = i;
}
}
}
return latestId;
}
private void handleAnimationEvents(int startId, int latestId, long playTime) {
if (mReversing) {
...
} else {
for (int i = startId + 1; i <= latestId; i++) {
AnimationEvent event = mEvents.get(i);
Node node = event.mNode;
if (event.mEvent == AnimationEvent.ANIMATION_START) {
mPlayingSet.add(event.mNode);
if (node.mAnimation.isStarted()) {
node.mAnimation.cancel();
}
node.mEnded = false;
node.mAnimation.startWithoutPulsing(false);
pulseFrame(node, 0);
} else if (event.mEvent == AnimationEvent.ANIMATION_END && !node.mEnded) {
// start event:
pulseFrame(node, getPlayTimeForNode(playTime, node));
}
}
}
}
private void pulseFrame(Node node, long animPlayTime) {
if (!node.mEnded) {
float durationScale = ValueAnimator.getDurationScale();
durationScale = durationScale == 0 ? 1 : durationScale;
node.mEnded = node.mAnimation.pulseAnimationFrame(
(long) (animPlayTime * durationScale));
}
}
// ValueAnimator的方法
boolean pulseAnimationFrame(long frameTime) {
if (mSelfPulse) {
return false;
}
return doAnimationFrame(frameTime);
}
首先通过findLatestEventIdForTime找到当前执行时间下,最后一个需要执行的event的index,交给handleAnimationEvents,startId是上一次处理的最后一个event的index,当前这次编舞者的回调仅仅需要执行这两个index之间的event,然后将这次执行的index记录,收集mPlayingSet(start event)队列,执行pulseFrame,mSelfPulse默认为true,但是在handleAnimationEvents中有调用动画startWithoutPulsing,这个会调用动画的start,将mSelfPulse赋值为false,set中的动画都是由set注册的编舞者回调驱动的,set的doAnimationFrame也有几乎一样的逻辑,只是多了mPlayingSet的处理,代码如下:
public boolean doAnimationFrame(long frameTime) {
if (mFirstFrame < 0) {
mFirstFrame = frameTime;
}
long unscaledPlayTime = (long) ((frameTime - mFirstFrame) / durationScale);
mLastFrameTime = frameTime;
int latestId = findLatestEventIdForTime(unscaledPlayTime);
int startId = mLastEventId;
handleAnimationEvents(startId, latestId, unscaledPlayTime);
mLastEventId = latestId;
// Pump a frame to the on-going animators
for (int i = 0; i < mPlayingSet.size(); i++) {
Node node = mPlayingSet.get(i);
if (!node.mEnded) {
pulseFrame(node, getPlayTimeForNode(unscaledPlayTime, node));
}
}
for (int i = mPlayingSet.size() - 1; i >= 0; i--) {
if (mPlayingSet.get(i).mEnded) {
mPlayingSet.remove(i);
}
}
boolean finished = false;
if (mReversing) {
if (mPlayingSet.size() == 1 && mPlayingSet.get(0) == mRootNode) {
finished = true;
} else if (mPlayingSet.isEmpty() && mLastEventId < 3) {
finished = true;
}
} else {
finished = mPlayingSet.isEmpty() && mLastEventId == mEvents.size() - 1;
}
if (finished) {
endAnimation();
return true;
}
return false;
}
从上面逻辑可以看出,event只会执行一次,但是start到end明显是有一个过程的,这里就需要遍历mPlayingSet来处理,mPlayingSet收集了start event,在动画end之前都会回调doAnimationFrame来驱动动画。
看到这里,set的逻辑就很清晰了,用node封装动画,计算其起止时间,生成start、delay和end三个event,对event进行排序,按顺序遍历event,以set的执行时间来判断当前该执行的动画以及该动画的执行时间。
4. 总结
- 组合动画的动画是以链式结构管理的;
- play、playTogether和playSequentially方法传入的第一个动画(之前未被传入过这个set)都会成为Root动画下的子动画(playTogether传入的所有动画都会是Root的子动画)。忽略Root动画,相当于它们都会创建至少一个根动画;
- set的自动化与直接start不同,它们不需要注册编舞者回调,由set去驱动它们。