属性动画详解

1. 动画分类

Android 中动画分为 3 种:View 动画(视图动画)、帧动画、属性动画。

(1)帧动画:将一系列的图片按照顺序播放,每一张图片就是动画中的一帧,连续播放后就形成了动画,使用起来比较简单,缺点是当图片过多或者过大是,容易导致 OOM。

(2)View 动画:动画变化分为 4 种,平移、缩放、旋转、透明度,通过这 4 种动画其中的一种变换或者组合变换,使视图完成一种渐进式的动画效果。

(3)属性动画:是在 Android 3.0(API 11)才提供动画库。属性动画不仅可以使用自带的 API 来实现最常用的动画,而且通过自定义 View 的方式来做出定制化的动画,相比于 View 动画功能更强大。

  • View 动画只能作用在单一视图上,即只对一个 Button、TextView、或者 ViewGroup,不能作用于非 View 对象的属性,如改变视图颜色属性、自定义 View 时的路径改变的动画效果等,这些通过 View 动画难以实现,通过属性动画可以很好的完成。
  • View 动画的效果只有 4 种,很难完成更复杂的动画效果。
  • View 动画不能控件的属性,如通过 View 动画移动一个 Button,移动后点击 Button 显示的位置,并不能触发点击事件,但是点击 Button 原来的位置,可以触发点击事件,可见 View 动画不能改变控件的属性,只是显示的效果改变了而已。

从以上这 3 点可以看出,属性动画的优势,目前多数动画都是采用属性动画实现的,很少存在兼容性问题,因为在 API 11 以前的手机基本很少有人使用了。

下面就来一起学习下属性动画。

2. 属性动画

content

属性动画中了解上图中的这些内容,基本可以完成日常的开发,其中按照常用的排序:

ViewPropertyAnimator –> ObjectAnimator –> ValueAnimator

当然这是单一动画的选择顺序,按照这个顺序使用起来会很方便,如果是使用 AnimationSet,组合动画中每个动画,一般使用 ObjectAnimator 来构建。

2.1 ValueAnimator

但是我们还是先来看 ValueAnimator,为啥呢?因为它是 ViewPropertyAnimator 和 ObjectAnimator 的底层实现,ObjectAnimator 还是继承它的。

主要原理:ValueAnimator 实际上是对 int 值、float 值、对象值来进行控制,有了初始值和结束值,以及持续时间,来得到每个时间点的值,但是得到了值,并不能关联到我们要控制的控件或者视图的属性上,这时就需要手动将时间点的值赋给要控制的对象,并刷新对象,从而实现对象的动画过渡效果。

原理可能有点不太直观,下面来看看具体的操作。

ValueAnimator.ofIntint values
ValueAnimator.ofFloatfloat values
ValueAnimator.ofObjectint values

ValueAnimator 操作值有 3 种方法,这里主要演示 ofInt 和 ofObject,ofFloat 和 ofInt 类似

(1)ofInt

方式一:使用代码实现

final TextView textView = findViewById(R.id.text_view);
final ValueAnimator valueAnimator1 = ValueAnimator.ofInt(16, 48, 10);
// ofInt 动画,改变 TextView 的字体大小
valueAnimator1.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int animatedValue = (int) animation.getAnimatedValue();
textView.setTextSize(animatedValue);
}
});
// Button 点击事件
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
valueAnimator1.setDuration(4000);
valueAnimator1.start();
}
});

上述代码逻辑很简单,通过一个点击按钮开启动画,动画 ValueAnimator 完成,ofInt 方法设置了 3 个值,控制 TextView 的字体大小,从 16 过渡到 48,再到 10,仅仅有这些还不够,还需要手动设置控件的字体,所以需要设置 AnimatorUpdateListener,在得到值后,更新控件的字体大小。

方式二:使用 XML 实现

在工程目录 res/animator/value_animator.xml 动画 XML 文件中设置动画的参数

<?xml version="1.0" encoding="utf-8"?>
<animator xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="4000"
android:fillAfter="false"
android:fillBefore="true"
android:fillEnabled="true"
android:repeatCount="1"
android:repeatMode="restart"
android:valueFrom="16"
android:valueTo="48"
android:valueType="intType" />

然后加载动画,设置监听 AnimatorUpdateListener

// 2.XML 方式
final ValueAnimator animator = (ValueAnimator) AnimatorInflater.loadAnimator(ValueAnimatorActivity.this, R.animator.value_animator);
// 设置需要动画的控件
animator.setTarget(textView);
// 设置动画更新监听 AnimatorUpdateListener
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int animatedValue = (int) animation.getAnimatedValue();
textView.setTextSize(animatedValue);
}
});

最后同样的操作开启动画

animator.start();

ofint

(2)ofObject

首先看一下 ofObject 方法的参数,第一个参数是一个估值器,后面是对象的协变参数,为什么多了一个估值器?

public static ValueAnimator ofObject(TypeEvaluator evaluator, Object... values) 

对于 ofInt 方法,没有估值器参数,实际上已经具备系统内置的估值器 IntEvaluator,内置的估值器已经实现从开始值到结束值的过渡过程,能够得到不同时刻的 int 值,同理,对于 ofFloat()方法,也内置了 FloatEvaluator。

ofInt(int... values)

对于 ofObject() 方法,系统没有提供估值器,因为系统不知道我们我们要传入的对象的类型,所以需要我们自己来实现一个估值器。

这里我们自定义一个类 MyPoint,通过 MyPoint 对象来表示控件的位置

public class MyPoint {

private int x;
private int y;

public MyPoint(int x, int y) {
this.x = x;
this.y = y;
}

public int getX() {
return x;
}

public void setX(int x) {
this.x = x;
}

public int getY() {
return y;
}

public void setY(int y) {
this.y = y;
}
}

然后实现一个估值器,通过这个估值器来得到从开始值到结束值之间的不同时刻下的值,其中 fraction 由插值器给出,代表动画的进度 startValue 和 endValue 表示开始值和结束值。

public class PointEvaluator implements TypeEvaluator<MyPoint> {

@Override
public MyPoint evaluate(float fraction, MyPoint startValue, MyPoint endValue) {
int x = (int) (fraction * (endValue.getX() - startValue.getX()) + startValue.getX());
int y = (int) (fraction * (endValue.getY() - startValue.getY()) + startValue.getY());
return new MyPoint(x, y);
}
}

接下来就可以使用自定义的估值器通过 ofObject 来构建 ValueAnimator,这里只给出通过代码实现的方式。

// ofObject 动画
final ValueAnimator valueAnimator2 = ValueAnimator.ofObject(new PointEvaluator(),
new MyPoint(30, 30), new MyPoint(500, 500));
// 同样需要设置更新监听,得到更新值后,手动通过 myPoint 表示的位置来设置 textView 位置
valueAnimator2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
MyPoint myPoint = (MyPoint) animation.getAnimatedValue();

textView.layout(myPoint.getX(), myPoint.getY(),
myPoint.getX() + textView.getWidth(), myPoint.getY() + textView.getHeight());
}
});
// 设置点击事件,开启动画
findViewById(R.id.of_object_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
valueAnimator2.setDuration(4000);
valueAnimator2.start();
}
});

ofobject

2.2 ObjectAnimator

这里为了演示使用 ObjectAnimator 的动画效果,使用了自定义的一个画矩形的 View。

public class RectView extends View {

private Paint mPaint;
private String color;

public RectView(Context context) {
super(context);
initView();
}

public RectView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initView();
}

public RectView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}

private void initView() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.GREEN);
}

public String getColor() {
return color;
}

public void setColor(String color) {
this.color = color;
mPaint.setColor(Color.parseColor(color));
invalidate();
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.translate(getWidth() >> 1, getHeight() >> 1);
canvas.drawRoundRect(-100.0f, -100.0f, 100.0f, 100.0f,
10, 10, mPaint);
}
}

然后通过 ObjectAnimator 来创建动画,使这个圆绕 X 轴旋转。


ObjectAnimator.ofFloat(rectView, "rotation", 0, 360.0f)
.setDuration(4000)
.start();

animator_object1

这样就开启了动画,不需要像 ValueAnimator 一样设置更新监听,手动赋值并刷新 View。只需要给出需要更改的属性即可。ofFloat() 方法的第一个参数是自定义绘制矩形的 View 对象,rotation 属性是基类 View 的有一个属性,代表围绕屏幕旋转角度,0 和 360.0f 表示开始时的角度值和结束值。这样开启动画后就在指定时间内矩形围绕屏幕方向的轴从 0 旋转到 360。除了 “rotation” 属性, View 的属性还有以下属性

property

只要将这些属性参数设置到 ObjectAnimator.ofFloat() 方法中,ObjectAnimator 就会根据属性参数找到对应的属性(前提是 该对象存在这个属性),然后进行自动赋值,实现动画效果。另外,我们还可以通过 ObjectAnimator 改变自定义 的属性,下面就来展示一下自定义属性的改变效果。

View 基类中没有 “color” 这样的一个颜色属性,那么我们就在上述的自定义矩形 View 中添加一个 color 的属性,注意在 setColor 方法中,首先解析出 color 颜色值外,还要重新绘制矩形,这样颜色值才会生效,ObjectAnimator 的自动赋值,就是通过这个 setXX 方法来完成的。

ObjectAnimator.ofObject(rectView, "color", new ColorEvaluator(), "#00FF00", "#FF0000")
.setDuration(4000)
.start();

animator_object2

注意: 要想自定义属性 xx 生效,需要满足下面的两个条件:

  • 操作的对象需要提供 setXX 方法,另外如果方法中没有传递开始值,还需要提供 getXX 方法,ObjectAnimator 会从 getXX 方法中获取初始值,如果不提供,程序会 crash

  • 提供了 setXX 方法,仅仅是将值传递给了对象,如果想要达到某种效果,还需要我们自己来设置,如改变颜色需要调用 invalidate() 重绘,改变尺寸布局等调用 requestLayout() 方法。

2.3 ViewPropertyAnimator

ViewPropertyAnimator 是谷歌提供的更加方便实现 View 动画的类,使用方式:View.animate() 后跟 translationX() 等方法,动画会自动执行。

textView.animate()
.translationYBy(-100)
.alphaBy(-0.1f)
.scaleX(1.5f)
.rotationBy(180)
.setDuration(4000)
.start();

animator_property

View 的每个方法都对应了 ViewPropertyAnimator 的两个方法,其中一个是带有 -By 后缀的,例如,View.setTranslationX() 对应了 ViewPropertyAnimator.translationX() 和 ViewPropertyAnimator.translationXBy() 这两个方法。其中带有 -By() 后缀的是增量版本的方法,例如,translationX(100) 表示用动画把 View 的 translationX 值渐变为 100,而 translationXBy(100) 则表示用动画把 View 的 translationX 值渐变地增加 100。

property1

2.4 AnimationSet

有时我们在改变一个控件或者视图时,可能需要改变多个属性的动画效果,而且不同的属性改变时的先后顺序,也有一定的要求,比如先拉伸,然后改变颜色,最后再旋转,这时就需要 AnimationSet 来完成一系列的动画组合。AnimationSet 的几个主要方法。

AnimatorSet.play(Animator anim)   // 播放当前动画
AnimatorSet.after(long delay)   // 将现有动画延迟x毫秒后执行
AnimatorSet.with(Animator anim)   // 将现有动画和传入的动画同时执行
AnimatorSet.after(Animator anim)   // 将现有动画插入到传入的动画之后执行
AnimatorSet.before(Animator anim) // 将现有动画插入到传入的动画之前执行
AnimatorSet.playSequentially // 各个动画按照顺序执行,前一个执行完,后面的再开始执行

示例:对自定义的矩形 View,先拉伸,然后改变颜色,最后再旋转

RectView rectView = findViewById(R.id.circle_view);
final ObjectAnimator animator1 = ObjectAnimator.ofFloat(rectView, "scaleX", 1, 2)
.setDuration(2000);
final ObjectAnimator animator2 = ObjectAnimator.ofObject(rectView,
"color", new ColorEvaluator(), "#0000FF", "#FF0000")
.setDuration(2000);
final ObjectAnimator animator3 = ObjectAnimator.ofFloat(rectView, "rotation", 0, 450.0f)
.setDuration(3000);

findViewById(R.id.start_btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playSequentially(animator1, animator2, animator3);
animatorSet.start();
}
});

animator_set

2.5 估值器

在上述过程中我们已经接触了估值器,估值器完成的工作就是给出不同进度下的值,然后 ObjectAnimator 或者 ValueAnimator 拿到值后再进行对对象的操作。下面以 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));
}
}

其实很很简单,就是这样一个线性的数学公式 result = min + k * (max - min),其系数 k 就是提到的进度,那么进度是怎么来的呢?这个进度是由插值器给出的,所以需要得到不同进度下的值,需要先知道进度,进度由插值器给出,插值器和估值器配合工作来得到不同进度下的值。

注意:上面提到的值,也可以是对象,道理是同样的。

2.6 插值器

插值器是来获取进度的,那么如何给出进度的?所有的插值器都是实现 TimeInterpolator 接口,随着时间的发展,给出不同时刻属性变化的百分比,这个百分比就是进度,可能用进度不是很准确,总之插值器给出这个百分比之后,最后给到估值器,估值器通过这个百分比计算对应时刻的值。

public interface TimeInterpolator {
float getInterpolation(float input);
}

常见的插值器就是 LinearInterpolator,是一个匀速插值器,实际上就是 y = x,这样一个数学公式,x 代表时间,随着时间流逝,输出 y ,代表属性变化的百分比,LinearInterpolator 完成的就是随时间均速变化的效果。

对于 AccelerateInterpolator 加速度插值器,是利用 y=x^2 这个数学公式给出属性变化的百分比。

当需要改变插值器时,通过 setInterpolator(Interpolator interpolator) 设置 Interpolator Interpolator 其实就是速度设置器,在参数里填入不同的 Interpolator ,动画就会以不同的速度模型来执行。

AccelerateDecelerateInterpolator  // 先加速再减速

LinearInterpolator // 匀速

AccelerateInterpolator // 加速

DecelerateInterpolator // 持续减速直到 0

AnticipateInterpolator // 回拉一下再进行正常动画轨迹

OvershootInterpolator // 动画会超过目标值一些,然后再弹回来

AnticipateOvershootInterpolator // 上面这两个的结合版:开始前回拉,最后超过一些然后回弹

BounceInterpolator // 在目标值处弹跳

CycleInterpolator // 正弦 / 余弦曲线模型

PathInterpolator // 自定义动画完成度 / 时间完成度曲线。

FastOutLinearInInterpolator // 加速模型,曲线公式是用的贝塞尔曲线

FastOutSlowInInterpolator // 先加速再减速。用的是贝塞尔曲线

LinearOutSlowInInterpolator // 持续减速

2.7 动画监听

(1)基类 Animation 中有一个监听 AnimatorListener ,可以监听动画开始、结束、重复、取消时刻,从而来进行一系列操作。

ObjectAnimator、ValueAnimator、AnimatorSet 都是继承自 Animation,所以都可以设置该监听,

(2)此外,在上面讲解 ValueAnimator 时,看到它还有另外一个监听 ValueAnimator.AnimatorUpdateListener,在值变化时,通过该监听得到不同时刻的值,从而对对象设置值,改变对象属性。

(3)有时我们可能不需要监听动画个多个时刻,如仅仅需要监听结束时刻,然后执行我们想要执行的一个动作,那对于其他时刻就没必要重写,AnimatorListenerAdapter 就是来满足这个需求的,我们可以根据需要的监听时刻进行重写。

public abstract class AnimatorListenerAdapter implements Animator.AnimatorListener,
Animator.AnimatorPauseListener {

/**
* {@inheritDoc}
*/
@Override
public void onAnimationCancel(Animator animation) {
}

/**
* {@inheritDoc}
*/
@Override
public void onAnimationEnd(Animator animation) {
}

/**
* {@inheritDoc}
*/
@Override
public void onAnimationRepeat(Animator animation) {
}

/**
* {@inheritDoc}
*/
@Override
public void onAnimationStart(Animator animation) {
}

/**
* {@inheritDoc}
*/
@Override
public void onAnimationPause(Animator animation) {
}

/**
* {@inheritDoc}
*/
@Override
public void onAnimationResume(Animator animation) {
}
}

3. 属性动画工作原理

有了上面的基础,来看看属性动画的工作原理(以 ObjectAnimator 为例):

(1)ObjectAnimator 的 start 方法最终调用的是 ValueAnimator 的 start(boolean playBackwards) 方法,该方法中完成了 2 项工作,设置包装属性的 PropertyValuesHolder,并设置当前的进度。

private void start(boolean playBackwards) {
...
addAnimationCallback(0);

if (mStartDelay == 0 || mSeekFraction >= 0 || mReversing) {

// 动画初始化,准备 PropertyValuesHolder 数组
startAnimation();
if (mSeekFraction == -1) {
// No seek, start at play time 0. Note that the reason we are not using fraction 0
// is because for animations with 0 duration, we want to be consistent with pre-N
// behavior: skip to the final value immediately.
setCurrentPlayTime(0);
} else {
// 设置当前进度
setCurrentFraction(mSeekFraction);
}
}
}

(2)接着看 setCurrentFraction 方法设置当前动画的进度,然后根据进度执行动画,最后一行 animateValue(currentIterationFraction);

public void setCurrentFraction(float fraction) {
initAnimation();
fraction = clampFraction(fraction);
mStartTimeCommitted = true; // do not allow start time to be compensated for jank
if (isPulsingInternal()) {
long seekTime = (long) (getScaledDuration() * fraction);
long currentTime = AnimationUtils.currentAnimationTimeMillis();
// Only modify the start time when the animation is running. Seek fraction will ensure
// non-running animations skip to the correct start time.
mStartTime = currentTime - seekTime;
} else {
// If the animation loop hasn't started, or during start delay, the startTime will be
// adjusted once the delay has passed based on seek fraction.
mSeekFraction = fraction;
}
mOverallFraction = fraction;
final float currentIterationFraction = getCurrentIterationFraction(fraction, mReversing);
animateValue(currentIterationFraction);
}

(3)根据当前动画的进度,可以计算出对应的属性值,计算过程由 PropertyValuesHolder,毕竟属性是包装在 PropertyValuesHolder 中。这个计算的过程就不详细分析了,里面通过调用 Keyframes 来完成,对于 int 类型和 float 类型有默认的估值器 IntEvaluator 和 FloatEvaluator,如果是 Object 类型的,会使用我们自己实现的估值器。

void animateValue(float fraction) {
fraction = mInterpolator.getInterpolation(fraction);
mCurrentFraction = fraction;
int numValues = mValues.length;
for (int i = 0; i < numValues; ++i) {
mValues[i].calculateValue(fraction);
}
if (mUpdateListeners != null) {
int numListeners = mUpdateListeners.size();
for (int i = 0; i < numListeners; ++i) {
mUpdateListeners.get(i).onAnimationUpdate(this);
}
}
}

(4)最后看一下,得到的对应属性,是如何设置到对象当中的,即怎么调用 set 方法。

// 设置属性值
void setAnimatedValue(Object target) {
if (mProperty != null) {
mProperty.set(target, getAnimatedValue());
}
if (mSetter != null) {
try {
mTmpValueArray[0] = getAnimatedValue();
mSetter.invoke(target, mTmpValueArray);
} catch (InvocationTargetException e) {
Log.e("PropertyValuesHolder", e.toString());
} catch (IllegalAccessException e) {
Log.e("PropertyValuesHolder", e.toString());
}
}
}

(5)设置属性值是在 setAnimatedValue 方法中完成的,是通过反射调用对象的 setXX 方法,这样就将当前动画进度的值赋给的对象,像上面的我们自定义 View 中的 setXX 方法,还有重绘操作,这样设置颜色值后就更新了 UI。

此外,上面 2 个条件中提到,如果没有设置初始值,还需要提供 getXX 方法,该方法也是通过反射调用的,在 PropertyValuesHolder 的 setupValue 方法中执行的。

private void setupValue(Object target, Keyframe kf) {
if (mProperty != null) {
Object value = convertBack(mProperty.get(target));
kf.setValue(value);
} else {
try {
if (mGetter == null) {
Class targetClass = target.getClass();
setupGetter(targetClass);
if (mGetter == null) {
// Already logged the error - just return to avoid NPE
return;
}
}
Object value = convertBack(mGetter.invoke(target));
kf.setValue(value);
} catch (InvocationTargetException e) {
Log.e("PropertyValuesHolder", e.toString());
} catch (IllegalAccessException e) {
Log.e("PropertyValuesHolder", e.toString());
}
}
}

4. 小例子

比较简单的一个小例子,实现 TextView 的 ZoomIn 的效果。

TextView textView = findViewById(R.id.hello_world);
AnimatorSet animatorSet = new AnimatorSet();
ObjectAnimator animator1 = ObjectAnimator.ofFloat(textView, "scaleX", 0.45f, 1);
animator1.setRepeatMode(ValueAnimator.RESTART);
animator1.setRepeatCount(ObjectAnimator.INFINITE);
animator1.setDuration(3000);
ObjectAnimator animator2 = ObjectAnimator.ofFloat(textView, "scaleY", 0.45f, 1);
animator2.setRepeatMode(ValueAnimator.RESTART);
animator2.setRepeatCount(ObjectAnimator.INFINITE);
animator2.setDuration(3000);
ObjectAnimator animator3 = ObjectAnimator.ofFloat(textView, "alpha", 0, 1);
animator3.setRepeatMode(ValueAnimator.RESTART);
animator3.setRepeatCount(ObjectAnimator.INFINITE);
animator3.setDuration(3000);
animatorSet.playTogether(animator1, animator2, animator3);
animatorSet.start();

animator_sample

5. 参考

官方文档

HenCoder Android 自定义 View 1-7:属性动画 Property Animation(进阶篇)

Android 属性动画:这是一篇很详细的 属性动画 总结&攻略

代码地址

练习代码

打赏一个呗

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦