很多时候我们讨论属性动画与之前的View Animation之间的区别,很多同学都会说:
“属性动画在移动后,仍然可以响应用户的点击”
原因可以看这篇:每日一问 | 为什么属性动画移动一个控件后,目标位置仍然能响应用户事件?
了解的同学应该清楚,能响应只是在事件分发的时候,做了一下坐标映射。
今天,我们讨论另一个区别,就是当属性动画与硬件加速配合时,会摩擦出什么火花?
看一个示例:
MyTextView tv =view.findViewById( R.id.tv_name);
tv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ValueAnimator valueAnimator = ValueAnimator.ofInt(0, 300).setDuration(2000);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
v.setTranslationY((int) animation.getAnimatedValue());
}
});
valueAnimator.start();
}
});
很简答,我们自定义一个TextView,点击的时候,对它做一点往下的移动。
布局文件:
<?xml version="1.0" encoding="utf-8"?>
<com.example.zhanghongyang.kotlinlearn.view.MyFrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.zhanghongyang.kotlinlearn.view.MyTextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="hello" />
</com.example.zhanghongyang.kotlinlearn.view.MyFrameLayout>
当硬件加速开启时(默认就是开启的):
- 动画执行过程中:MyTextView的draw方法会回调吗?
- 动画执行过程中:MyFrameLayout的dispatchDraw方法会回调吗?
当硬件加速关闭时:
- 动画执行过程中:MyTextView的draw方法会回调吗?
- 动画执行过程中:MyFrameLayout的dispatchDraw方法会回调吗?
如果两者有差异:
- v.setTranslationY在开启和不开启硬件加速过程中,执行的逻辑到底有什么变化?
更多问答 >>
-
每日一问 | 当Unsafe遇上final,超神奇的事情发生了?
2020-11-02 00:16 -
每日一问 | Call requires API level 23 (current min is 14) 扫描出来的原理是?
2020-12-27 22:39 -
每日一问 | View invalidate() 相关的一些细节探究~
2020-12-27 22:38 -
每日一问 | RxJava中Observable、Flowable、Single、Maybe 有何区别?
2021-01-03 20:34 -
每日一问 | Java中匿名内部类写成 lambda,真的只是语法糖吗?
2021-01-11 00:00 -
每日一问 | 关于 RecyclerView$Adapter setHasStableIds(boolean)的一切
2020-10-26 23:44 -
每日一问 | 玩转 Gradle,可不能不熟悉 Transform,那么,我要开始问了。
2020-10-26 23:45 -
每日一问 | 启动了Activity 的 app 至少有几个线程?
2020-10-12 00:47 -
2020-10-03 11:43
-
2020-09-09 23:54
测试结果如下:
为什么会这样呢?
先来了解下硬件加速的开启与关闭,对视图绘制的分派逻辑有什么影响吧:自定义一个View,在重写的onDraw
方法中打印一下堆栈信息:运行看下log,先是关闭硬件加速的:
我们从ViewRootImpl.draw开始往上看:ViewRootImpl的
Parent.draw -> Parent.dispatchDraw -> Parent.drawChild -> Child.draw -> ...... -> Child.onDrawdraw
方法调用了drawSoftware
,然后会回调DecorView.draw
,接下来就是:好,现在来看下开启了硬件加速后的堆栈:
很明显,在ViewRootImpl.draw时,原来的
Parent.updateDisplayListIfDirty -> Parent.dispatchDraw -> Parent.drawChild -> Child.draw -> ...... -> Child.onDrawdrawSoftware
变成了ThreadedRenderer.draw
,而且DecorView.draw
是由updateDisplayListIfDirty
方法回调的,之后的分派逻辑是:小结
在开启硬件加速的情况下,View的dispatchDraw
是由View.updateDisplayListIfDirty
方法来回调的,源头是ThreadedRenderer.draw
;关闭硬件加速后,dispatchDraw
是在View.draw
方法中调用的,源头是ViewRootImpl.drawSoftware
。为什么调用View的
在开头的测试中发现,就算是开启了硬件加速,OnDrawListener也依然会在setTranslationX/Y/Z
、setRotationX/Y/Z
、setScaleX/Y
这一系列方法,在硬件加速下,不会导致View的draw
方法回调呢?明明内容有更新的setTranslationY
之后回调。这就说明调用setTranslationY
方法并不是真的没有发起重绘,可能只是在回调dispatchDraw
之前被打断了而已,毕竟dispatchDraw
前面还有那么多个方法。嗯,那现在就借助Debug来检查下,这个分派流程究竟是在哪里被打断了的:
先在
你会发现,每一层View的mAttachInfo.mThreadedRenderer.draw
那里打个断点,等调用setTranslationY
后,顺着刚刚的堆栈方法一路走下去……updateDisplayListIfDirty
依然会被调用!而draw
方法刚好是在这方法里面回调的,看下它的代码吧:先看第4处标记,这段代码一共有3个分支,但无论最终走的是哪一个,
也就是说,只要进入了2,且没有在3里面return,那么draw
方法都是会被回调的(buildDrawingCache
和dispatchDraw
里面也会调用draw
方法)。draw
方法就会回调了。由此可以总结出回调draw
方法的必要条件:目标View的
canHaveDisplayList
方法返回true;目标View的
mPrivateFlags
没有PFLAG_DRAWING_CACHE_VALID标记;RenderNode的
hasDisplayList
返回false或者mRecreateDisplayList
为true;好,那先看下
canHaveDisplayList
何时为true吧:如果View已经Attach到了Window,并且硬件加速是开启状态,那么这个方法就会返回true。
这个条件,即使什么都不用做也能满足了,下一个:RenderNode的hasDisplayList
何时为true何时为false呢?它里面调用的是native方法,这里就不扯太多了,直接说结论吧:在View draw过一次之后,hasDisplayList
就会一直返回true,直到这个View被Detached为止。结合题目中的场景,是点击了View之后才播放的动画,那么这时候hasDisplayList
的返回值肯定是true了。emmmm,现在来看,回调draw
方法其实只需要2个条件:mPrivateFlags
没有PFLAG_DRAWING_CACHE_VALID标记;mRecreateDisplayList
为true;咦等等,上面
看一下:这个方法在View中是空白的,但是ViewGroup重写了它:updateDisplayListIfDirty
方法的代码,标记3的if里面调用了一个dispatchGetDisplayList
方法,如果进入了这里,draw
方法还有可能被回调吗?在这里会遍历所有子View,如果子View当前可见,或者set有Animation的话,就会调用
recreateChildDisplayList
方法,并把对应子View传了进去:咦?!
在这里居然会调用子View的updateDisplayListIfDirty
!它在调用updateDisplayListIfDirty
之前还做了一件事:更新目标子View的mRecreateDisplayList
的值:如果目标子View.mPrivateFlags
中有PFLAG_INVALIDATED标记,mRecreateDisplayList
就是true!反之为false,接着会把PFLAG_INVALIDATED标记去掉。也就是说,目标子View.mPrivateFlags
有没有PFLAG_INVALIDATED标记会直接影响draw
方法的回调!!!那现在回调draw
方法的条件可以改为:目标子View的mPrivateFlags
有PFLAG_INVALIDATED标记,并且没有PFLAG_DRAWING_CACHE_VALID标记。想一下,我们手动调用
看看:invalidate
,肯定能让draw
方法回调的,那是不是invalidate
方法会对mPrivateFlags
的值进行修改,使它能满足上面的条件呢?妈耶!!!还真的是!
invalidate()
调用的是invalidate(boolean invalidateCache)
方法,而且参数invalidateCache
是写死为true的;invalidate(boolean invalidateCache)
里面会调用invalidateInternal
,在invalidateInternal
中,如果参数invalidateCache
为true的话,就会给mPrivateFlags
加入PFLAG_INVALIDATED标识,并去掉PFLAG_DRAWING_CACHE_VALID标识!!!这样不就跟我们分析出来的那2个条件完全匹配了嘛!照这样看来,
还是看看代码吧:setTranslationY
里面肯定不会调用invalidate()
的了,但如果不调用invalidate()
,它怎么发起重绘呢?在
mRenderNode.setTranslationY
的前后,都分别调用了一次invalidateViewProperty
方法:如果没有开启硬件加速,或者RenderNode的
不过,代入题目中的场景(开启硬件加速),这几个条件肯定都不满足了,所以这里会进入else,也就是调用hasDisplayList
返回false,还有当前正在播放补间动画的话,就会进入if里面。damageInParent
:它会调用Parent的
onDescendantInvalidated
方法:首先是检查子View的
去掉PFLAG_DRAWING_CACHE_VALID标识?那样就满足回调mPrivateFlags
除PFLAG_DIRTY_MASK之外还有没有其他标识(这个肯定是有的),如果有的话就会把自己的mPrivateFlags
去掉PFLAG_DRAWING_CACHE_VALID标识draw
的第一个条件了。在updateDisplayListIfDirty
方法中,如果mPrivateFlags
没有PFLAG_DRAWING_CACHE_VALID标识,就能进入上面的标记2处,要是此时的mRecreateDisplayList
为false的话,还会进入标记3处,调用dispatchGetDisplayList
方法后return!捋一捋:子View调用setTranslationY
之后,会使父容器的mPrivateFlags
去掉PFLAG_DRAWING_CACHE_VALID标识并且发起重绘请求。等到下一次父容器的updateDisplayListIfDirty
方法被回调时,如果mRecreateDisplayList
为true则回调draw
,否则根据子View的mPrivateFlags
有没有PFLAG_INVALIDATED标识而决定回不回调子View的draw
方法。好,先暂停,我们把onDescendantInvalidated
的代码先看完,看看最后有没有给mPrivateFlags
加入PFLAG_INVALIDATED:方法的最后,可以看到它会递归调用,那么,最顶层的Parent是谁呢?就是ViewRootImpl了嘛!ViewRootImpl它并不是View,只是实现了ViewParent接口而已,这个onDescendantInvalidated
也是ViewParent接口的方法来的。看下ViewRootImpl是怎么实现的:它最终会调用
到此,整个scheduleTraversals
方法(收到屏幕刷新的通知后就会调用performDraw
开始绘制流程)。onDescendantInvalidated
的流程就执行完了,可以看到并没有给mPrivateFlags
加入PFLAG_INVALIDATED,这就印证了setTranslationY
不会回调draw
的说法。还有最后一个条件:mRecreateDisplayList
,在updateDisplayListIfDirty
被调用时,它如果为true就会直接回调draw
,否则进入dispatchGetDisplayList
,在里面调用每一个子View的updateDisplayListIfDirty
……其实,这个变量在每次draw
方法结束前,都会重置为false:也就是在每一次
updateDisplayListIfDirty
之前,它都是false。好啦,现在已经把所有问题都搞清楚了,来做个总结吧:
为什么调用View的setTranslationX/Y/Z
、setRotationX/Y/Z
、setScaleX/Y
这一系列方法,在硬件加速下,不会导致View的draw
方法回调呢?在硬件加速开启后,View.draw
方法的回调主要取决于两个条件:View的
mPrivateFlags
是否 有 PFLAG_INVALIDATED 标记;View的
mPrivateFlags
是否 没有 PFLAG_DRAWING_CACHE_VALID 标记;只要这两个条件同时满足,
draw
方法才会回调。而我们常用的invalidate
方法,就有对应的操作:去掉PFLAG_DRAWING_CACHE_VALID并加入PFLAG_INVALIDATED。setTranslationXYZ
等方法,虽然最终也会发起重绘请求,但因为没有对mPrivateFlags
的值进行修改,所以在下一次的绘制流程中,会因条件不足而不回调draw
方法。@陈小缘 回答的太棒了,我这边补充一些我的理解。
首先要了解到,软/硬绘制的目标是不同的,软绘的目标 Canvas 其实背后是一个 Bitmap 位图,而硬绘的目标背后是 DisplayList。
DisplayList 中记录并不是绘制的结果,而是一组绘制的指令,记录了先绘制什么,然后经过什么变换,最后绘制什么。打个比方,例如我们用地图 App,如果搜索的是 A 地址到 B 地址,那么我们得到的是个路线图,这类似软绘得到的 Bitmap。而如果我们在开车的场景下切换到导航模式,它就会“提示前方多少米左转、多少米有个红灯”等,这就是类似硬绘得到的指令。
硬件绘制的流程,包含 2 个步骤:「录制 + 回放」。其中只有步骤一「录制」阶段,需要 View 的 draw() 参与,因为这里要记录 View 的绘制步骤,并编译为指令,后续只需要回放即可还原绘制内容,而这个绘制指令,是可以修改的。
那么我们在硬件加速模式下,View 首次绘制的时候,DisplayList 中已经记录了绘制的指令,后续我们通过 setTranslationY() 等操作,实际上只需要修改绘制指令即可,而无需真实的触发 View 的绘制(draw),这样在回放阶段,就可以完成新效果的渲染。这也就是为什么 MyTextView 的 draw() 不会触发的原因。
结论:硬件绘制的指令,是可以编辑的,这一步无需 draw() 参与,即不会触发 draw()。
还是这个问题的场景下,实际上 MyTextView 的内容并没有变化,软绘下每次还要经历重绘,有点浪费资源。那么软绘下有没有办法复用绘制内容?
其实也是可以的,我们只需要增加绘制缓存即可,将 MyTextView 中增加 layerType = software 属性,还是同样的操作,播放动画的时候,只会回调 MyFrameLayout 的 dispatchDraw() 而不会回调 MyTextView 的 draw(),因为此时有绘制缓存的参与,MyTextView 并不需要重绘。
结合上面大佬的流程分析 再加上美团这篇Android硬件加速原理与实现简介 https://tech.meituan.com/2017/01/19/hardware-accelerate.html , 硬件加速流程就清晰多了
draw(MyFrameLayout)→dispatchDraw(MyFrameLayout)→draw(MyTextView)
开启不开启硬件加速 都是 draw(MyTextView)里面这个这个步骤里面去区分的吧..不是呀