在上篇文章 属性动画(一) 中已经对属性动画有了基本的介绍,本篇文章将对属性动画中稍微高级点的内容进行介绍,主要介绍下图中绿色部分标明的知识点。
1. Evaluators
Evaluator
是干什么用的呢?简单来说,它是用于告诉动画系统,某种类型的属性值怎么从初始值变化到结束值的。
1.1 TypeEvaluator
解析
系统中自带一些默认的的 Evaluator
,比如:IntEvaluator
、FloatEvaluator
和 ArgbEvaluator
等等,我们简单分析一下其中的 IntEvaluator
的源码。
public class IntEvaluator implements TypeEvaluator<Integer> {
public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
int startInt = startValue;
return (int)(startInt + fraction * (endValue - startInt));
}
}
IntEvaluator
实现了 TypeEvaluator
接口,重写其中的 evaluate(float fraction, Integer startValue, Integer endValue)
方法。该方法中有三个参数值:
-
float fraction
:表示动画的完成度,用于计算此时属性的值 -
Integer startValue
:属性值的初始值 -
Integer endValue
:属性值的结束值
evaluate(float fraction, Integer startValue, Integer endValue)
方法内部的实现也很简单:用最终值减去初始值再乘以完成度的,加上初始值,就是动画当前的属性值。
1.2 自定义 Evaluator
仿照 IntEvaluator
,我们可以自定义 Evaluator
。
假设我们现在有一个自定义 View
,其颜色是是从纯绿色渐变为纯红色,那该怎么实现的?答案是我们可以通过自定义 Evaluator
来实现。
假设自定义View
的代码如下:
public class CustomView extends View {
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
int color = 0xff00ff00;
public CustomView(Context context) {
super(context);
}
public CustomView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public int getColor() {
return color;
}
public void setColor(int color) {
this.color = color;
invalidate();
}
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
paint.setColor(color);
canvas.drawCircle(width / 2, height / 2, width / 6, paint);
}
}
上述自定义 View
的代码其实很简单,根据设置的大小,画一个默认是绿色的圆。
需要注意的是,在该自定义
View
中,如果要改变color
颜色的属性,该属性需要有一个对应的setter
的方法,并在其中调用invalidate()
方法,保证color
属性变化的时候,都会重新调用onDraw(Canvas canvas)
重新绘制该View
。
那接下来该实现颜色变化的自定义 Evaluator
了,具体代码如下所示:
public class HsvEvaluator implements TypeEvaluator<Integer> {
private float[] startColor;
private float[] endColor;
private float[] midColor;
public HsvEvaluator() {
startColor = new float[3];
endColor = new float[3];
midColor = new float[3];
}
// 重写 evaluate() 方法,让颜色按照 HSV 来变化
@Override
public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
Color.colorToHSV(startValue, startColor);
Color.colorToHSV(endValue, endColor);
midColor[0] = startColor[0] + fraction * (endColor[0] - startColor[0]);
midColor[1] = startColor[1] + fraction * (endColor[1] - startColor[1]);
midColor[2] = startColor[2] + fraction * (endColor[2] - startColor[2]);
Integer integer = Color.HSVToColor(midColor);
return integer;
}
}
在其中,主要是重写 evaluate(float fraction, Integer startValue, Integer endValue)
方法,通过 Color.colorToHSV(int color, float hsv[])
方法,将 Integer
类型的颜色值转换为保存在长度为 3 的 float
数组中,其中每一位对应的是该颜色的 HSV
值。
然后,通过完成度的值 fraction
,计算出当前时刻的颜色属性的 HSV
值,并保存在对应的数组中,最后通过 Color.HSVToColor(float hsv[])
方法转换成 Integer
类型的颜色值,并返回。
那实现该动画的代码如下所示:
CustomView view = (CustomView) findViewById(R.id.objectAnimatorView);
ObjectAnimator animator = ObjectAnimator.ofInt(view, "color", 0xffff0000, 0xff00ff00);
animator.setEvaluator(new HsvEvaluator()); // 使用自定义的 HsvEvaluator
animator.setInterpolator(new LinearInterpolator());
animator.setDuration(2000);
animator.start();
2. Interpolators
Interpolator
直译过来叫做 插值器
,定义属性动画的变化率,Interpolator
允许动画不是线性变化的,而是非线性变化的,比如加速变化、减速变化等等。
系统已经默认提供了许多实现了 Interpolator
的类:
BaseInterpolator
LinearInterpolator
AccelerateDecelerateInterpolator
AccelerateInterpolator
AnticipateInterpolator
AnticipateOvershootInterpolator
BounceInterpolator
......
2.1 TimeInterpolator
Interpolator
都实现了接口 TimeInterpolator
,它的源码还是非常简单的,如下所示:
/**
* A time interpolator defines the rate of change of an animation. This allows animations
* to have non-linear motion, such as acceleration and deceleration.
*/
public interface TimeInterpolator {
/**
* Maps a value representing the elapsed fraction of an animation to a value that represents
* the interpolated fraction. This interpolated value is then multiplied by the change in
* value of an animation to derive the animated value at the current elapsed animation time.
*
* @param input A value between 0 and 1.0 indicating our current point
* in the animation where 0 represents the start and 1.0 represents
* the end
* @return The interpolation value. This value can be more than 1.0 for
* interpolators which overshoot their targets, or less than 0 for
* interpolators that undershoot their targets.
*/
float getInterpolation(float input);
}
其中只有一个方法,getInterpolation(float input)
,按照其注释,不难理解。
- 输入的
float
参数input
是随着动画的运行均匀变化的,范围是0
到1
,开始时是0
,结束时是1
。 - 其返回值,则是由输入的
input
计算得到的。返回值可以比1
大,表示动画属性值超过设定的结束值,也可以比1
小,表示动画属性值小于设定的结束值。 - 其实,
getInterpolation(float input)
的返回值,就是之前提到的动画完成度 ----fraction
我们简单分析下 LinearInterpolator
匀速插值器的源码,如下所示:
/**
* An interpolator where the rate of change is constant
*/
@HasNativeInterpolator
public class LinearInterpolator extends BaseInterpolator implements NativeInterpolatorFactory {
public LinearInterpolator() {
}
public LinearInterpolator(Context context, AttributeSet attrs) {
}
public float getInterpolation(float input) {
return input;
}
/** @hide */
@Override
public long createNativeInterpolator() {
return NativeInterpolatorFactoryHelper.createLinearInterpolator();
}
}
可以看到,其中的 getInterpolation(float input)
方法的实现非常简单,直接将输入的值 input
作为结果返回,则实现了匀速变化的效果。
再简单分析下 AccelerateDecelerateInterpolator
的源码,如下所示:
public class AccelerateDecelerateInterpolator extends BaseInterpolator
implements NativeInterpolatorFactory {
public AccelerateDecelerateInterpolator() {
}
@SuppressWarnings({"UnusedDeclaration"})
public AccelerateDecelerateInterpolator(Context context, AttributeSet attrs) {
}
public float getInterpolation(float input) {
return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f;
}
/** @hide */
@Override
public long createNativeInterpolator() {
return NativeInterpolatorFactoryHelper.createAccelerateDecelerateInterpolator();
}
}
通过数学做图工具,得到如下图所示的示意图:
可以看到:
- 在
0.0
到0.3
之间时,y
轴随x
轴变化较慢 - 在
0.3
到0.7
之间时,y
轴随x
轴变化较快 - 在
0.7
到1.0
之间时,y
轴随x
轴变化较又变慢
所以它是一个先加速再减速的过程
2.2 自定义 TimeInterpolator
通过对 LinearInterpolator
和 AccelerateDecelerateInterpolator
源码的分析,我们也可以实现一个自定义的 Interpolator
。
我们可以实现一个Interpolator
。如下所示:
public class DecelerateAccelerateInterpolator implements BaseInterpolator {
@Override
public float getInterpolation(float input) {
return (float) Math.sin(input * Math.PI);
}
}
使用数学做图工具,可以得到如下所示的变化示意图:
可以看到:
- 在
0.0
到0.5
之间时,y
轴随x
轴变化慢慢减缓 - 在
0.5
到1.0
之间时,y
轴随x
轴变化慢慢加速 - 在时间走到一半的时候,属性值已经到达了设定的结束值,然后在剩下的一半时间内,属性值又从设定的结束值恢复到初始值。
3.Animations in XML
属性动画允许在 xml
文件中定义动画,而不是通过 Java
代码的形式。使用 xml
定义动画,不仅使得重复利用相同的动画更加方便,而且也更容易修改动画。
为区分在 xml
中定义的视图动画,属性动画的 xml
文件放在 res/animator/
目录下。
属性动画在 xml
中支持以下几个标签:
-
ValueAnimator
--<animator>
-
ObjectAnimator
--<objectAnimator>
-
AnimatorSet
--<set>
可以在 这儿Animation Resources 找到属性动画中的所有属性。
以下代码是一个简单的 xml
属性动画的例子:
<set android:ordering="sequentially">
<set>
<objectAnimator
android:propertyName="x"
android:duration="500"
android:valueTo="400"
android:valueType="intType"/>
<objectAnimator
android:propertyName="y"
android:duration="500"
android:valueTo="300"
android:valueType="intType"/>
</set>
<objectAnimator
android:propertyName="alpha"
android:duration="500"
android:valueTo="1f"/>
</set>
在 xml
中定义好属性动画之后,在 Java
代码中使用该动画的话,需要先将该动画集合加载进来,之后调用 setTarget()
为该动画设置目标对象即可,如下所示:
AnimatorSet set = (AnimatorSet) AnimatorInflater.loadAnimator(myContext,
R.anim.property_animator);
set.setTarget(myObject);
set.start();
在 xml
中也可以通过使用 <animator>
标签定义 ValueAnimator
动画,如下所示:
<animator xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="1000"
android:valueType="floatType"
android:valueFrom="0f"
android:valueTo="-100f" />
在 Java
代码中使用上面定义的 ValueAnimator
动画,也需要将该 xml
动画加载进来,如下所示:
ValueAnimator xmlAnimator = (ValueAnimator) AnimatorInflater.loadAnimator(this,R.animator.animator);
xmlAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator updatedAnimation) {
float animatedValue = (float)updatedAnimation.getAnimatedValue();
textView.setTranslationX(animatedValue);
}
});
xmlAnimator.start();
关于在 xml
中定义属性动画更多的信息,请查阅 Animation Resources。
4.硬件加速
从 Android 3.0(API level 11),Android 支持硬件加速绘制 2D
渲染,意思就是说在 View
上进行的一些 canvas
操作都使用 GPU
来实现。因为使用硬件加速需要更多的资源,所以会造成内存增加的后果。
在 API level
大于 14
的时候,硬件加速默认是打开的。如果你的应用只会绘制标准的 View
和 Drawable
对象,全局都打开硬件加速并不会造成什么不好的影响。但是硬件加速并不是支持所有的的 2D
绘制操作的,打开硬件加速可能会造成你的自定义控件和一些绘制操作出现异常。为避免造成异常,Android 可以在几个不同的层级上对硬件加速进行开关的控制。
如果开启硬件加速之后,自定义控件和自定义绘制出现了问题,可以在真机上打开硬件加速进行测试,找出问题所在。
4.1 控制硬件加速的开关
总共可以在四个层级上对硬件加速进行控制,如下:
Application
Activity
Window
View
4.1.1 Applicatin
层级
在 AndroidManifest.xml
文件中,可以通过在 application
中使用以下元素对硬件加速进行控制
<application android:hardwareAccelerated="true" ...>
4.1.2 Activity
层级
如果在 Application
层级对硬件加速控制的不够精细,可以对某个单独的 Activity
中的硬件加速进行开关的控制,如下所示:
<application android:hardwareAccelerated="true">
<activity ... />
<activity android:hardwareAccelerated="false" />
</application>
上面代码的意思是,在应用的全局都打开硬件加速,但是在这个 Activity
中关闭了硬件加速。
4.1.3 Window
层级
可以对单独的某个 Window
进行硬件加速的控制,如下代码所示:
getWindow().setFlags(
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
对 Window
来说,只能打开硬件加速,而不能关闭硬件加速。
4.1.4 View
层级
使用以下代码,可以在运行的时候,把该 View
的硬件加速关闭
myView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
对 View
来说,只能关闭硬件加速,而不能打开硬件加速。
4.2 检测硬件加速是否打开
如果在应用中自定义控件和自定义绘制有很多的话,检查硬件加速是否处于打开状态还是非常有必要的,有以下两种方式可以判断:
View.isHardwareAccelerated();
Canvas.isHardwareAccelerated();
-
View.isHardwareAccelerated()
如果返回true
可以说明此View
依附的Window
的硬件加速是打开的 -
Canvas.isHardwareAccelerated()
如果返回true
则说明该canvas
对象的硬件加速是打开的。
4.3 Android 绘制模式
如果硬件加速开启,Android FrameWork 会使用新的绘制模式将应用显示在屏幕上,这种新的绘制模式中使用到了一个关键的概念 ---- display lists
。
为了完全理解硬件加速相关的知识,首先理解 Android 不使用硬件加速是的软件绘制模式是非常有帮助的。
4.3.1 软件绘制模式
在软件绘制模式中,将 views
绘制到屏幕上需要以下两个步骤:
Invalidate the hierarchy
Draw the hierarchy
当应用需要更加 UI
中的某一部分时,有改变的 View
(脏区域)需要调用 invalidate()
,invalidation
信号就会遍历 View
树来计算哪些 views
需要重新绘制,然后 Android 系统会将与这些 views
相关的 views
都进行重新绘制。这种绘制模式有两个缺点:
- 这种模式在进行每一次重新绘制的时候,都会进行大量的代码计算。比如,在界面中有一个
button
在另一个view
之上,该button
调用了invalidate()
方法,那么 Android 系统也会将该button
之下的view
进行重新绘制。 - 另一个问题是,软件绘制模式会有一些隐藏的 bug。比如,有一个
view
的重新绘制依赖于另一个与其相关的view
的重新绘制,当另一个view
重新绘制的操作去掉之后,这个view
的绘制工作也就不会被进行,所以可能会怀疑此view
的代码有什么问题,这样就带来了问题。所以最好的方式是,在有所改变的自定义view
中,主动去调用invalidate()
方法进行重新绘制。
4.3.2 硬件加速绘制模式
在硬件加速绘制模式中,还是调用 incalidate()
方法和 draw()
方法进行重新绘制的,但是实际上处理绘制的操作还是有所不同的。
在硬件加速绘制模式中,Android 系统不会立即执行绘制指令,而是会先将其记录在 display lists
中,在 display lists
中存储着 view
树的绘制代码,Android 系统只需要将调用了 invalidate()
方法的 views
的绘制代码,记录和更新到 display lists
中即可。那些没有调用 incalidate()
方法的 views
,只需要将原来存储于 display lists
中的绘制代码简单地绘制出来即可。
硬件加速绘制模式包括以下三个步骤:
Invalidate the hierarchy
Record and update display lists
Draw the display lists
使用这种方式,就不能再使用与脏区域
相关的特性而执行本 view
的 draw()
方法了。为了使 Android 系统记录和更新一个 view
在 display lists
的绘制代码,必须调用该 view
的 invalidate()
方法了,如果忘记调用 incalidate()
方法,则该 view
不会发生什么变化。
使用 display lists
对属性动画的性能也有很大的好处,一些属性(比如:透明度、旋转角度)并不需要调用 invalidate()
方法即可完成变化,它完全可以自动完成计算。这种特性也会应用在一些普通 View
的操作上。比如,现在有一个 LinearLayout
包含一个 ListView
和一个 Button
,此时该 LinearLayout
的 display lists
是这样的:
DrawDisplayList(ListView)
DrawDisplayList(Button)
假如现在改变 ListView
的透明度,调用 ListView
的 setAlpha(0.5f)
方法,那么此时,该 LinearLayout
的 display lists
如下所示:
SaveLayerAlpha(0.5)
DrawDisplayList(ListView)
Restore
DrawDisplayList(Button)
复杂的 ListView
的绘制操作计算并没有被执行,相反,系统只需要更新该 LinearLayout
的 display lists
即可。如果使用软件绘制模式,就会重新计算 ListView
和 Button
的绘制操作。
4.4 不支持硬件加速的绘制操作
大部分的 Canvas
的方法都支持硬件加速,但是有一些方法是从某些版本之后才开始支持硬件加速的,如下表所示:
4.5 View Layers(视图层级)
在 Android 所有的版本中,View
都有能力实现离屏缓冲,实现离屏缓冲的方式:
- 可以通过
View
的绘制缓存 - 通过
Canvas.saveLayer()
的方式
离屏缓冲有一些很好的用途:当进行于复杂视图的动画,或者一些组合效果时,使用离屏缓冲会有更好的性能。比如实现一个渐变的效果,通过使用 Canvas.saveLayer()
先临时将 view
缓冲到 layers
(层级)上,然后再通过一个不透明因子将其绘制到屏幕上。
从 Android 3.0 (API level 11)开始,通过使用 View.setLayerType()
方法,对什么时候以及怎样使用视图层级有了更高的控制权限。在该方法中有两个参数:
- 一个是视图的层级类型
-
LAYER_TYPE_NONE
:View
进行普通的渲染,并且不会使用离屏缓冲,这是默认参数 -
LAYER_TYPE_HARDWARE
:如果应用启动了硬件加速,则使用硬件将View
绘制到hardware texture
上。如果硬件加速没有开启,则视图层级的操作和使用LAYER_TYPE_SOFTWARE
参数时的视图操作是一致的 -
LAYER_TYPE_SOFTWARE
:使用软件将该View
先绘制到一个Bitmap
对象上
-
- 另一个是可选的
Paint
对象,该Paint
对象可以用于描述怎样实现当前的层级。通过该Paint
对象可以将一些color filters
(颜色过滤器)、opacity
(不透明度)应用于该层级。
对于视图类型参数的选择,从以下几个因素考虑:
-
Performance
(性能):使用LAYER_TYPE_HARDWARE
参数会将View
绘制到hardware texture
之上,一旦该View
绘制到hardware texture
之上,那么它的绘制代码就不会改变,除非再次调用invalidate()
方法。在一些动画中,比如透明度改变的动画,会使用GPU
直接改变视图层级上的代码,这样更高效。 -
Visual effects
:不论是使用LAYER_TYPE_HARDWARE
还是使用LAYER_TYPE_SOFTWARE
参数,都可以通过第二个参数Paint
的对象,将一些特殊的视觉效果应用于该View
,比如通过对Paint
对象使用ColorMatrixColorFilter
参数,将View
变为黑白的。 -
Compatibility
(兼容性):有时候使用硬件加速,会有一些问题,这时候就要考虑使用LAYER_TYPE_SOFTWARE
参数关闭硬件加速,通过使用软件绘制该View
了。
4.6 视图层级和动画
如果应用开启了硬件加速,使用硬件层级绘制一些复杂的动画会更快且更平滑。如果有一些很复杂的动画,每秒中进行 60 帧的绘制会是一个问题,有时候会掉帧,这时候使用硬件加速将 View
会知道 hardware texture
之上,就会有一定的改善。
View
不会进行重新绘制,除非调用了 invalidate()
方法。如果在应用中设置了一个动画,但是动画并不平滑,可以考虑使用硬件加速优化动画效果。
如果开启了硬件加速,在 View
中有一些属性的改变并不会对该 View
进行重新绘制操作,而是直接改变图层上的绘制代码:
-
alpha
:改变图层的透明度 -
x, y, translationX, translationY
:改变图层的位置 -
scaleX, scaleY
:改变图层的大小 -
rotation, rotationX, rotationY
:改变图层在 3D 空间的方向 -
pivotX, pivotY
:改变图层变化的原点位置
例如下面这样的动画代码效率会更高,而且更平滑:
view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
ObjectAnimator.ofFloat(view, "rotationY", 180).start();
因为硬件加速会占用很高的内存,所以一般是在使用的时候开启硬件加速,不使用的时候关闭硬件加速,如下所示:
view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
ObjectAnimator animator = ObjectAnimator.ofFloat(view, "rotationY", 180);
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
view.setLayerType(View.LAYER_TYPE_NONE, null);
}
});
animator.start();
4.7 一些建议
虽然使用硬件加速可以提高 View
的显示效果,提高性能,但是还是应该更加合理的使用硬件加速,不能滥用。对此给出以下几点建议:
- 减少应用中
View
的数量。不论是使用硬件绘制还是使用软件绘制,都会占用资源,所以当然是绘制的View
越少越好 - 避免过渡绘制。在开发中会不可避免的遇到一些复杂的布局,这时很可能会出现多个
View
重叠的现象,而且布局文件可能也会嵌套的比较深,所以最好减少布局文件,将嵌套层次尽可能的降低。就目前的硬件条件来看,一个像素点最多不要超过 2.5 倍的绘制。简言之,就是嵌套层级尽可能少,不要对同一个像素点进行过多的绘制。 - 在绘制方法中不要创建对象。因为像
onDraw()
这样的绘制方法,会不断的被调用,如果在其中创建对象,就会造成内存增长过快,垃圾回收器就会更频繁的调用。 - 不要频繁修改外形。复杂的形状,比如
shapes, paths, and circles
,是使用texture masks
进行绘制的。每次修改上面这些形状时,都会创建texture masks
,这样的代价还是比较大的。 - 不要频繁的修改
Bitmap
对象。每一次修改bitmap
中的内容,在绘制的时候都会上传至 GPU 中的texture
。 - 小心使用透明度。当使用
setAlpha
、AlphaAnimation
和ObjectAnimator
来改变一个View
的透明度时,它渲染到离屏缓冲时需要两倍的填充率。当需要改变一个很大的View
的透明度时,考虑使用硬件加速来实现。
参考资料:
HenCoder Android 自定义 View 1-7:属性动画 Property Animation(进阶篇) -- HenCoder
HenCoder Android 自定义 View 1-8 硬件加速 -- HenCoder
HenCoder Android 自定义 View 1-8 硬件加速 -- HenCoder
Android硬件加速原理与实现简介) -- 美团点评技术团队
关于硬件加速的那么点儿东西 -- 黑白咖