迟来的续集--Drawable+Animator,将优雅进行到底
Posted leobert_lan
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了迟来的续集--Drawable+Animator,将优雅进行到底相关的知识,希望对你有一定的参考价值。
前言
2021年初,读过一篇关于splash页面动效的推送文章,作者讲解了如何实现一个闪屏页效果:
将一个英文单词拆分为多个字母,散落在屏幕中,然后按照一定的路径回归,最终展示一段流光效果。
通过自定义View的方式予以实现。
当时我脑中闪过一个念头:他的实现很棒,但如果不需要点触、手势交互,使用Drawable实现更好。并由此编写了一篇文章:三思系列:重新认识Drawable
, 并在不久之后通过 三思系列:为什么要自定义View 一文阐释了对于 “自定义View适用场景” 的个人拙见。
简单通过思维导图回顾 三思系列:重新认识Drawable 一文的内容:
阅读原文大约需要10-15分钟
文中,我们最终以该方案实现了 “自定义一个动画Drawable” : unscheduleSelf()
/ scheduleSelf()
机制 停止回调/设置定时回调
+ invalidateSelf()
机制进行刷新绘制;
方案的本质是 在预设时间点绘制关键帧 。仔细观察后不难发现问题:效果并不顺滑 。效果如下:
视频:链接
彼时,文章的主旨为重新认识Drawable,并未对此展开讨论并进一步优化。 本篇文章作为迟来的续集,将会 对问题展开讨论、探索优化方案、追究原理、并进一步拓宽思路。按照此方式展开将迎来久违的三思系列。
思危:问题本质
上文已经提到,我们通过 unscheduleSelf()
/ scheduleSelf()
机制 停止回调/设置定时回调,重新绘制关键帧。那么 scheduleSelf()
的本质又是什么?
阅读代码可知,源码中通过接口回调的设计,将功能的实现剥离:
class Drawable
public void scheduleSelf(@NonNull Runnable what, long when)
final Callback callback = getCallback();
if (callback != null)
callback.scheduleDrawable(this, what, when);
public final void setCallback(@Nullable Callback cb)
mCallback = cb != null ? new WeakReference<>(cb) : null;
@Nullable
public Callback getCallback()
return mCallback != null ? mCallback.get() : null;
public interface Callback
void invalidateDrawable(@NonNull Drawable who);
void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when);
void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what);
继续寻找 Callback
实现类:重点关注 scheduleDrawable 即可
public class View implements Drawable.Callback
public void invalidateDrawable(@NonNull Drawable drawable)
if (verifyDrawable(drawable))
final Rect dirty = drawable.getDirtyBounds();
final int scrollX = mScrollX;
final int scrollY = mScrollY;
invalidate(dirty.left + scrollX, dirty.top + scrollY,
dirty.right + scrollX, dirty.bottom + scrollY);
rebuildOutline();
//看这里
public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when)
if (verifyDrawable(who) && what != null)
final long delay = when - SystemClock.uptimeMillis();
if (mAttachInfo != null)
mAttachInfo.mViewRootImpl.mChoreographer.postCallbackDelayed(
Choreographer.CALLBACK_ANIMATION, what, who,
Choreographer.subtractFrameDelay(delay));
else
// Postpone the runnable until we know
// on which thread it needs to run.
getRunQueue().postDelayed(what, delay);
public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what)
if (verifyDrawable(who) && what != null)
if (mAttachInfo != null)
mAttachInfo.mViewRootImpl.mChoreographer.removeCallbacks(
Choreographer.CALLBACK_ANIMATION, what, who);
getRunQueue().removeCallbacks(what);
public void unscheduleDrawable(Drawable who)
if (mAttachInfo != null && who != null)
mAttachInfo.mViewRootImpl.mChoreographer.removeCallbacks(
Choreographer.CALLBACK_ANIMATION, null, who);
简单解释程序逻辑如下:如果 “该Drawable作用于自身” 且 “Runnable非空”,计算回调的delay,如果View已经添加到Window,则交给Choreographer,否则丢入缓存队列。
而缓存队列的内容将在View添加到Window时交给 Choreographer
public class View
void dispatchAttachedToWindow(AttachInfo info, int visibility)
//ignore
// Transfer all pending runnables.
if (mRunQueue != null)
mRunQueue.executeActions(info.mHandler);
mRunQueue = null;
//ignore
读者诸君,如果您熟悉android的
屏幕刷新机制
和消息机制
,一定不会对Choreographer
感到陌生
Choreographer 直译为编舞者,暗含了 “编制视图变化效果” 的隐喻,其本质依旧是利用 VSync+Handler消息机制。delay Callback的设计存在毫秒级的误差。
作者按:本篇不再展开讨论Android的消息机制,以下仅给出 基于消息机制的界面绘制设计
关键部分流程图:
结合前面的代码分析,scheduleDrawable
的流程可以参考此图理解。
作者按,虽然仍有差异,但机制一致,可参考理解
验证
Talk is cheap, show you the code
在 View
中有一段代码和 scheduleDrawable
高度相似:
class View
public void postOnAnimationDelayed(Runnable action, long delayMillis)
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null)
attachInfo.mViewRootImpl.mChoreographer.postCallbackDelayed(
Choreographer.CALLBACK_ANIMATION, action, null, delayMillis);
else
// Postpone the runnable until we know
// on which thread it needs to run.
getRunQueue().postDelayed(action, delayMillis);
注意:scheduleDrawable
基于执行的目标时间 when
,和当前系统时钟计算了delay,又额外调整了delay时间, Choreographer.subtractFrameDelay(delay)
,_
它是隐藏API_
public final class Choreographer
private static final long DEFAULT_FRAME_DELAY = 10;
// The number of milliseconds between animation frames.
private static volatile long sFrameDelay = DEFAULT_FRAME_DELAY;
public static long subtractFrameDelay(long delayMillis)
final long frameDelay = sFrameDelay;
return delayMillis <= frameDelay ? 0 : delayMillis - frameDelay;
设计一个简单的验证代码:
class Demo
//...
fun test()
val btn = findViewById<Button>(R.id.btn)
var index = 0
var s = System.currentTimeMillis()
val action: Runnable = object : Runnable
override fun run()
Log.e("lmsg", "$index, offset time $System.currentTimeMillis() - s - index * 30")
index++
if (index < 100)
btn.postOnAnimationDelayed(
this,
30L - 10L /*hide api:android.view.Choreographer#subtractFrameDelay*/
)
else
Log.e("lmsg", "finish, total time $System.currentTimeMillis() - s")
btn.setOnClickListener
index = 0
s = System.currentTimeMillis()
it.postOnAnimationDelayed(action, 0L)
参考一下结果:注意执行结果不会幂等,但整体表现为超出预期时长
思退:使用Animator改进
Android 在 Android 3.0,API11
中提供了更强大的动画 Animator
,借助其中的 ValueAnimator
,可以很方便的 编排
动画。
即便尚未分析原理,只要使用过属性动画,也知道它具有非常丝滑的效果
以上还都是推测,接下来进行实测。
实现
刨去一致部分,我们需要完成以下两点:
- 创建
ValueAnimator
实例,并按照动画需求设置时长
、插值器
、UpdateListener
等 - 若没有额外需要,可将
Animatable2
弱化为Animatable
,仅保留动画控制API,通过ValueAnimator
实例委托实现API业务逻辑。
核心代码如下: 完整代码可从github获取:DrawableWorkShop
class AnimLetterDrawable2 : Drawable(), Animatable
// 相似部分略去
private val totalFrames = 30 * 3 //3 second, 30frames per second
private val valueAnimator = ValueAnimator.ofInt(totalFrames).apply
duration = 3000L
this.interpolator = LinearInterpolator()
addUpdateListener
setFrame(it.animatedValue as Int)
private var frameIndex = 0
private fun setFrame(frame: Int)
if (frame >= totalFrames)
return
frameIndex = frame
invalidateSelf()
override fun start()
Log.d(tag, "start called")
valueAnimator.start()
override fun stop()
valueAnimator.cancel()
setFrame(0)
override fun isRunning(): Boolean
return valueAnimator.isRunning
效果和关键代码对比
gif的效果太差,可以在 github项目仓库
中获取 webm视频
关键代码差异:
在原方案中,我们计算了下一帧的播放时间点,借助 scheduleSelf
-> View#scheduleDrawable
进行了刷新
class AnimLetterDrawable
private fun setFrame(frame: Int, unschedule: Boolean, animate: Boolean)
if (frame >= totalFrames)
return
mAnimating = animate
frameIndex = frame
if (unschedule || animate)
unscheduleSelf(this)
if (animate)
// Unscheduling may have clobbered these values; restore them
frameIndex = frame
scheduleSelf(this, SystemClock.uptimeMillis() + durationPerFrame)
invalidateSelf()
而新方案中,我们借助ValueAnimator的更新回调函数直接刷新,显示预定帧
class AnimLetterDrawable2
private val valueAnimator = ValueAnimator.ofInt(totalFrames).apply
duration = 3000L
this.interpolator = LinearInterpolator()
addUpdateListener
setFrame(it.animatedValue as Int)
private fun setFrame(frame: Int)
if (frame >= totalFrames)
return
frameIndex = frame
invalidateSelf()
Animator的原理
此时,再来思索一番,为何 Animator
的实现效果明显丝滑呢?
思危:是否和scheduleDrawable相比使用了不一样的底层机制?
源码跟进
单纯阅读文章内的代码会很枯燥,建议读者诸君对文中列出的源码进行泛读,抓住思路后再精读一遍源码。
以下将有6个关键点,可厘清其原理
- 1,start方法 – 找到动画被驱动的核心
- 2, AnimationHandler#addAnimationFrameCallback(AnimationFrameCallback)
- 3,
mAnimationCallbacks
何时移除元素 - 4,
AnimationHandler#doAnimationFrame
方法的逻辑 - 5,向前看,何人调用FrameCallback – 驱动动画的底层逻辑
- 6,向后看,ValueAnimator#doAnimationFrame – 丝滑的原因
1,start方法
class ValueAnimator
public void start()
start(false);
private void start(boolean playBackwards)
if (Looper.myLooper() == null)
throw new AndroidRuntimeException("Animators may only be run on Looper threads");
//略去一部分
addAnimationCallback(0); //这里是核心
if (mStartDelay == 0 || mSeekFraction >= 0 || mReversing)
startAnimation();
if (mSeekFraction == -1)
setCurrentPlayTime(0);
else
setCurrentFraction(mSeekFraction);
private void addAnimationCallback(long delay)
//startWithoutPulsing 才会return
if (!mSelfPulse)
return;
getAnimationHandler().addAnimationFrameCallback(this, delay); //这里是核心
简单阅读,可以排除掉 startAnimation
setCurrentPlayTime
setCurrentFraction
,他们均不是动画回调的核心,只是在进行必要地初始化和FLAG状态维护。
真正的核心是:getAnimationHandler().addAnimationFrameCallback(this, delay);
注意:AnimationHandler 存在线程单例设计:
//使用方:
class ValueAnimator
public AnimationHandler getAnimationHandler()
return mAnimationHandler != null ? mAnimationHandler : AnimationHandler.getInstance();
//ThreadLocal线程单例设计
class AnimationHandler
public final static ThreadLocal<AnimationHandler> sAnimatorHandler = new ThreadLocal<>();
private boolean mListDirty = false;
public static AnimationHandler getInstance()
if (sAnimatorHandler.get() == null)
sAnimatorHandler.set(new AnimationHandler());
return sAnimatorHandler.get();
2, AnimationHandler#addAnimationFrameCallback(AnimationFrameCallback)
方法逻辑中,有两处需要关注:
- 如果无
AnimationFrameCallback
回调实例 , 说明没有在运行中的动画 ,则挂载Choreographer.FrameCallback mFrameCallback
, 为更新动画(_
调用动画的AnimationFrameCallback回调接口_)做准备。 - 在动画的
AnimationFrameCallback
回调实例未被注册的情况下,注册该回调实例
看完这一段源码,读者诸君一定会对以下两点产生兴趣,我们在下文展开:
doAnimationFrame
方法的逻辑mAnimationCallbacks
何时移除元素
先看源码:
public class AnimationHandler
private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback()
@Override
public void doFrame(long frameTimeNanos)
doAnimationFrame(getProvider().getFrameTime());
//这不就破案了,只要还有动画的 AnimationFrameCallback,就挂载 mFrameCallback
if (mAnimationCallbacks.size() > 0)
getProvider().postFrameCallback(this);
;
private AnimationFrameCallbackProvider getProvider()
if (mProvider == null)
mProvider = new MyFrameCallbackProvider();
return mProvider;
public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay)
if (mAnimationCallbacks.size() == 0)
getProvider().postFrameCallback(mFrameCallback);
if (!mAnimationCallbacks.contains(callback))
mAnimationCallbacks.add(callback);
//注意,delay为0,阅读时可以忽略这段逻辑
if (delay > 0)
mDelayedCallbackStartTime.put(callback, (SystemClock.uptimeMillis() + delay));
3,mAnimationCallbacks
何时移除元素
AnimationHandler中 “清理” mAnimationCallbacks
的设计 : 先设置null,再择机集中清理null,维护链表结构。可以避免循环过程中移除元素带来的潜在bug、以及避免频繁调整链表空间带来的损耗
关键代码为:android.animation.AnimationHandler#removeCallback
,它有两处调用点,看完下面这一段源码后再行分析。
class AnimationHandler
public void removeCallback(AnimationFrameCallback callback)
mCommitCallbacks.remove(callback);
mDelayedCallbackStartTime.remove(callback);
int id = mAnimationCallbacks.indexOf(callback);
if (id >= 0)
mAnimationCallbacks.set(id, 好玩系列:听说你的ImageSpan没能动起来?