迟来的续集--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没能动起来?

迟来的HTTP2简明教程

迟来的2015年总结

NOIP 2016 迟来的满贯

KafkaKafka版本的 watermark 迟来的消息 直接报错

迟来的2019年总结