DailyCast 技术团队博客


  • Home

  • Archives

深入理解Android属性动画的实现(动画启动)-2

Posted on 2017-12-21 | In Android

属性动画的启动分析

在本文中,我们会分析属性动画如何启动的而且和Andoid 黄油计划有什么关系

我们看看 ObjectAnimator.start()

1
2
3
4
5
6
7
8
9
10
11
12
13
public void start() {
AnimationHandler.getInstance().autoCancelBasedOn(this);
if (DBG) {
Log.d(LOG_TAG, "Anim target, duration: " + getTarget() + ", " + getDuration());
for (int i = 0; i < mValues.length; ++i) {
PropertyValuesHolder pvh = mValues[i];
Log.d(LOG_TAG, " Values[" + i + "]: " +
pvh.getPropertyName() + ", " + pvh.mKeyframes.getValue(0) + ", " +
pvh.mKeyframes.getValue(1));
}
}
super.start();
}

AnimationHandler.getInstance().autoCancelBasedOn(this) cancel 相同Target和相同属性的动画
AnimationHandler 实例在线程局部单例。autoCancelBasedOn(this)会遍历 AnimationHandler实例持有的所有未完成的 ValueAnimator实例,cancel 掉符合条件的动画。

紧接着 super.start() 调用了ValueAnimator.start()

1
2
3
4
@Override
public void start() {
start(false);
}

调用了带参数的 start方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void start(boolean playBackwards) {
if (Looper.myLooper() == null) {
throw new AndroidRuntimeException("Animators may only be run on Looper threads");
}
mReversing = playBackwards;

....

mStarted = true;
mPaused = false;
mRunning = false;
mAnimationEndRequested = false;
// Resets mLastFrameTime when start() is called, so that if the animation was running,
// calling start() would put the animation in the
// started-but-not-yet-reached-the-first-frame phase.
mLastFrameTime = 0;
AnimationHandler animationHandler = AnimationHandler.getInstance();
animationHandler.addAnimationFrameCallback(this, (long) (mStartDelay * sDurationScale));

....
}

我们略去与主流程无关的逻辑代码。这个方法标记了动画的状态,如

1
2
3
mStarted = true;
mPaused = false;
mRunning = false;

然后这个方法结束啦,有没有很疑惑?有没有很懵逼?动画怎么执行的?什么时候执行的?
要解答这问题,我们还是要分析 这两行不起眼的代码

1
2
AnimationHandler animationHandler = AnimationHandler.getInstance();
animationHandler.addAnimationFrameCallback(this, (long) (mStartDelay * sDurationScale));

第一行在当前线程获取 AnimationHandler实例;
第二行注册了一个callback。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* Register to get a callback on the next frame after the delay.
*/
public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
if (mAnimationCallbacks.size() == 0) {
getProvider().postFrameCallback(mFrameCallback);
}
if (!mAnimationCallbacks.contains(callback)) {
mAnimationCallbacks.add(callback);
}

if (delay > 0) {
mDelayedCallbackStartTime.put(callback, (SystemClock.uptimeMillis() + delay));
}
}
```
***注释说明:callback 会在delay 时间后的下一个 frame 获得回调。***

```java
if (mAnimationCallbacks.size() == 0) {
getProvider().postFrameCallback(mFrameCallback);
}

这两行代码 做了一件事情:保证一个ValueAnimator对象只向Provider注册一次 mFrameCallback

瞅瞅 getProvider是啥?

1
2
3
4
5
6
private AnimationFrameCallbackProvider getProvider() {
if (mProvider == null) {
mProvider = new MyFrameCallbackProvider();
}
return mProvider;
}

创建了一个 MyFrameCallbackProvider实例, MyFrameCallbackProvider 继承 AnimationFrameCallbackProvider

1
2
3
4
5
6
7
public interface AnimationFrameCallbackProvider {
void postFrameCallback(Choreographer.FrameCallback callback);
void postCommitCallback(Runnable runnable);
long getFrameTime();
long getFrameDelay();
void setFrameDelay(long delay);
}

AnimationFrameCallbackProvider接口定义了一些回调接口,按照注释说明主要作用是 提高 ValueAnimator的可测性,通过这个接口隔离,我们可以自定义 定时脉冲,而不用使用系统默认的 Choreographer,这样我们可以在测试中使用任意的时间间隔的定时脉冲.既然可以方便测试,那肯定有API来更改Provider 吧?

果不其然,我们猜对了。

1
2
3
4
5
6
7
public void setProvider(AnimationFrameCallbackProvider provider) {
if (provider == null) {
mProvider = new MyFrameCallbackProvider();
} else {
mProvider = provider;
}
}

ValueAnimator提供了一个 setProvider 通过自定义的Provider 提供我们想要的任意时间间隔的回调,来更新动画。

明白了 接口AnimationFrameCallbackProvider的作用,也知道了一个新的名词Choreographer,它就是 Android 黄油计划 的核心。使用vsync(垂直同步)来协调View的绘制和动画的执行间隔。关于 Choreographer 在文章最后会做进一步解释
我们知道了默认情况下系统使用Choreographer,我们可以简单的认为 它是一个与 绘制和动画有关的 消息处理器 。
继续我们的 代码 AnimationHandler.addAnimationFrameCallback

1
2
3
4
5
6
7
8
9
10
11
12
public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
if (mAnimationCallbacks.size() == 0) {
getProvider().postFrameCallback(mFrameCallback);
}
if (!mAnimationCallbacks.contains(callback)) {
mAnimationCallbacks.add(callback);
}

if (delay > 0) {
mDelayedCallbackStartTime.put(callback, (SystemClock.uptimeMillis() + delay));
}
}

执行了 getProvider().postFrameCallback(mFrameCallback) 通过上面的分析我们知道 getProvider() 得到的是一个MyFrameCallbackProvider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* Default provider of timing pulse that uses Choreographer for frame callbacks.
*/
private class MyFrameCallbackProvider implements AnimationFrameCallbackProvider {

final Choreographer mChoreographer = Choreographer.getInstance();

@Override
public void postFrameCallback(Choreographer.FrameCallback callback) {
mChoreographer.postFrameCallback(callback);
}

@Override
public void postCommitCallback(Runnable runnable) {
mChoreographer.postCallback(Choreographer.CALLBACK_COMMIT, runnable, null);
}

@Override
public long getFrameTime() {
return mChoreographer.getFrameTime();
}

@Override
public long getFrameDelay() {
return Choreographer.getFrameDelay();
}

@Override
public void setFrameDelay(long delay) {
Choreographer.setFrameDelay(delay);
}
}

注释说明系统默认使用的 Choreographer做定时脉冲来协调 frame 的更新 。

MyFrameCallbackProvider.postFrameCallback()方法调用了mChoreographer.postFrameCallback(callback) 这里说明一下 Choreographer 实例也是线程局部单例的。从这些信息中我们知道了动画可以在子线程中执行的(注意:这不意味着可以在子线程更新UI),但是这个子线程必须有Looper。
接着分析 Choreographer.postFrameCallback方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public void postFrameCallback(FrameCallback callback) {
postFrameCallbackDelayed(callback, 0);
}

public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
if (callback == null) {
throw new IllegalArgumentException("callback must not be null");
}

postCallbackDelayedInternal(CALLBACK_ANIMATION,
callback, FRAME_CALLBACK_TOKEN, delayMillis);
}

private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
....
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
if (dueTime <= now) {
scheduleFrameLocked(now);
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}
```
动画使用的 `callbackType`为 `CALLBACK_ANIMATION`,而默认支持 三种类型:

`CALLBACK_INPUT`:处理输出相关的回调,最先被处理
`CALLBACK_ANIMATION`:处理动画相关的回调,在 `CALLBACK_TRAVERSAL`类型之前处理
`CALLBACK_TRAVERSAL`: 处理View 的layout 和 draw。该类型在所有其他异步消息处理完后开始处理,也基于这一点保证了界面不卡顿。

每一个类型的 `callbackType` 都拥有一个`CallbackQueue` 我们的动画callback 会通过 `mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);` 保存在 `CallbackQueue`中成为一个链表中的一个节点。
处理完 callback 之后 下面会执行`scheduleFrameLocked()`

```java
private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
if (USE_VSYNC) {
...
if (isRunningOnLooperThreadLocked()) {
scheduleVsyncLocked();
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);
mHandler.sendMessageAtFrontOfQueue(msg);
}
} else {
final long nextFrameTime = Math.max(
mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, nextFrameTime);
}
}
}

默认情况下USE_VSYNC为true,当然设备厂商也可以设置为 false。如果 为false 会以 10 ms 为间隔计算下一次 doFrame 的时间,然后使用Handler来处理。

我们看看 scheduleVsyncLocked() 这行代码做了何事?

1
2
3
private void scheduleVsyncLocked() {
mDisplayEventReceiver.scheduleVsync();
}

mDisplayEventReceiver 当USE_VSYNC为true 时 实例化成 FrameDisplayEventReceiver对象 ,它主要是和 native层 做交互,协调 vsync 信号。
mDisplayEventReceiver.scheduleVsync() 请求当下一帧开始时同步vsync信号。

当vsync 信号来时 回调 JNI 层会回调DisplayEventReceiver.dispatchVsync方法

1
2
3
4
5
6
7
8
// Called from native code.
@SuppressWarnings("unused")
private void dispatchVsync(long timestampNanos, int builtInDisplayId, int frame) {
onVsync(timestampNanos, builtInDisplayId, frame);
}

public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
}

FrameDisplayEventReceiver实现了doVsync方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {
private boolean mHavePendingVsync;
private long mTimestampNanos;
private int mFrame;

public FrameDisplayEventReceiver(Looper looper) {
super(looper);
}

@Override
public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
....

long now = System.nanoTime();
if (timestampNanos > now) {
....
timestampNanos = now;
}

if (mHavePendingVsync) {
....log
} else {
mHavePendingVsync = true;
}

mTimestampNanos = timestampNanos;
mFrame = frame;
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}

@Override
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame);
}
}

主要实现:根据JNI层的 timestampNanos纳秒值计算成毫秒,通过 Handler 执行
Runable,即执行了doFrame(mTimestampNanos, mFrame);这行代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
 void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
if (!mFrameScheduled) {
return; // no work to do
}

long intendedFrameTimeNanos = frameTimeNanos;
startNanos = System.nanoTime();
final long jitterNanos = startNanos - frameTimeNanos;
if (jitterNanos >= mFrameIntervalNanos) {
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
....
}
final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
....
frameTimeNanos = startNanos - lastFrameOffset;
}

if (frameTimeNanos < mLastFrameTimeNanos) {
....
scheduleVsyncLocked();
return;
}

mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
mFrameScheduled = false;
mLastFrameTimeNanos = frameTimeNanos;
}

try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);

mFrameInfo.markInputHandlingStart();
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);

mFrameInfo.markAnimationsStart();
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);

mFrameInfo.markPerformTraversalsStart();
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);

doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
} finally {
AnimationUtils.unlockAnimationClock();
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
.....
}

与动画相关的关键代码:

1
2
mFrameInfo.markAnimationsStart();
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);

以下是 doCallbacks的实现,有没有感觉即将看见胜利的曙光?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void doCallbacks(int callbackType, long frameTimeNanos) {
CallbackRecord callbacks;
synchronized (mLock) {
final long now = System.nanoTime();
//取出可以执行的CallbackRecord 链表。
callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(
now / TimeUtils.NANOS_PER_MS);
if (callbacks == null) {
return;
}
mCallbacksRunning = true;
....略去与动画无关的代码
}
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, CALLBACK_TRACE_TITLES[callbackType]);
for (CallbackRecord c = callbacks; c != null; c = c.next) {
.... log
//循环执行链表
c.run(frameTimeNanos);
}
} finally {
synchronized (mLock) {
mCallbacksRunning = false;
do {
final CallbackRecord next = callbacks.next;
recycleCallbackLocked(callbacks);
callbacks = next;
} while (callbacks != null);
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}

所以该方法最核心的功能是找到需要执行的 CallbackRecord链表,然后循环执行 它们的 run方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static final class CallbackRecord {
public CallbackRecord next;
public long dueTime;
public Object action; // Runnable or FrameCallback
public Object token;

public void run(long frameTimeNanos) {
if (token == FRAME_CALLBACK_TOKEN) {
((FrameCallback)action).doFrame(frameTimeNanos);
} else {
((Runnable)action).run();
}
}
}

在前面 注册回调postFrameCallbackDelayed时 token 是 FRAME_CALLBACK_TOKEN 所以执行 run 方法中的第一个if 分支((FrameCallback)action).doFrame(frameTimeNanos);
回调 FrameCallback.doFrame(frameTimeNanos) 方法。
下面是 Choreographer.FrameCallback 的实现

1
2
3
4
5
6
7
8
9
private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
doAnimationFrame(getProvider().getFrameTime());
if (mAnimationCallbacks.size() > 0) {
getProvider().postFrameCallback(this);
}
}
};

看到doAnimationFrame(getProvider().getFrameTime());这一行代码,我们已经确认,我们的动画逻辑即将开始了。
我们先搁置doAnimationFrame这行代码,先分析完 doFrame 回调。

1
2
3
if (mAnimationCallbacks.size() > 0) {
getProvider().postFrameCallback(this);
}

这里如果 mAnimationCallbacks.size >0会再次将该 callback 注册给 Choreographer。为什么还要注册一次呢?之前不是注册过一次了吗?难道Choreographer把这个callback 释放了。
我们回到Choreographer.doCallbacks方法。
有一段 final 括号体中的代码我们没有分析

1
2
3
4
5
6
7
8
9
10
11
 finally {
synchronized (mLock) {
mCallbacksRunning = false;
do {
final CallbackRecord next = callbacks.next;
recycleCallbackLocked(callbacks);
callbacks = next;
} while (callbacks != null);
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}

这里调用了recycleCallbackLocked(callbacks);来回收整个链表中的节点记录。

1
2
3
4
5
6
private void recycleCallbackLocked(CallbackRecord callback) {
callback.action = null;
callback.token = null;
callback.next = mCallbackPool;
mCallbackPool = callback;
}

当 mAnimationCallbacks.size >0时 需要再次添加回调,以便来获取下一帧的回调。
当动画 paused 或 end 后 mAnimationCallbacks 会相应的remove callback。

小结1

到目前为止 我们知道了当调用 ObjectAnimator.start()后:

  1. 取消之前的动画对相同属性,相同target的动画,防止出现多个动画同时更新 Target 的属性,出现错乱。不过这一行为默认是关闭的,设置ObjectAnimator.setAutoCancel(true)来打开;
  2. 执行 ValueAnimator.start()方法;
  3. AnimationHandler.addAnimationFrameCallback向Choreographer注册Choreographer.FrameCallback回调,通过该回调获得渲染时间脉冲的回调;
  4. 通过系统的vsync垂直同步信号来协调 cpu,gpu 和渲染的时序;
  5. Choreographer 获得 vsync信号后 根据 当前帧的纳秒来查找哪些 Choreographer.FrameCallback会被执行。
  6. 执行AnimationHandler.doAnimationFrame()方法,开始真正的动画逻辑。

动画属性更新

刚才我们分析到 AnimationHandler.doAnimationFrame() 方法,现在看看这个方法的功能是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void doAnimationFrame(long frameTime) {
int size = mAnimationCallbacks.size();
long currentTime = SystemClock.uptimeMillis();
for (int i = 0; i < size; i++) {
final AnimationFrameCallback callback = mAnimationCallbacks.get(i);
if (callback == null) {
continue;
}
if (isCallbackDue(callback, currentTime)) {
callback.doAnimationFrame(frameTime);
if (mCommitCallbacks.contains(callback)) {
getProvider().postCommitCallback(new Runnable() {
@Override
public void run() {
commitAnimationFrame(callback, getProvider().getFrameTime());
}
});
}
}
}//for
cleanUpList();
}

该方法主要作用是遍历所有的 AnimationFrameCallback。 这个AnimationFrameCallback 是什么?什么时候添加到mAnimationCallbacks的?
刚才分析Choreographer.FrameCallback.doFrame时提到过,在动画paused 或 end 时会将AnimationFrameCallback 从mAnimationCallbacks中移除,如果mAnimationCallbacks的size 为0 就不再向Choreographer注册 callback。也就代表没有动画要被执行了。
其实 AnimationFrameCallback就是 ObjectAnimator或ValueAnimaor本身。我们看看 ValueAnimator的定义:

1
2
public class ValueAnimator extends Animator implements AnimationHandler.AnimationFrameCallback {
}

所以 这些AnimationFrameCallback就是那些待执行的 属性动画。
接下来看看isCallbackDue(callback, currentTime)这个 if 判断:

1
2
3
4
5
6
7
8
9
10
11
private boolean isCallbackDue(AnimationFrameCallback callback, long currentTime) {
Long startTime = mDelayedCallbackStartTime.get(callback);
if (startTime == null) {
return true;
}
if (startTime < currentTime) {
mDelayedCallbackStartTime.remove(callback);
return true;
}
return false;
}

根据 当前的的 纳秒时间 判断 动画是否需要执行,因为有些动画做了delay可能在当前的frame 窗口不需要执行。
callback.doAnimationFrame(frameTime); 这行代码 开始回调 ValueAnimator.doAnimationFrame方法。然后判断if (mCommitCallbacks.contains(callback)) 为 true的话会再次向Choreographer注册一个Runnable的callback,当下一个 frame 时间到来时 执行 Runnable。

我们关注重点代码callback.doAnimationFrame(frameTime) 也即是ValueAnimator.doAnimationFrame(frameTime)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public final void doAnimationFrame(long frameTime) {
AnimationHandler handler = AnimationHandler.getInstance();
if (mLastFrameTime == 0) {
// First frame
//如果是动画的第一次回调,注册调整到下一个 frame 窗口 再执行。
handler.addOneShotCommitCallback(this);
if (mStartDelay > 0) {
startAnimation();
}
if (mSeekFraction < 0) {
mStartTime = frameTime;
} else {
long seekTime = (long) (getScaledDuration() * mSeekFraction);
mStartTime = frameTime - seekTime;
mSeekFraction = -1;
}
mStartTimeCommitted = false; // allow start time to be compensated for jank
}
mLastFrameTime = frameTime;
if (mPaused) {
mPauseTime = frameTime;
handler.removeCallback(this);
return;
} else if (mResumed) {
mResumed = false;
if (mPauseTime > 0) {
// Offset by the duration that the animation was paused
mStartTime += (frameTime - mPauseTime);
mStartTimeCommitted = false; // allow start time to be compensated for jank
}
handler.addOneShotCommitCallback(this);
}
... comments
final long currentTime = Math.max(frameTime, mStartTime);
boolean finished = animateBasedOnTime(currentTime);
if (finished) {
endAnimation();
}
}

当时第一帧动画时 会调整到下一个 frame 窗口执行。如果这个动画是 delay 动画,会执行startAnimation()初始化动画,标记动画正在 mRunning,并且 对PropertyValuesHolder执行初始化操作–主要就是初始化估值器。
下面分析 boolean finished = animateBasedOnTime(currentTime);
返回值 finished标记是否动画已经执行完毕。如果最后一个关键帧(Keyframe)执行完毕,这里返回true,会执行endAnimation()做一些状态位复位和动画结束回调等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
boolean animateBasedOnTime(long currentTime) {
boolean done = false;
if (mRunning) {
final long scaledDuration = getScaledDuration();
final float fraction = scaledDuration > 0 ?
(float)(currentTime - mStartTime) / scaledDuration : 1f;
final float lastFraction = mOverallFraction;
final boolean newIteration = (int) fraction > (int) lastFraction;
final boolean lastIterationFinished = (fraction >= mRepeatCount + 1) &&
(mRepeatCount != INFINITE);
if (scaledDuration == 0) {
// 0 duration animator, ignore the repeat count and skip to the end
done = true;
} else if (newIteration && !lastIterationFinished) {
// Time to repeat
if (mListeners != null) {
int numListeners = mListeners.size();
for (int i = 0; i < numListeners; ++i) {
mListeners.get(i).onAnimationRepeat(this);
}
}
} else if (lastIterationFinished) {
done = true;
}
mOverallFraction = clampFraction(fraction);
float currentIterationFraction = getCurrentIterationFraction(mOverallFraction);
animateValue(currentIterationFraction);
}
return done;
}

currentTime是Choreographer发出的计时脉冲时间,纳秒计时。
根据 currentTime 计算 fraction系数,即动画时间流逝比。
然后执行animateValue(currentIterationFraction) 计算动画的在当前时间比例下属性动画的值,如果是 ObjectAnimator还会降属性值设置该Target。
animateValue方法被 ObjectAnimator 重载了。我们先分析ValueAnimator. animateValue(fraction) 然后再分析ObjectAnimator. animateValue(fraction)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void animateValue(float fraction) {
fraction = mInterpolator.getInterpolation(fraction);
mCurrentFraction = fraction;
int numValues = mValues.length;
for (int i = 0; i < numValues; ++i) {
mValues[i].calculateValue(fraction);
}
if (mUpdateListeners != null) {
int numListeners = mUpdateListeners.size();
for (int i = 0; i < numListeners; ++i) {
mUpdateListeners.get(i).onAnimationUpdate(this);
}
}
}

参数 fraction这个时间流逝比系数 是线性的。通过mInterpolator.getInterpolation()计算出我们想要的fraction。然后使用这个系数计算 PropertyValuesHolder.calculateValue(fraction)。
计算完 属性值后 执行 mUpdateListeners的更新操作。到目前为止我们终于知道我们经常使用的 AnimatorUpdateListener.onAnimationUpdate()何时执行的了。

我们再分析 被 ObjectAnimator重载后的animateValue(fraction)方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void animateValue(float fraction) {
final Object target = getTarget();
if (mTarget != null && target == null) {
// We lost the target reference, cancel and clean up. Note: we allow null target if the
/// target has never been set.
cancel();
return;
}

super.animateValue(fraction);
int numValues = mValues.length;
for (int i = 0; i < numValues; ++i) {
mValues[i].setAnimatedValue(target);
}
}

主要功能是 通过调用 super.animateValue()计算属性的值。然后调用PropertyValuesHolder.setAnimatedValue(Object)来更新属性值到对应的Target上。

深入理解Android属性动画的实现-1 这篇文章中介绍过 PropertyValuesHolder具有更新属性的能力,也持有关键帧的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void setAnimatedValue(Object target) {
if (mProperty != null) {
mProperty.set(target, getAnimatedValue());
}
if (mSetter != null) {
try {
mTmpValueArray[0] = getAnimatedValue();
mSetter.invoke(target, mTmpValueArray);
} catch (InvocationTargetException e) {
Log.e("PropertyValuesHolder", e.toString());
} catch (IllegalAccessException e) {
Log.e("PropertyValuesHolder", e.toString());
}
}
}

通过 Property更新属性值。如果之前是通过 propertyName来初始化的动画,这里通过 mSetter来反射调用 set 方法,更新属性值。

深入理解Android属性动画的实现-1 这篇文章还介绍了如何通过fraction计算动画的属性值。这里就不在赘述。

总结

到目前位置我们已经分析完 属性动画的创建—> 属性动画的启动 –> 属性的计算和更新。
这里我们再回顾一下:

  1. ObjectAnimator.start()开始启动动画;
  2. 向Choreographer注册callback;
  3. Choreographer获得vsync垂直同步信号量后回调Choreographer.FrameCallback.doFrame()执行逻辑进入到AnimationHandler中;
  4. AnimationHandler持有 AnimationFrameCallback也即是ValueAnimator,然后执行ValueAnimator.doAnimationFrame(time);
  5. ValueAnimator.animateBasedOnTime(time)执行,通过 TimeInterpolator计算最终的 时间流逝比fraction,然后调用PropertyValuesHolder.calculateValue(fraction)计算属性的值,并回调 AnimatorUpdateListener.onAnimationUpdate()方法。
  6. PropertyValuesHolder调用Keyframes.getIntValue(fraction),这中间又使用到估值器TypeEvaluator和Keyframe最终结算处我们需要的属性值。
  7. 然后ObjectAnimator调用PropertyValuesHolder.setAnimatedValue(target)来更新 target的属性值。

附加知识点

文章中提到过Choreographer和Android黄油计划。其实在本文的流程分析中已经简单分析了 Choreographer在动画类型上的执行流程:

  1. 创建DisplayEventReceiver的子类FrameDisplayEventReceiver来与JNI 层交互。JNI层的 vsync信号量通过callback 这个类的 dispatchVsync方法来告诉应用层 可以开始新的一帧的渲染了。
  2. Choreographer接收到 vsync信号后执行doFrame(frameTimeNanos,frame)方法,对三个支持的类型CALLBACK_INPUT,CALLBACK_ANIMATION,CALLBACK_TRAVERSAL做相应的回调。
    需要说明下: _源码中还有一个类型CALLBACK_COMMIT主要处理注册 commit Runnable的需求,即延迟一个frame 的需求。因为它是一种业务辅助,不像其它三种,有明显的业务支持。所以在本文中倾向说三种支持类型,请知悉_。
  3. 回调进入Choreographer.FrameCallback.doFrame(timeNanos)
  4. 然后进入到业务层,例如 :
    CALLBACK_ANIMATION类型进入到AnimationHandler.mFrameCallback;
    CALLBACK_TRAVERSAL类型进入到ViewRootImpl 执行scheduleTraversals() 进而执行了View.layout,View.measure,View.draw方法开启View的渲染操作; CALLBACK_INPUT这个类型相比前两个特殊一些,因为输入事件由另一个引擎负责。让输入引擎产生输入事件后不是立刻在 视图层产生响应。而是要等待下一个 vsync垂直同步信号,跟着统一的时钟脉冲来响应。所以 在ViewRootImpl中会使用到mChoreographer.postCallback(Choreographer.CALLBACK_INPUT,mConsumedBatchedInputRunnable, null)。具体这里不再展开描述。

简言之,Choreographer就是一个消息处理器,根据 vsync垂直同步信号 来处理三种支持类型的回调。

至于 Android黄油计划(Project Butter) Choreographer只是其中一个重要的特性。还有其他方便的优化。例如 引入了三重缓冲和 JNI层的vsync。至于 vsync的好处以及和 该计划之前 的Android的渲染相比 请参考Android 4.4 Graphic系统详解(2) VSYNC的生成 这篇优质文章。在这里也对该篇文章的愿作者致敬。

深入理解Android属性动画的实现-1

Posted on 2017-11-26 | In Android

导读

在文章开头我们先简单回忆一下常用的属性动画 API 有哪些?
1.动画相关的类:
ObjectAnimator,ValueAnimator,Property,PropertyValuesHolder,Keyframe,Keyframes,TimeInterpolator,TypeEvaluator
在剖析过程中我们会解析相关类的作用和实现方式
2.常用的 属性动画 方法:

1
2
3
4
5
ObjectAnimator.ofInt()
ObjectAnimator.ofFloat()
ObjectAnimator.ofArgb()
ObjectAnimator.ofObject()
ObjectAnimator.ofPropertyValuesHolder()

下面我们以一个常用的方法 ObjectAnimator.ofInt() 为切入点层层深入解析属性动画的创建过程。

动画的创建

早期版本的属性动画 ofInt 方法有两个实现,到Android O 为止 已经有 4 个实现了。
主要就是增加了 Path 参数。我们这里并不去解析新方法,因为它的实现方式和最基本的方法相同。

1
2
3
4
5
6
7
8
9
10
public static ObjectAnimator ofInt(Object target, String propertyName, int... values) {
ObjectAnimator anim = new ObjectAnimator(target, propertyName);
anim.setIntValues(values);
return anim;
}
public static <T> ObjectAnimator ofInt(T target, Property<T, Integer> property, int... values) {
ObjectAnimator anim = new ObjectAnimator(target, property);
anim.setIntValues(values);
return anim;
}

注意:我这里以Android-25 源码为代码源。
上述代码中ofInt 方法的第一行均是 创建 ObjectAnimator 对象。但分别使用了两种构造方式。
下面我们探索一下 ObjectAnimator 的构造方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
//无参构造
public ObjectAnimator() {
}
//指定 propertyName
private ObjectAnimator(Object target, String propertyName) {
setTarget(target);
setPropertyName(propertyName);
}
//指定 Property
private <T> ObjectAnimator(T target, Property<T, ?> property) {
setTarget(target);
setProperty(property);
}

无参构造方法没有什么可说的。另外两个构造方法第一行代码都是

1
setTarget(target);

该setTarget方法的作用是记录需要做动画的对象,方便以后对该对象的相关属性做更改。

1.我们首先看看 propertyName 参数的构造方法的setPropertyName 方法

1
2
3
4
5
6
7
8
9
10
11
public void setPropertyName(@NonNull String propertyName) {
if (mValues != null) {
PropertyValuesHolder valuesHolder = mValues[0];
String oldName = valuesHolder.getPropertyName();
valuesHolder.setPropertyName(propertyName);
mValuesMap.remove(oldName);
mValuesMap.put(propertyName, valuesHolder);
}
mPropertyName = propertyName;
mInitialized = false;
}

我们这里的调用情形,mValues肯定为null,不会走 if分支,剩下的就是简单的记录我们设置的propertyName用mPropertyName保持。如果mValues不为空的情况下,会更新 mValues的第一个元素的属性值,并更新对应的map集合
2.我们再看看Property参数的构造方法的setProperty方法,同样也是简单的记录mProperty和mPropertyName
那么 这个Property是何方神圣?

Property 解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public abstract class Property<T, V> {
private final String mName;
private final Class<V> mType;
public static <T, V> Property<T, V> of(Class<T> hostType, Class<V> valueType, String name) {
return new ReflectiveProperty<T, V>(hostType, valueType, name);
}
public Property(Class<V> type, String name) {
mName = name;
mType = type;
}
public boolean isReadOnly() {
return false;
}
public void set(T object, V value) {
throw new UnsupportedOperationException("Property " + getName() +" is read-only");
}
public abstract V get(T object);
public String getName() {
return mName;
}
public Class<V> getType() {
return mType;
}
}

这是一个 abstract修饰的类。这个类很简单,就是定义了一些需要被子类实现的方法。(为了减小博客的字数,我这里隐去源码中的相关注释,如有需要请自行到源码中查看)
通过对注释的理解我们知道 这个类的注意功能就是代表需要动画的属性,实现这个类有一些先决条件需要满足:
必须满足一下三个条件中的一个
1.被 Property 代表的类的属性必须有一个public的无参的get方法,和一个可选的set方法,set方法的参数和get方法的返回值一个类型
2.有一个public的无参的is 方法,和一个可选的set方法 set方法的参数和is方法的返回值一个类型
3.一个public的 属性 name。
假如有 get/is方法,但是没有set方法,那么这个 Property 是一个只读的属性。
set 方法必须实现,都在抛出异常
到目前为止我们了解了 动画初始化时会初始化 mPropertyName变量,记录属性的名称;或初始化mProperty,来保持对属性的访问能力和更新能力。
这里说明一下,常规情况下需要满足这三个中一个即可,如果我们又其他方式来变量属性值,而且也在 Property 中做了相应的映射关系,不满足也可以。这个在之后文章中自定义属性动画中可以看出
我们回到之前的 ofInt方法
紧接着 执行了 代码

1
anim.setIntValues(values);

这行代码看似简单,其实包含了很多信息。从字面意思理解,就是保存动画的属性值范围。

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public void setIntValues(int... values) {
if (mValues == null || mValues.length == 0) {
if (mProperty != null) {
setValues(PropertyValuesHolder.ofInt(mProperty, values));
} else {
setValues(PropertyValuesHolder.ofInt(mPropertyName, values));
}
} else {
super.setIntValues(values);
}
}

第一次调用 mValues为 null走入if流程,假如我们调用的 propertyName参数的方法,会执行setValues(PropertyValuesHolder.ofInt(mPropertyName, values));,反之执行setValues(PropertyValuesHolder.ofInt(mProperty, values));
我们先看看PropertyValuesHolder.ofInt(mProperty, values)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 public static PropertyValuesHolder ofInt(Property<?, Integer> property, int... values) {
return new IntPropertyValuesHolder(property, values);
}

public IntPropertyValuesHolder(Property property, int... values) {
super(property);
setIntValues(values);
if (property instanceof IntProperty) {
mIntProperty = (IntProperty) mProperty;
}
}
@Override
public void setIntValues(int... values) {
super.setIntValues(values);
mIntKeyframes = (Keyframes.IntKeyframes) mKeyframes;
}
public void setIntValues(int... values) {
mValueType = int.class;
mKeyframes = KeyframeSet.ofInt(values);
}

该方法主要的作用就是创建一个PropertyValuesHolder,保持属性Property
并将 属性值保存成关键帧,我们关注核心如何生成关键帧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static KeyframeSet ofInt(int... values) {
int numKeyframes = values.length;
IntKeyframe keyframes[] = new IntKeyframe[Math.max(numKeyframes,2)];
if (numKeyframes == 1) {
keyframes[0] = (IntKeyframe) Keyframe.ofInt(0f);
keyframes[1] = (IntKeyframe) Keyframe.ofInt(1f, values[0]);
} else {
keyframes[0] = (IntKeyframe) Keyframe.ofInt(0f, values[0]);
for (int i = 1; i < numKeyframes; ++i) {
keyframes[i] =
(IntKeyframe) Keyframe.ofInt((float) i / (numKeyframes - 1), values[i]);
}
}
return new IntKeyframeSet(keyframes);
}

上述代码是生成关键帧的逻辑:
1.判断值的个数,如果只有一个值。那么分配长度为2的IntKeyframe数组,并将值保持在第二个位置。
2.如果多于一个值,会分配 值个数 长度的数组,将每一个数值都保持在数组对应的位置。
需要注意的是:除了第一帧,其他帧都分配了fraction,这个系数有这些数字的传入顺序决定。假如我们传入3个值,第一帧的系数默认为0,第二帧的系数为0.5,最后一个值得系数是1
。这个系数在后来动画的执行中为生成相应的属性值起到关键作用。

到目前为止,我们知道PropertyValuesHolder持有了Property并将动画的值保存成关键帧,并持有这些关键帧。

同样setValues(PropertyValuesHolder.ofInt(mPropertyName, values));方法也相同的方式生成了PropertyValuesHolder并持有 生成的关键帧

然后我们分析一下setValues方法。
这是父类 ValueAnimator的方法

1
2
3
4
5
6
7
8
9
10
11
public void setValues(PropertyValuesHolder... values) {
int numValues = values.length;
mValues = values;
mValuesMap = new HashMap<String, PropertyValuesHolder>(numValues);
for (int i = 0; i < numValues; ++i) {
PropertyValuesHolder valuesHolder = values[i];
mValuesMap.put(valuesHolder.getPropertyName(), valuesHolder);
}
// New property/values/target should cause re-initialization prior to starting
mInitialized = false;
}

主要功能:
1.保存前面生成的 PropertyValuesHolder,用 mValues持有引用。
2.将 PropertyValuesHolder数组按照属性名为key,PropertyValuesHolder为值保存至map中,并标记为初始化mInitialized = false,在下一篇文章中会提到何时将标记位mInitialized置为 true

到目前为止我们已经分析完了 ObjectAnimator.ofInt方法。
其中有几个未解释到的类:
1.PropertyValuesHolder
2.Keyframe

PropertyValuesHolder

由前面的分析我们大致知道 PropertyValuesHolder 持有了 Property或mPropertyName,以及持有 关键帧。
除此之外,它还有 属性mSetter,mGetter,mEvaluator

1
2
3
Method mSetter = null;
private Method mGetter = null;
private TypeEvaluator mEvaluator;

以及一堆 of方法

ofInt
ofMultiInt
ofFloat
ofMultiFloat
ofObject
ofKeyframe

这些 of方法我们前面解析过 ofInt 这里不再展开解释。
看到 mSetter和mGetter方法,我们联想到这个类肯定会操作 动画对象的属性。果不其然,让我们找到了 setupSetter和setupGetter 方法

1
2
3
4
5
6
7
8
9
10
11
 void setupSetter(Class targetClass) {
Class<?> propertyType = mConverter == null ? mValueType : mConverter.getTargetType();
mSetter = setupSetterOrGetter(targetClass, sSetterPropertyMap, "set", propertyType);
}

/**
* Utility function to get the getter from targetClass
*/
private void setupGetter(Class targetClass) {
mGetter = setupSetterOrGetter(targetClass, sGetterPropertyMap, "get", null);
}

这两个方法通过 拼接set 和get,通过反射获取属性的 get set 方法,从而获得对象属性的控制权。
当然 一些PropertyValuesHolder的子类重载使用了 jni 方法获得反射方法。其实都是目的都是一样。

当然,获得了这些属性的控制权,自然会更新这些属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void setAnimatedValue(Object target) {
if (mProperty != null) {
mProperty.set(target, getAnimatedValue());
}
if (mSetter != null) {
try {
mTmpValueArray[0] = getAnimatedValue();
mSetter.invoke(target, mTmpValueArray);
} catch (InvocationTargetException e) {
Log.e("PropertyValuesHolder", e.toString());
} catch (IllegalAccessException e) {
Log.e("PropertyValuesHolder", e.toString());
}
}
}

这个 setAnimatedValue方法就是用于更新这些属性;如果有 mProperty就通过 Property更新属性,如果没有的话,就通过反射更新。
当然在更新属性之前需要计算出正确的属性值。

1
2
3
4
void calculateValue(float fraction) {
Object value = mKeyframes.getValue(fraction);
mAnimatedValue = mConverter == null ? value : mConverter.convert(value);
}

calculateValue方法就是用于计算这些值。计算又用到了Keyframes,下面分析关键帧相关的API。到此为止我们理解了 PropertyValuesHolder的作用

总结一下:
1.持有 Property 用于获取,更新属性的能力。
2.持有 mPropertyName 通过反射获取get , set 方法,获取更新属性的能力
3.计算属性的值
4.更新属性
所以 PropertyValuesHolder就是控制动画对象属性的关键。所以也很容易理解,为什么所有的属性动画的value都转化成PropertyValuesHolder来操作.

Keyframe

下面我们在详细了解下何为关键帧,它是如何运作的?
先看几个关于关键帧的类 Keyframe,Keyframes 以及它们的子类
Keyframe顾名思义就是记录关键帧数据,他默认有三个实现类IntKeyframe,FloatKeyframe,ObjectKeyframe,当然我们也能继承Keyframe实现自己的关键帧类型。不过大部分情况,提供的这三种方式已经够用了。
Keyframe 有三个重要的属性值:
1.mHasValue 用于记录关键帧是否初始化了值,以 子类 IntKeyframe 为例,它有两个构造方法:

1
2
3
4
5
6
7
8
9
10
11
IntKeyframe(float fraction, int value) {
mFraction = fraction;
mValue = value;
mValueType = int.class;
mHasValue = true;
}

IntKeyframe(float fraction) {
mFraction = fraction;
mValueType = int.class;
}

第一个构造初始化了 mValue ,同时 mHasValue会标记 为true;
第二个构造方法 只初始化了 mFraction,并未给 mValue赋值,默认 mHasValue为false,假如我们使用了mHasValue为 false的 关键帧。那么在动画初始化时会调用PropertyValuesHolder.setupSetterAndGetter给每一个关键帧赋初始值。

2.mFraction记录该关键帧所有关键帧中的位置,float类型,值范围0 - 1
3.mValue记录value值,当然不同类型的Keyframe value值类型也不同,所以 mValue由子类实现

Keyframes

我们可以认为 Keyframes是一堆 Keyframe组成的集合。
先看看继承体系:
它的实现类有KeyframeSet,PathKeyframes,它的子接口 有IntKeyframes,FloatKeyframes
我们看看常用的KeyframeSet,继承它的两个子类IntKeyframeSet,FloatKeyframeSet,通知实现了对应的IntKeyframes和FloatKeyframes接口。

其实 Keyframes最核心的方法是getValue方法,其子实现均是为了实现它。下面是KeyframeSet的getValue方法的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public Object getValue(float fraction) {
if (mNumKeyframes == 2) {
if (mInterpolator != null) {
fraction = mInterpolator.getInterpolation(fraction);
}
return mEvaluator.evaluate(fraction, mFirstKeyframe.getValue(),
mLastKeyframe.getValue());
}
if (fraction <= 0f) {
final Keyframe nextKeyframe = mKeyframes.get(1);
final TimeInterpolator interpolator = nextKeyframe.getInterpolator();
if (interpolator != null) {
fraction = interpolator.getInterpolation(fraction);
}
final float prevFraction = mFirstKeyframe.getFraction();
float intervalFraction = (fraction - prevFraction) /
(nextKeyframe.getFraction() - prevFraction);
return mEvaluator.evaluate(intervalFraction, mFirstKeyframe.getValue(),
nextKeyframe.getValue());
} else if (fraction >= 1f) {
final Keyframe prevKeyframe = mKeyframes.get(mNumKeyframes - 2);
final TimeInterpolator interpolator = mLastKeyframe.getInterpolator();
if (interpolator != null) {
fraction = interpolator.getInterpolation(fraction);
}
final float prevFraction = prevKeyframe.getFraction();
float intervalFraction = (fraction - prevFraction) /
(mLastKeyframe.getFraction() - prevFraction);
return mEvaluator.evaluate(intervalFraction, prevKeyframe.getValue(),
mLastKeyframe.getValue());
}
Keyframe prevKeyframe = mFirstKeyframe;
for (int i = 1; i < mNumKeyframes; ++i) {
Keyframe nextKeyframe = mKeyframes.get(i);
if (fraction < nextKeyframe.getFraction()) {
final TimeInterpolator interpolator = nextKeyframe.getInterpolator();
final float prevFraction = prevKeyframe.getFraction();
float intervalFraction = (fraction - prevFraction) /
(nextKeyframe.getFraction() - prevFraction);
// Apply interpolator on the proportional duration.
if (interpolator != null) {
intervalFraction = interpolator.getInterpolation(intervalFraction);
}
return mEvaluator.evaluate(intervalFraction, prevKeyframe.getValue(),
nextKeyframe.getValue());
}
prevKeyframe = nextKeyframe;
}
// shouldn't reach here
return mLastKeyframe.getValue();
}

该方法核心是 通过参数fraction(时间流逝比)来计算对应的属性值,计算出来的值如无TypeConverter转换需求,那就会把这个值设置到 动画Target的属性上,从而实现属性动画对属性的更改。
观察到这里还使用了一个TypeEvaluator,这哥们大家估计通过其他途径都了解过,我们经常把 TimeInterpolator和TypeEvaluator一起来解释属性动画的原理,即插值器和估值器对属性动画的作用。我们这里不展开讨论,只基于源码去分析。
我们先看第一个if分支

1
2
3
4
5
6
7
if (mNumKeyframes == 2) {
if (mInterpolator != null) {
fraction = mInterpolator.getInterpolation(fraction);
}
return mEvaluator.evaluate(fraction, mFirstKeyframe.getValue(),
mLastKeyframe.getValue());
}

对应当仅有两个关键帧时的情况。举个例子,假如我们调用ObjectAnimator.ofInt方法只传入了一个int值,这里的 mNumKeyframes会是2,而且看注释,只有在这种情况下才使用mInterpolator-插值器。通过插值器 得到修正后的系数fraction,然后拿这个系数,通过估值器mEvaluator计算出属性值。我们发现估值器是何等的重要,必须要被赋值的,否则无法计算出属性值。
继续 if (fraction <= 0f)和if (fraction >= 1f)这两个分支是特殊情况,出现在动画第一帧和最后一帧的情况。
下面看看正常动画中的分支流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Keyframe prevKeyframe = mFirstKeyframe;
for (int i = 1; i < mNumKeyframes; ++i) {
Keyframe nextKeyframe = mKeyframes.get(i);
if (fraction < nextKeyframe.getFraction()) {
final TimeInterpolator interpolator = nextKeyframe.getInterpolator();
final float prevFraction = prevKeyframe.getFraction();
float intervalFraction = (fraction - prevFraction) /
(nextKeyframe.getFraction() - prevFraction);
// Apply interpolator on the proportional duration.
if (interpolator != null) {
intervalFraction = interpolator.getInterpolation(intervalFraction);
}
return mEvaluator.evaluate(intervalFraction, prevKeyframe.getValue(),
nextKeyframe.getValue());
}
prevKeyframe = nextKeyframe;
}

通过fraction查找符合fraction < nextKeyframe.getFraction()的第一帧,然后计算 fraction在前一帧到当前帧范围内的位置,例如查找到符合条件的当前帧 fraction =0.5;前一帧fraction = 0.2, 动画当前的 fraction =0.3,那么新的 intervalFraction = 1/3f。然后拿着这个值给估值器计算属性值。

到目前为止我们知道了,关键帧的作用如下:
1.保存每个关键帧的值
2.保存每个关键帧的 fraction
3.通过动画类传入的 fraction,利用估值器计算出 真正的属性值。

那么剩下的就是如何将这些计算出来的属性值设置到对应的Target上了。这个问题我们下一篇分析动画的启动原理时会再分析。

回顾

1.以ObjectAnimator.ofInt为切入点 分析 ObjectAnimator对象的创建
2.属性名称propertyName和Property的作用:
propertyName 用于拼接 get/set方法,获得控制属性的能力;
Property 利用自己的get和set方法直接控制属性。

  1. PropertyValuesHolder的作用:通过 propertyName 或 Property 获取更新属性的能力;根据动画时间流逝比计算属性的值;更新属性值到对应的Target上
  2. 关键帧 keyframe用于保存动画的每一个关键数据帧,个数由传入的 value 个数决定,系数 fraction 由 value 传入顺序决定。
  3. keyframes以及它的子类KeyframeSet的作用:利用估值器和 fraction 计算出应该更新到 Target上的属性值。

基本API 解析完毕,下一篇我们会分析动画如何开始的,fraction系数如何得到的,以及何时更新 属性值。

Android属性动画分析

Posted on 2017-11-25 | In Android

属性动画分析概略

Android 动画分三类:
Drawable Animation:几帧图循环播放;
View Animation : 只有基本的 缩放,平移,旋转,渐变,通过对 Canvas 操作来实现。只在绘制层做了动画假象,实际属性并未改变
ValueAnimator:属性动画,可以实现更平滑的动画和对属性的变更。

我将从以下几个方面分析 属性动画的实现原理

  1. 通过常用 ObjectAnimator.of.. 方法切入,深入分析 PropertyValueHolder,Property,Keyframe的实现原理和作用。
  2. 通过对属性动画的启动方式的解析 代入 Android 新的渲染方式(黄油计划)
  3. 属性动画其他常用API的使用
  4. ValueAnimator和ObjectAnimator的关系以及如何选择
  5. 高级的属性动画自定义方式。例如自定义PropertyValueHolder和自定义keygrame实现属性动画效果。

针对源码分析,前三篇文章会涉及的多一些,也更能体会到Android属性动画设计的巧妙和灵活之处。后几篇文章着重应用。

作为一个有责任心的程序员,我希望通过这几篇文章,能帮助小伙伴们更深入理解Android属性动画,而不仅仅局限在使用上。

Java 形参与实参

Posted on 2017-11-16

前几天在头条上看到一道经典面试题,引发了一些思考。也是写这篇文章的导火索。

背景

请看题:

1
2
3
4
5
6
7
8
9
10
11
12
13
public	class Main {
public static void main(String[] args) {
Integer a = 1;
Integer b = 2;
System.out.println("a=" + a + ",b=" + b);
swap(a, b);
System.out.println("a=" + a + ",b=" + b);
}

private static void swap(Integer numa, Integer numb) {
//请实现
}
}

看到这个题后 瞬间觉得有坑。也觉得为什么要书写一个swap方法呢?如下实现不是更简单:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
Integer a = 1;
Integer b = 2;
System.out.println("a=" + a + ",b=" + b);
Integer tmp = a;
a = b;
b = tmp;
System.out.println("a=" + a + ",b=" + b);
}

输出:

1
2
a=1,b=2
a=2,b=1

完美实现交换。但是请注意,这是一道面试题,要的就是考验一些知识点。所以还是老老实实的实现swap方法吧。
有的同学可能会想,Integer 是一个包装类型,是对Int的装箱和拆箱操作。其实也是一个对象。既然是对象,直接更改对象的引用不就行了?
思路没问题,我们首先看看实现:

1
2
3
4
5
6
private static void swap(Integer numa, Integer numb) {
Integer tmp = numa;
numa = numb;
numb = tmp;
System.out.println("numa=" + numa + ",numb=" + numb);
}

输出:

1
2
3
a=1,b=2
numa=2,numb=1
a=1,b=2

不出意外,没有成功
这是什么原因呢?
技术老手一看就知道问题出在形参和实参
混淆了

JAVA的形参和实参的区别:

形参 顾名思义:就是形式参数,用于定义方法的时候使用的参数,是用来接收调用者传递的参数的。
形参只有在方法被调用的时候,虚拟机才会分配内存单元,在方法调用结束之后便会释放所分配的内存单元。
因此,形参只在方法内部有效,所以针对引用对象的改动也无法影响到方法外。

实参 顾名思义:就是实际参数,用于调用时传递给方法的参数。实参在传递给别的方法之前是要被预先赋值的。
在本例中 swap 方法 的numa, numb 就是形参,传递给 swap 方法的 a,b 就是实参

注意:
在值传递调用过程中,只能把实参传递给形参,而不能把形参的值反向作用到实参上。在函数调用过程中,形参的值发生改变,而实参的值不会发生改变。
而在引用传递调用的机制中,实际上是将实参引用的地址传递给了形参,所以任何发生在形参上的改变也会发生在实参变量上。
那么问题来了,什么是值传递和引用传递

值传递和引用传递

在谈值传递和引用传递之前先了解下 Java的数据类型有哪些

JAVA的数据类型

Java 中的数据类型分为两大类,基本类型和对象类型。相应的,变量也有两种类型:基本类型和引用类型
基本类型的变量保存原始值,即它代表的值就是数值本身,原始值一般对应在内存上的栈区
而引用类型的变量保存引用值,引用值指向内存空间的地址。代表了某个对象的引用,而不是对象本身。对象本身存放在这个引用值所表示的地址的位置。被引用的对象对应内存上的堆内存区。
基本类型包括:byte,short,int,long,char,float,double,boolean 这八大基本数据类型
引用类型包括:类类型,接口类型和数组

变量的基本类型和引用类型的区别

基本数据类型在声明时系统就给它分配空间

1
2
3
int a;//虽然没有赋值,但声明的时候虚拟机就会 分配 4字节 的内存区域,而引用数据类型不同,它声明时只给变量分配了引用空间,而不分配数据空间:	
String str;//声明的时候没有分配数据空间,只有 4byte 的引用大小,在栈区,而在堆内存区域没有任何分配
str.length(); //这个操作就会报错,因为堆内存上还没有分配内存区域,而 a = 1; 这个操作就不会报错。

好了,Java的数据类型说完了,继续我们的值传递和引用传递的话题。
先背住一个概念:基本类型的变量是值传递;引用类型的变量
结合前面说的 形参和实参。

值传递

方法调用时,实际参数把它的值传递给对应的形式参数,函数接收的是原始值的一个copy,
此时内存中存在两个相等的基本类型,即实际参数和形式参数,后面方法中的操作都是对形参这个值的修改,不影响实际参数的值

引用传递

也称为地址传递,址传递。方法调用时,实际参数的引用(地址,而不是参数的值)被传递给方法中相对应的形式参数,函数接收的是原始值的内存地址
在方法执行中,形参和实参内容相同,指向同一块内存地址,方法执行中对引用的操作将会影响到实际对象
通过例子来说话:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static class Person {
int age;
Person(int age) {
this.age = age;
}
}

private static void test() {
int a = 100;
testValueT(a);
System.out.println("a=" + a);
Person person = new Person(20);
testReference(person);
System.out.println("person.age=" + person.age);
}

private static void testValueT(int a) {
a = 200;
System.out.println("int testValueT a=" + a);
}

private static void testReference(Person person) {
person.age = 10;
}

输出:

1
2
3
int testValueT a=200
a=100
person.age=10

看见 值传递 a的值并没有改变,而 引用传递的 persion.age已经改变了
有人说

1
2
3
private static void testReference(Person person) {
person = new Person(100);
}

为什么 输出的 person.age 还是20呢?
我想说 了解一下什么是引用类型吧? 方法内把 形参的地址引用换成了另一个对象,并没有改变这个对象,并不能影响 外边实参还引用原来的对象,因为 形参只在方法内有效哦。

有人或许还有疑问,按照文章开头的例子,Integer也是 引用类型该当如何呢?
其实 类似的 String,Integer,Float,Double,Short,Byte,Long,Character等等基本包装类型类。因为他们本身没有提供方法去改变内部的值,例如Integer 内部有一个value 来记录int基本类型的值,但是没有提供修改它的方法,而且 也是final类型的,无法通过常规手段更改。
所以虽然他们是引用类型的,但是我们可以认为它是值传递,这个也只是认为,事实上还是引用传递,址传递。


好了,基础知识补充完毕,然我们回到面试题吧


回归正题

1
2
3
4
5
6
private static void swap(Integer numa, Integer numb) {
Integer tmp = numa;
numa = numb;
numb = tmp;
System.out.println("numa=" + numa + ",numb=" + numb);
}

通过补习基础知识,我们很明显知道 上面这个方法实现替换 是不可行的。因为Interger虽然是引用类型
但是上述操作只是改变了形参的引用,而没有改变实参对应的对象。

那么思路来了,我们通过特殊手段改变 Integer内部的value属性

1
2
3
4
5
6
7
8
9
10
11
private static void swap(Integer numa, Integer numb) {
Integer tmp = numa;
try {
Field field = Integer.class.getDeclaredField("value");
field.setAccessible(true);
field.set(numa, numb);//成功的将numa 引用的 1的对象 值改为 2
field.set(numb, tmp); //由于 tmp 也是指向 numa 未改变前指向的堆 即对象1 ,经过前一步,已经将对象1的值改为了2,自然 numb 也是2,所以改动失效
} catch (Exception e) {
e.printStackTrace();
}
}

输出结果:

1
2
a=1,b=2
a=2,b=2

又来疑问了?为何 a的值改变成功,而b的改变失败呢?

见代码注释
所以其实 field.set(numb, tmp); 是更改成功的,只是 tmp 经过前一行代码的执行,已经变成了 2。
那么如何破呢?
我们有了一个思路,既然是 tmp的引用的对象值变量,那么我让tmp不引用 numa了

1
2
3
4
5
6
7
8
9
10
11
private static void swap(Integer numa, Integer numb) {
int tmp = numa.intValue();//tmp 定义为基本数据类型
try {
Field field = Integer.class.getDeclaredField("value");
field.setAccessible(true);
field.set(numa, numb);//这个时候并不改变 tmp 的值
field.set(numb, tmp);
} catch (Exception e) {
e.printStackTrace();
}
}

这种情况下 对 numa 这个对象的修改就不会导致 tmp 的值变化了,看一下运行结果

a=1,b=2
a=2,b=2

这是为啥?有没有快疯啦?
难道我们的思路错了?
先别着急,我们看看这个例子:
仅仅是将前面的例子 a的值改为 129,b的值改为130

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) {
Integer a = 129;
Integer b = 130;

System.out.println("a=" + a + ",b=" + b);
swap(a, b);
System.out.println("a=" + a + ",b=" + b);
}

private static void swap(Integer numa, Integer numb) {
int tmp = numa.intValue();
try {
Field field = Integer.class.getDeclaredField("value");
field.setAccessible(true);
field.set(numa, numb);
field.set(numb, tmp);
} catch (Exception e) {
e.printStackTrace();
}
}

运行结果:

1
2
a=129,b=130
a=130,b=129

有没有怀疑人生?我们的思路没有问题啊?为什么 换个数值就行了呢?
我们稍微修改一下程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) {
Integer a = new Integer(1);
Integer b = new Integer(2);

System.out.println("a=" + a + ",b=" + b);
swap(a, b);
System.out.println("a=" + a + ",b=" + b);
}

private static void swap(Integer numa, Integer numb) {
int tmp = numa.intValue();
try {
Field field = Integer.class.getDeclaredField("value");
field.setAccessible(true);
field.set(numa, numb);
field.set(numb, tmp);
} catch (Exception e) {
e.printStackTrace();
}
}

运行结果:

1
2
a=1,b=2
a=2,b=1

哎?为啥 1 和 2 也可以了?
我们这时肯定猜想和Integer的装箱 拆箱有关

装箱,拆箱 概念

Integer的装箱操作

为什么 Integer a = 1 和 Integer a = new Integer(1) 效果不一样
那就瞅瞅源码吧?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public Integer(int value) {
this.value = value;
}

/**
* Returns an {@code Integer} instance representing the specified
* {@code int} value. If a new {@code Integer} instance is not
* required, this method should generally be used in preference to
* the constructor {@link #Integer(int)}, as this method is likely
* to yield significantly better space and time performance by
* caching frequently requested values.
*
* This method will always cache values in the range -128 to 127,
* inclusive, and may cache other values outside of this range.
*
* @param i an {@code int} value.
* @return an {@code Integer} instance representing {@code i}.
* @since 1.5
*/
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}

通过注释知道,java推荐 Integer.valueOf 方式初始化一个Interger因为有 缓存了-128 - 127的数字
我们直接定义 Integer a = 1 具有这个功能,所以 Jvm 底层实现 是通过 Integer.valueOf这个方法
再看 field.set(numb, tmp);
我们打断点,发现通过反射设置 value时 竟然走了 Integer.valueOf 方法
下面是 我们调用 swap前后的 IntegerCache.cache 值得变化

反射修改前:

反射修改后


在反射修改前

1
2
3
IntegerCache.cache[128]=0
IntegerCache.cache[129]=1
IntegerCache.cache[130]=2

通过反射修改后

1
2
3
IntegerCache.cache[128]=0
IntegerCache.cache[129]=2
IntegerCache.cache[130]=2

再调用 field.set(numb, tmp) tmp这时等于1 对应的 角标 129 ,但是这个值已经变成了2
所以出现了刚才 奇怪的结果
原来都是缓存的锅
下面趁机再看个例子 加深理解

1
2
3
4
5
6
Integer testA = 1;
Integer testB = 1;

Integer testC = 128;
Integer testD = 128;
System.out.println("testA=testB " + (testA == testB) + ",\ntestC=testD " + (testC == testD));

输出结果:

1
2
testA=testB true,
testC=testD false

通过这小示例,在 -128 到 127的数字都走了缓存,这样 testA 和 testB引用的是同一片内存区域的同一个对象。
而 testC testD 数值大于127 所以 没有走缓存,相当于两个Integer对象,在堆内存区域有两个对象。
两个对象自如不相等。
在前面的示例中 我们 通过

1
2
Integer a = new Integer(1);
Integer b = new Integer(2);

方式初始化 a,b 我们的交换算法没有问题,也是这个原因。

那么到目前为止我们的swap 方法可以完善啦
1
2
3
4
5
6
7
8
9
10
11
private static void swap(Integer numa, Integer numb) {
int tmp = numa.intValue();
try {
Field field = Integer.class.getDeclaredField("value");
field.setAccessible(true);
field.set(numa, numb);
field.set(numb, new Integer(tmp));
} catch (Exception e) {
e.printStackTrace();
}
}

只需将之前的 field.set(numb, tmp) 改为 field.set(numb, new Integer(tmp))

到此, 这个面试我们已经通过了,还有一个疑问我没有解答。
为什么 field.set(numb, tmp) 会执行 Integer.valueOf() 而 field.set(numb, new Integer(tmp)) 不会执行。
这就是Integer的装箱操作,当 给 Integer.value 赋值 int时,JVM 检测到 int不是Integer类型,需要装箱,才执行了Integer.valueOf()方法。而field.set(numb, new Integer(tmp)) 设置的 是Integer类型了,就不会再拆箱后再装箱。

Over Thanks

Markdown-常用语法

Posted on 2017-11-15

本篇文章简要介绍常用的Markdown语法,方便快速查阅。

标题

一级标题

# 一级标题

二级标题

## 二级标题

三级标题

### 三级标题

四级标题

#### 四级标题
五级标题
##### 五级标题
六级标题
###### 六级标题

分隔线


`***实现分隔线`

`---实现分隔线`

`___实现分隔线`

表格

Title Content
项目1 实现项目1方案
| Title|Content|
|---|---
|项目1|实现项目1方案

文本

最普通的文本

普通文本,直接书写,没有任何特殊符号

单行文本

这是一行单行文本

实现方案:
在单行文本前面加一个Tab

多行文本

实现方案1:

在每行文本前都加一个Tab 相当于多个单行文本

这是多行文本的第一行
这是多行文本的第二行
这是多行文本的第三行
实现方案2:

使用三个反引号,这也是代码的使用方式

1
2
3
4
5
反引号是 带有 ~ 符合的按键
String VIDEO = "video";
String TEXT = "text";
String PHOTO = "photo";
String HOME = "home";

换行

按照惯例,使用回车来换行,但是在Markdown上行不通。换行实现有以下方式:

1.两行直接空出一行。  
2.前一行后面保留两个空格,第二行自动换行

斜体,粗体,删除线,下划线

语法 效果
*斜体* 文字
_斜体_ 文字
<u>下滑线</u> 文字
**加粗** 文字
__加粗__ 文字
~~删除线~~ 文字
***斜粗体*** 文字
__斜粗体__ 文字
***~~斜粗体删除线~~*** 斜粗体删除线
~~**斜粗体删除线***~~ 斜粗体删除线

高亮

使用一对反引号即可
高亮文字,高亮文字

高亮文字

实现方案:
[高亮文字]()

超链接

百度

实现方案:
[百度](https://www.baidu.com)

引用图片

baidu

实现方案:
![alt](URL "title")
alt 和 title 都可以省略

![baidu](http://www.baidu.com/img/bdlogo.gif "百度logo")

emoji 表情

默认情况下Github 的Markdown 语法支持emoji表情
emoji大全
or
emoji大全

简单举例:

比如:blush:,可以显示:blush:

Kotlin-入门-基础1

Posted on 2017-11-15 | In Kotlin

Kotlin的定义和目的

Kotlin到底是什么?
它是一种针对Java 平台的新的 静态类型 的虚拟机编程语言。它由著名工具 IntelliJ IDEA 的创作团队发明。它是免费的并且开源。
Kotlin简洁,安全,务实,并且和Java代码无缝互操作。

Kotlin 的设计哲学

  1. 务实
  2. 简洁
  3. 安全
  4. 互操作性

Kotlin 基础

函数

搭建Shopify+Git团队本地开发环境

Posted on 2017-11-14

使用Shopify开发商城项目,在人少的时候可以直接在线编辑,随便没有历史但是还是能接受的,但是如果团队开发的话就必须配备CVS了,但是如果继续使用在线的编辑方式结合Git使用那成本实在太高了。如下是构建本地开发环境的步骤。

安装Theme工具包

如果是*nix用户直接使用如下命令安装:

1
curl -s https://raw.githubusercontent.com/Shopify/themekit/master/scripts/install | sudo python

或者使用Homebrew安装

1
2
brew tap shopify/shopify
brew install themekit

创建API并获取Key

image

关联theme

创建git仓库并clone到本地

1
git clone shopify-theme@gitlab.com

进入目录并关联theme

通过如下图片寻找ID,然后关联到项目
image

1
2
3
cd shopify-theme
theme configure --password=[your-password] --store=[you-store.myshopify.com] --themeid=[your-theme-id]
theme download

实时更新

运行如下命令实时同步修改的文件到远端

1
theme watch

这时候因为你已经关联了自己的theme_id,所以需要在https://username.myshopify.com/admin/themes页面点击preview实时查看自己的更新内容。如果需要修改theme_id等信息可以去根目录的config.yml文件修改。

团队协作

  • 每一个team member在Shopify控制台创建一个属于自己的theme,同时在Git创建一个自己的开发分支。
  • 开发过程中,每一个开发人员在自己的Git分支开发和Theme版本预览。
  • 开发完成之后合并dev分支代码在自己的Theme版本预览,测试没有问题,提交Merge Request到dev分支。
  • 在Shopify控制台创建Feature名称的Theme,与dev分支绑定。测试没有问题点击Publish。

搭建DailyCast技术博客

Posted on 2017-11-14

DailyCast技术博客搭建采用Hexo,一款快速、简洁且高效的博客框架。下面是具体的搭建和更新博客的步骤。

环境准备

  1. 根据 官网文档 安装NodeJS和Git。
  2. 安装NodeJS完成以后,使用npm安装hexo-cli博客管理工具,如果网络不好这个过程通常很慢。
    1
    $ npm install -g hexo-cli

创建博客

如果你是博客的发布人员,直接查看发布文章段落。

  1. 创建Github仓库dailycast.github.io
  2. 初始化博客系统

    1
    hexo init dailycast.github.io
  3. 进入博客目录,并初始化博客。

    1
    2
    cd dailycast.github.io/
    npm install
  4. 修改博客基本信息
    在_config.yml里面修改标题和描述

  5. 配置博客插件。
    当前项目目录安装发布工具,

    1
    npm install hexo-deployer-git --save
  6. 同时安装启动服务插件,以便本地可以启动

    1
    npm install hexo-server --save
  7. 然后在_config.yml里面配置如下内容:

    1
    2
    3
    4
    deploy: 
    type: git
    repo: https://github.com/DailyCast/dailycast.github.io
    branch: master
  8. 关联Github仓库,并把源码推送到远程,因为master是留给生成文件的,所以发布到了非master分支。

    1
    2
    3
    4
    5
    6
    git init
    git remote add origin https://github.com/DailyCast/dailycast.github.io.git
    git checkout -b source
    git add .
    git commit -m "init blog"
    git push -u origin source
  9. 发布博客
    直接运行如下命令发布博客,该命令会自动发布内容到master分支

    1
    hexo deploy

发布文章

  1. 如果本地没有仓库请clone仓库,并且切换到source分支。

    1
    2
    git clone https://github.com/DailyCast/dailycast.github.io.git
    git branch source
  2. Setup本地环境
    在项目目录运行如下命令安装依赖和初始化环境。

    1
    npm install
  3. 运行如下命令创建博客文章,后面的参数便是文章的标题

    1
    hexo new '搭建DailyCast技术博客'
  4. 在source/_posts目录找到刚才对应的文章,进入编辑文档即可。编辑过程中可以使用如下命令启动服务器和实时预览效果。

    1
    2
    hexo server
    hexo generate --watch
  5. 发布源码
    编辑文章完成以后运行如下命令发布源码到Github仓库

    1
    2
    3
    git add .
    git commit -m "add new post"
    git push origin source
  6. 发布文章

    1
    hexo generate --deploy

DailyCast

8 posts
2 categories
8 tags
© 2017 DailyCast
Powered by Hexo
|
Theme — NexT.Muse v5.1.3