Android 关于绘制的一个小细节分享

Posted 冬天的毛毛雨

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 关于绘制的一个小细节分享相关的知识,希望对你有一定的参考价值。

作者:互联网程序员

很多时候我们在自定义 View 的需要做动画的时候,我们可以依赖属性动画的回调周期性修改 自定义的属性值,然后调用 invalidate 方法实现。

不过我还见过一个比较野的路子,它在 onDraw 里面直接修改属性,然后调用 invalidate() 方法。

运行起来好像也没问题。

那么问题来了:

1. 在 onDraw 里面调用 修改绘制相关属性(例如画圆,修改半径) invalidate() ,这种与属性动画的回调调用 invalidate()源码分析有什么区别?

2. 在 onDraw 里面调用 invalidate() 会存在什么问题?

在View.onDraw方法里调用View.invalidate和在ValueAnimator.AnimatorUpdateListener中调用View.invalidate,有区别吗?

了解ValueAnimator的同学会知道,它播放动画的实现原理并不是直接使用线程来不断计算并回调AnimatorUpdateListener,而是。。。来写代码测试下就知道了:

ValueAnimator.ofInt(1).apply 
    addUpdateListener 
        // 因为animatedFraction=0时是直接回调的
        if (it.animatedFraction > 0) 
            throw RuntimeException()
        
    
    start()

代码很简单,随便创建一个ValueAnimator然后在它的UpdateListener里面去抛出一个异常。

看看堆栈信息:

E/androidRuntime: FATAL EXCEPTION: main
    Process: com.wuyr.wanandroidqa, PID: 16027
    java.lang.RuntimeException
        at com.wuyr.wanandroidqa.activities.main.TestActivity$start$1$1.onAnimationUpdate(TestActivity.kt:112)
        at android.animation.ValueAnimator.animateValue(ValueAnimator.java:1566)
        at android.animation.ValueAnimator.animateBasedOnTime(ValueAnimator.java:1357)
        at android.animation.ValueAnimator.doAnimationFrame(ValueAnimator.java:1489)
        at android.animation.AnimationHandler.doAnimationFrame(AnimationHandler.java:146)
        at android.animation.AnimationHandler.access$100(AnimationHandler.java:37)
        //
        //   3
        //
        at android.animation.AnimationHandler$1.doFrame(AnimationHandler.java:54)
        //
        //   2
        //
        at android.view.Choreographer$CallbackRecord.run(Choreographer.java:970)
        at android.view.Choreographer.doCallbacks(Choreographer.java:796)
        //
        //   1
        //
        at android.view.Choreographer.doFrame(Choreographer.java:727)
        at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:957)
        at android.os.Handler.handleCallback(Handler.java:938)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7656)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)

看标记1处,来到了Choreographer.doFrame方法,View的绘制,各种输入/触摸事件等也是在这里开始处理的。

接着看2,Choreographer.java第970行:

private static final class CallbackRecord 
    public Object action; // Runnable or FrameCallback

    public void run(long frameTimeNanos) 
            ......
            ((FrameCallback)action).doFrame(frameTimeNanos); // 970行
            ......
    

这里把action强转为FrameCallback,而标记3处:

public class AnimationHandler 
    private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() 
        @Override
        public void doFrame(long frameTimeNanos) 
            doAnimationFrame(getProvider().getFrameTime()); // 54行
            if (mAnimationCallbacks.size() > 0) 
                getProvider().postFrameCallback(this);
            
        
    ;

可以看到它回调的正是AnimationHandler的mFrameCallback。

但是ValueAnimator怎么会跟AnimationHandler扯上关系呢?

其实在我们调用start方法播放动画的时候,它就已经把一个Callback添加到AnimationHandler里面去了:

public class ValueAnimator extends Animator implements AnimationHandler.AnimationFrameCallback 

    @Override
    public void start() 
        start(false);
    

    private void start(boolean playBackwards) 
        ......

        addAnimationCallback(0);

        ......
    

     private void addAnimationCallback(long delay) 
        AnimationHandler.getInstance().addAnimationFrameCallback(this, delay);
    

    /*
        这里实现AnimationHandler.AnimationFrameCallback接口的方法
    */
    @Override
    public final boolean doAnimationFrame(long frameTime) 
        ......
    

这个Callback正是ValueAnimator自身。

那它最终会被传到哪里呢?

代码套那么多层就不全贴了,它最终会通过Choreographer.postFrameCallback方法:

public final class Choreographer 

    public void postFrameCallback(FrameCallback callback) 
        postFrameCallbackDelayed(callback, 0);
    

    public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) 
        ......
        postCallbackDelayedInternal(CALLBACK_ANIMATION,
                callback, FRAME_CALLBACK_TOKEN, delayMillis);
    

传到了Choreographer中。

可以看到它最后调用的是postCallbackDelayedInternal方法,记住!这个很重要!

好,回到主题。

通过刚刚一段分析,可以知道,ValueAnimator.AnimatorUpdateListener,是在Choreographer.doFrame回调时才回调的。也就是说,ValueAnimator开始后,AnimatorUpdateListener会在每一次屏幕刷新的时候回调!

还有一个区别就是,动画进度计算方式不同,ValueAnimator是根据记录的开始时间来计算的,所以它不会受到Activity生命周期或其他因素影响。而直接在onDraw里回调的就不同了,如果动画在播放过程中Activity Stopped了,onDraw就会暂停回调,那么下一次的invalidate时间,也就无从确定了。不过,可能刚好有这样的需求,需要在Activity不可见时暂停动画呢?

在View.onDraw中直接调用invalidate方法会有什么问题?

看了*@xujiafeng*同学的回答,他说这样做的话,IdleHandler不会被回调。公众号文章链接在这里:

Android 避坑指南:实际经历来说说IdleHandler的坑

emmmm,其实我觉得这不应该是一个问题,因为Handler的机制就是这样的啊,MessageQueue还有事情没处理完,肯定不会告诉你说它有空啦。

等动画播放完毕,IdleHandler还是会正常回调的。

不过你说是要无限循环播放的话,让MessageQueue一直忙碌,导致IdleHandler一直没能被回调的话,那确实是个问题,就拿常见的场景来说:每日一问 | Activity 调用了finish()方法会立即调用onDestory()吗? ,Activity的Destory也是依赖IdleHandler来完成的(虽然有超时机制)。(以后会跟大家一起debug AMS来详细分析这个问题)

https://www.wanandroid.com/wenda/show/13244

如果真的有这样的需求,除了改用ValueAnimator之外,就没其他方法了吗?

肯定有啦,你想想ScrollView、RecyclerView、ViewPager等等这些View的惯性滚动动画效果是怎么做的?

它们其实是通过一个叫postInvalidateOnAnimation的方法来invalidate的,关于这个方法,我记得在前面好几个回答都提到过了。

来看下它原理是怎么样的吧:

长话短说,它最终是调用Choreographer.postCallback方法来把一个会调用View.invalidate的Runnable传进去:

public final class Choreographer 
    public void postCallback(int callbackType, Runnable action, Object token) 
        postCallbackDelayed(callbackType, action, token, 0);
    

    public void postCallbackDelayed(int callbackType, Runnable action, Object token, long delayMillis) 
        ......
        postCallbackDelayedInternal(callbackType, action, token, delayMillis);
    

!!!!!看到了没?!它最终调用的是postCallbackDelayedInternal方法!还记得刚刚分析ValueAnimator的时候,叫记住的那个方法吗?就是它啊!

这就说明了,使用View.postInvalidateOnAnimation方法,跟在ValueAnimator.AnimatorUpdateListener中调用invalidate,效果是一样的!

同样是调用invalidate方法,为什么在AnimatorUpdateListener.onAnimationUpdate里面调用,就不会阻止IdleHandler回调呢?

看图就明白了,这是在onDraw里调用invalidate的流程:

就算MQ里没有其他的msg,在每次Traversal任务即将处理完毕时又向MQ塞入了新的msg,所谓一波未平,一波又起,这样的话,IdleHandler肯定没机会回调了。

来看下在AnimatorUpdateListener中调用invalidate的流程:

因为AnimatorUpdateListener的onAnimationUpdate方法是每次屏幕刷新时才回调的,也就是大概16ms左右,在这16ms的间隔内,Looper可能已经把MQ里剩下的msg都取出来了,所以如果在AnimatorUpdateListener里调用invalidate的话,会看到这样的log:

onAnimationUpdate: invoked
onAnimationUpdate: invoked
onDraw: invoked
queueIdle: invoked
onAnimationUpdate: invoked
onDraw: invoked
queueIdle: invoked
onAnimationUpdate: invoked
onDraw: invoked
queueIdle: invoked
onAnimationUpdate: invoked
onDraw: invoked
queueIdle: invoked
onAnimationUpdate: invoked
onDraw: invoked
queueIdle: invoked

以上是关于Android 关于绘制的一个小细节分享的主要内容,如果未能解决你的问题,请参考以下文章

Android -- 自定义View小Demo,关于Rect绘制Android机器人

关于Android Framework源码阅读——阿里大佬分享的核心经验

10 个案例分享几个 Python 可视化小技巧,助你绘制高质量图表

Android-View的绘制源码学习总结

Android -- 自定义View小Demo,关于Path类的使用

Android应用优化小手册