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. 属性动画
animator_content.png属性动画中了解上图中的这些内容,基本可以完成日常的开发,其中按照常用的排序:
ViewPropertyAnimator --> ObjectAnimator --> ValueAnimator
当然这是单一动画的选择顺序,按照这个顺序使用起来会很方便,如果是使用 AnimationSet,组合动画中每个动画,一般使用 ObjectAnimator 来构建。
2.1 ValueAnimator
但是我们还是先来看 ValueAnimator,为啥呢?因为它是 ViewPropertyAnimator 和 ObjectAnimator 的底层实现,ObjectAnimator 还是继承它的。
主要原理:ValueAnimator 实际上是对 int 值、float 值、对象值来进行控制,有了初始值和结束值,以及持续时间,来得到每个时间点的值,但是得到了值,并不能关联到我们要控制的控件或者视图的属性上,这时就需要手动将时间点的值赋给要控制的对象,并刷新对象,从而实现对象的动画过渡效果。
原理可能有点不太直观,下面来看看具体的操作。
ValueAnimator.ofInt(int values)
ValueAnimator.ofFloat(float values)
ValueAnimator.ofObject(int 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();
animator_ofint.gif
(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();
}
});
animator_ofobject.gif
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.gif
这样就开启了动画,不需要像 ValueAnimator 一样设置更新监听,手动赋值并刷新 View。只需要给出需要更改的属性即可。ofFloat() 方法的第一个参数是自定义绘制矩形的 View 对象,rotation 属性是基类 View 的有一个属性,代表围绕屏幕旋转角度,0 和 360.0f 表示开始时的角度值和结束值。这样开启动画后就在指定时间内矩形围绕屏幕方向的轴从 0 旋转到 360。除了 "rotation" 属性, View 的属性还有以下属性
animator_property1.png只要将这些属性参数设置到 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.gif
注意: 要想自定义属性 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.gif
View 的每个方法都对应了 ViewPropertyAnimator 的两个方法,其中一个是带有 -By 后缀的,例如,View.setTranslationX() 对应了 ViewPropertyAnimator.translationX() 和 ViewPropertyAnimator.translationXBy() 这两个方法。其中带有 -By() 后缀的是增量版本的方法,例如,translationX(100) 表示用动画把 View 的 translationX 值渐变为 100,而 translationXBy(100) 则表示用动画把 View 的 translationX 值渐变地增加 100。
animator_property.png2.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.gif
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.gif
5. 参考
HenCoder Android 自定义 View 1-7:属性动画 Property Animation(进阶篇)
Android 属性动画:这是一篇很详细的 属性动画 总结&攻略
网友评论