文章概览
本系列将介绍以下内容:
1 属性动画Property Animation
属性动画是在API Level 11(Android 3.0)时引入的,包含ValueAnimator和ObjectAnimator,均位于包android.animation中,且ObjectAnimator继承自ValueAnimator,其UML图(Class Diagram类图)如图所示:
Animator是抽象类,内含三个接口和一个具体类AnimatorConstantState。
AnimationHandler内含两个接口和一个具体类MyFrameCallbackProvider。
ValueAnimator内含三个接口,且实现了AnimationHandler.AnimationFrameCallback接口,详情见源码。
ObjectAnimator不含任何内部类和接口。
TypeEvaluator是一个泛型接口,Keyframes是普通接口,PropertyValuesHolder是普通类,它们三个将在Evaluator中涉及到,本文不展开。
属性动画的执行流程:
2 ValueAnimator
ValueAnimator是针对值的,不会对控件执行任何操作,功能要点:
1、ValueAnimator只负责对指定值区间进行动画运算
2、需要对运算过程进行监听,然后自己对控件执行动画操作
注意:动画过程中产生的数值与构造时传入的值的类型是一样的。
package java.lang;
/**
* Cloneable是个空接口,什么都没有
*/
public interface Cloneable {
}
package android.animation;
public abstract class Animator implements Cloneable {
...
public void start() {
}
public void cancel() {
}
public abstract void setStartDelay(long startDelay);
public abstract Animator setDuration(long duration);
public abstract void setInterpolator(TimeInterpolator value);
public void addListener(AnimatorListener listener) {
if (mListeners == null) {
mListeners = new ArrayList<AnimatorListener>();
}
mListeners.add(listener);
}
public void removeListener(AnimatorListener listener) {
if (mListeners == null) {
return;
}
mListeners.remove(listener);
if (mListeners.size() == 0) {
mListeners = null;
}
}
public void addPauseListener(AnimatorPauseListener listener) {
if (mPauseListeners == null) {
mPauseListeners = new ArrayList<AnimatorPauseListener>();
}
mPauseListeners.add(listener);
}
public void removePauseListener(AnimatorPauseListener listener) {
if (mPauseListeners == null) {
return;
}
mPauseListeners.remove(listener);
if (mPauseListeners.size() == 0) {
mPauseListeners = null;
}
}
public void removeAllListeners() {
if (mListeners != null) {
mListeners.clear();
mListeners = null;
}
if (mPauseListeners != null) {
mPauseListeners.clear();
mPauseListeners = null;
}
}
/**
* <p>An animation listener receives notifications from an animation.
* Notifications indicate animation related events, such as the end or the
* repetition of the animation.</p>
* 监听动画变化时的4个状态
*/
public static interface AnimatorListener {
default void onAnimationStart(@NonNull Animator animation, boolean isReverse) {
onAnimationStart(animation);
}
default void onAnimationEnd(@NonNull Animator animation, boolean isReverse) {
onAnimationEnd(animation);
}
void onAnimationStart(@NonNull Animator animation);
void onAnimationEnd(@NonNull Animator animation);
void onAnimationCancel(@NonNull Animator animation);
void onAnimationRepeat(@NonNull Animator animation);
}
public static interface AnimatorPauseListener {
void onAnimationPause(@NonNull Animator animation);
void onAnimationResume(@NonNull Animator animation);
}
private static class AnimatorConstantState extends ConstantState<Animator> {
...
}
interface AnimatorCaller<T, A> {
...
}
}
package android.animation;
public class AnimationHandler {
...
private class MyFrameCallbackProvider implements AnimationFrameCallbackProvider {
...
}
public interface AnimationFrameCallback {
boolean doAnimationFrame(long frameTime);
void commitAnimationFrame(long frameTime);
}
public interface AnimationFrameCallbackProvider {
void postFrameCallback(Choreographer.FrameCallback callback);
void postCommitCallback(Runnable runnable);
long getFrameTime();
long getFrameDelay();
void setFrameDelay(long delay);
}
}
package android.animation;
@SuppressWarnings("unchecked")
public class ValueAnimator extends Animator implements AnimationHandler.AnimationFrameCallback {
...
private int mRepeatCount = 0;
private int mRepeatMode = RESTART;
private TimeInterpolator mInterpolator = sDefaultInterpolator;
PropertyValuesHolder[] mValues;
@IntDef({
RESTART, REVERSE})
@Retention(RetentionPolicy.SOURCE)
public @interface RepeatMode {
}
public static final int RESTART = 1;
public static final int REVERSE = 2;
public static final int INFINITE = -1;
public static ValueAnimator ofInt(int... values) {
ValueAnimator anim = new ValueAnimator();
anim.setIntValues(values);
return anim;
}
public static ValueAnimator ofArgb(int... values) {
ValueAnimator anim = new ValueAnimator();
anim.setIntValues(values);
anim.setEvaluator(ArgbEvaluator.getInstance());
return anim;
}
public static ValueAnimator ofFloat(float... values) {
ValueAnimator anim = new ValueAnimator();
anim.setFloatValues(values);
return anim;
}
public static ValueAnimator ofPropertyValuesHolder(PropertyValuesHolder... values) {
ValueAnimator anim = new ValueAnimator();
anim.setValues(values);
return anim;
}
public static ValueAnimator ofObject(TypeEvaluator evaluator, Object... values) {
ValueAnimator anim = new ValueAnimator();
anim.setObjectValues(values);
anim.setEvaluator(evaluator);
return anim;
}
public void setIntValues(int... values) {
...
}
public void setFloatValues(float... values) {
...
}
public void setObjectValues(Object... values) {
...
}
public void setValues(PropertyValuesHolder... values) {
...
}
@Override
public ValueAnimator setDuration(long duration) {
if (duration < 0) {
throw new IllegalArgumentException("Animators cannot have negative duration: " +
duration);
}
mDuration = duration;
return this;
}
public Object getAnimatedValue() {
if (mValues != null && mValues.length > 0) {
return mValues[0].getAnimatedValue();
}
// Shouldn't get here; should always have values unless ValueAnimator was set up wrong
return null;
}
public Object getAnimatedValue(String propertyName) {
PropertyValuesHolder valuesHolder = mValuesMap.get(propertyName);
if (valuesHolder != null) {
return valuesHolder.getAnimatedValue();
} else {
// At least avoid crashing if called with bogus propertyName
return null;
}
}
public void setRepeatCount(int value) {
mRepeatCount = value;
}
public void setRepeatMode(@RepeatMode int value) {
mRepeatMode = value;
}
public void addUpdateListener(AnimatorUpdateListener listener) {
if (mUpdateListeners == null) {
mUpdateListeners = new ArrayList<AnimatorUpdateListener>();
}
mUpdateListeners.add(listener);
}
public void removeAllUpdateListeners() {
if (mUpdateListeners == null) {
return;
}
mUpdateListeners.clear();
mUpdateListeners = null;
}
public void removeUpdateListener(AnimatorUpdateListener listener) {
if (mUpdateListeners == null) {
return;
}
mUpdateListeners.remove(listener);
if (mUpdateListeners.size() == 0) {
mUpdateListeners = null;
}
}
/**
* ValueAnimator默认使用LinearInterpolator
*/
@Override
public void setInterpolator(TimeInterpolator value) {
if (value != null) {
mInterpolator = value;
} else {
mInterpolator = new LinearInterpolator();
}
}
public void setEvaluator(TypeEvaluator value) {
if (value != null && mValues != null && mValues.length > 0) {
mValues[0].setEvaluator(value);
}
}
@Override
private void start(boolean playBackwards) {
...
}
@Override
public void start() {
start(false);
}
@Override
public void cancel() {
...
}
/**
* Implementors of this interface can add themselves as update listeners
* to an <code>ValueAnimator</code> instance to receive callbacks on every animation
* frame, after the current frame's values have been calculated for that
* <code>ValueAnimator</code>.
* 监听动画过程中值的实时变化
*/
public static interface AnimatorUpdateListener {
/**
* <p>Notifies the occurrence of another frame of the animation.</p>
*
* @param animation The animation which was repeated.当前动画的实例
*/
void onAnimationUpdate(@NonNull ValueAnimator animation);
}
public interface DurationScaleChangeListener {
void onChanged(@FloatRange(from = 0) float scale);
}
}
2.1 ValueAnimator的用法
这里的普通用法指的是ValueAnimator.ofInt(xxx)/ofArgb(xxx)/ofFloat(xxx)等只能传入既定类型参数的用法。
使用ValueAniamtor时直接创建即可,但ValueAnimator可以添加两种监听,一种是父类Animator中的AnimatorListener,一种是自身的AnimatorUpdateListener,详见Demo。
布局文件
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#ffff00"
android:padding="10dp"
android:singleLine="false"
android:text="ValueAnimator能更改属性,修复补间动画的点击位置问题" />
<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:padding="10dp"
android:text="Start Anim"
android:textAllCaps="false"
android:textSize="18sp" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
示例代码:
package com.example.myapplication;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.animation.LinearInterpolator;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
public class ValueAnimDemoActivity extends Activity {
private TextView tv;
private Button btn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.translate_anim_activity);
tv = findViewById(R.id.tv);
btn = findViewById(R.id.btn);
btn.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
doValueAnimator(tv);
}
});
tv.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
Toast.makeText(ValueAnimDemoActivity.this, "clicked me", Toast.LENGTH_SHORT).show();
}
});
}
/**
* ValueAnimator的使用
*/
private void doValueAnimator(final TextView tv) {
// ofXxx(X.. values) 此处values是可变参数,动画过程中产生的数值与构造时传入的值类型一样
ValueAnimator valueAnimator = ValueAnimator.ofInt(0, 400);
// 自带的监听,监听动画过程中值的实时变化
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
public void onAnimationUpdate(ValueAnimator animation) {
// 参数animation为当前状态动画的实例
int curValue = (Integer) animation.getAnimatedValue();
// 永久性改变控件位置,控件在新位置能够响应点击事件(补间动画不行)
tv.layout(curValue, curValue, curValue + tv.getWidth(), curValue + tv.getHeight());
}
});
// 父类Animator的监听,监听动画变化时的4个状态
valueAnimator.addListener(new Animator.AnimatorListener() {
public void onAnimationStart(Animator animation) {
Log.d("tag", "onAnimationStart");
}
public void onAnimationEnd(Animator animation) {
}
public void onAnimationCancel(Animator animation) {
}
public void onAnimationRepeat(Animator animation) {
}
});
valueAnimator.setRepeatMode(ValueAnimator.RESTART);
valueAnimator.setRepeatCount(ValueAnimator.REVERSE);
valueAnimator.setInterpolator(new LinearInterpolator());
valueAnimator.setDuration(1000);
valueAnimator.start();
}
}
效果图:
需要注意的是,如果要移除监听,如调用removeAllListeners()后,并没有取消动画,因此动画会继续执行,但不再打印日志。
2.2 进阶用法ofObject
进阶用法指的是ValueAnimator.ofObject(xxx),因为它可以传入任何类型的参数,详见Demo。
布局文件:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/start_anim"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="开始动画" />
<TextView
android:id="@+id/tv"
android:layout_width="50dp"
android:layout_height="20dp"
android:layout_marginLeft="30dp"
android:layout_toRightOf="@id/start_anim"
android:background="#ffff00"
android:gravity="center" />
</RelativeLayout>
自定义Evaluator:
package com.harvic.InterploatorEvaluator;
import android.animation.TypeEvaluator;
/**
* 字符转换
*/
public class CharEvaluator implements TypeEvaluator<Character> {
@Override
public Character evaluate(float fraction, Character startValue, Character endValue) {
int startInt = (int) startValue;
int endInt = (int) endValue;
int curInt = (int) (startInt + fraction * (endInt - startInt));
char result = (char) curInt;
return result;
}
}
示例代码:
package com.harvic.InterploatorEvaluator;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.widget.TextView;
public class CharacterEvaluatorActivity extends Activity {
private TextView tv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.evaluator_activity);
tv = (TextView) findViewById(R.id.tv);
findViewById(R.id.start_anim).setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
// 从A到Z的字母变化动画
ValueAnimator animator = ValueAnimator.ofObject(new CharEvaluator(), new Character('A'), new Character('Z'));
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
public void onAnimationUpdate(ValueAnimator animation) {
char text = (Character) animation.getAnimatedValue();
tv.setText(String.valueOf(text));
}
});
// 由慢到快的加速变化
animator.setInterpolator(new AccelerateInterpolator());
animator.setDuration(10000);
animator.start();
}
});
}
}
用法也很简单,只要传入ofObject(xxx)对应参数即可,效果图:
更高阶用法:
ValueAnimator和ObjectAnimator都还有一个ofPropertyValuesHolder(xxx)方法获取动画实例,两者用法类似,本文仅在ObjectAnimator章节介绍这部分内容,具体请参见下文。
3 ObjectAnimator
ValueAnimator只能对动画中的数值进行计算,如果想对控件执行操作则需要监听动画过程,会比较繁琐。
为了能让动画直接与对应控件关联,ObjectAnimator出场了。
package android.animation;
public final class ObjectAnimator extends ValueAnimator {
...
public void setPropertyName(@NonNull String propertyName) {
...
}
public void setProperty(@NonNull Property property) {
...
}
/**
* 其余重载方法未列出
*/
public static ObjectAnimator ofInt(Object target, String propertyName, int... values) {
ObjectAnimator anim = new ObjectAnimator(target, propertyName);
anim.setIntValues(values);
return anim;
}
/**
* 其余重载方法未列出
*/
public static ObjectAnimator ofMultiInt(Object target, String propertyName, int[][] values) {
PropertyValuesHolder pvh = PropertyValuesHolder.ofMultiInt(propertyName, values);
return ofPropertyValuesHolder(target, pvh);
}
/**
* 其余重载方法未列出
*/
public static ObjectAnimator ofArgb(Object target, String propertyName, int... values) {
ObjectAnimator animator = ofInt(target, propertyName, values);
animator.setEvaluator(ArgbEvaluator.getInstance());
return animator;
}
/**
* 其余重载方法未列出
*/
public static ObjectAnimator ofFloat(Object target, String propertyName, float... values) {
ObjectAnimator anim = new ObjectAnimator(target, propertyName);
anim.setFloatValues(values);
return anim;
}
/**
* 其余重载方法未列出
*/
public static ObjectAnimator ofMultiFloat(Object target, String propertyName,
float[][] values) {
PropertyValuesHolder pvh = PropertyValuesHolder.ofMultiFloat(propertyName, values);
return ofPropertyValuesHolder(target, pvh);
}
/**
* 其余重载方法未列出
*/
public static ObjectAnimator ofObject(Object target, String propertyName,
TypeEvaluator evaluator, Object... values) {
ObjectAnimator anim = new ObjectAnimator(target, propertyName);
anim.setObjectValues(values);
anim.setEvaluator(evaluator);
return anim;
}
@NonNull
public static ObjectAnimator ofPropertyValuesHolder(Object target,
PropertyValuesHolder... values) {
ObjectAnimator anim = new ObjectAnimator();
anim.setTarget(target);
anim.setValues(values);
return anim;
}
...
}
3.1 ObjectAnimator的用法
基本使用较简单,与ValueAnimator相似:
TextView tv = findViewById(R.id.tv);
findViewById(R.id.start_alpha).setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
// 实现alpha值变化
ObjectAnimator animator = ObjectAnimator.ofFloat(tv, "alpha", 1, 0, 1);
animator.setInterpolator(new LinearInterpolator());
animator.setEvaluator(new IntEvaluator());
animator.setDuration(2000);
animator.start();
}
});
ObjectAnimator做动画,不是根据控件xml中的属性值来改变的,而是通过指定属性所对应的set方法来改变的,如对TextView的透明度做动画,就会调用View类中的setAlpha(float alpha)方法,归纳起来就是:
1、要使用ObejctAnimator来构造动画,在要操作的控件中必须存在对应属性的set方法,且参数类型必须与构造所使用的ofInt(xxx)、ofArgb(xxx)、ofFloat(xxx)、ofObject(xxx)等方法一致。
2、set方法的命名必须采用驼峰命名法,且以set开头,即setXxxx(xxx)。如setPropertyName(xxx)所对应的属性是propertyName。
3.2 ObjectAnimator动画原理
从上文属性动画执行流程图中可以看出,ValueAnimator动画与ObjectAnimator动画的流程相似,区别是最后一个步骤,ValueAnimator需要通过添加监听器监听当前的数值,而ObjectAnimator则需要根据属性值拼接对应的setXxx(xxx)方法,并通过反射找到对应控件的同名setXxx(xxx)方法,再将当前的数值作为setXxxx(xxx)方法的参数传入。
总结动画执行流程,有几个注意事项:
1、拼接对应的setXxxx(xxx)方法。
2、确定函数类型。与ValueAnimator一样,ObjectAnimator动画中产生的数值类型与构造时传入的值类型也是一样的。如果方法名一样,参数类型不一样,系统会报错。
3、调用setXxx(xxx)方法之后的操作由谁做?
ObjectAnimator只负责把动画过程中的数值传到对应属性的setXxx(xxx)方法中,该setXxxx(xxx)方法相当于ValueAnimator中添加的监听器,而setXxx(xxx)方法中对控件的操作需要开发者手动编码。
4、由于动画在进行时每隔十几毫秒会刷新一次,因此setXxx(xxx)方法也会同频率调用一次。
3.3 自定义ObjectAnimator属性
再捋一遍ObjectAnimator动画的设置流程:
ObjectAnimator需要指定要操作的控件对象,在开始动画后,先到控件类中区寻找要设置的属性所对应的set方法,然后把动画中间值作为参数传给这个set方法并执行它。
因此,控件类中必须存在所要设置的属性所对应的set方法。
用法举例:
1、自定义圆形的shape文件circle.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#ff0000" />
</shape>
2、自定义ImageView,命名为FallingBallImageView.java
package com.example.objectanim;
import android.content.Context;
import android.graphics.Point;
import android.util.AttributeSet;
import android.widget.ImageView;
public class FallingBallImageView extends ImageView {
public FallingBallImageView(Context context) {
super(context);
}
public FallingBallImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FallingBallImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
/**
* 在ObjectAnimator中设置fallingPos属性时对应该方法:
* 1、该方法所对应的属性只能是FallingPos或者fallingPos
* 2、方法参数是Point对象,则ObjectAnimator的构造方法必须使用ofObject()
*/
public void setFallingPos(Point pos) {
layout(pos.x, pos.y, pos.x + getWidth(), pos.y + getHeight());
}
/**
* ObjectAnimator调用ofXxx(xxx)方法时,若参数中只设置一个过渡值,则会使用方法参数类型的默认值,如果没有默认值就会报错。
* ofObject(xxx)没有参数类型默认值,只传入一个参数过渡值时,会调用该方法获取默认值。
*/
public Point getFallingPos() {
Point point = new Point(0, 0);
return point;
}
}
3、布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="horizontal">
<com.example.objectanim.FallingBallImageView
android:id="@+id/ball_img"
android:layout_width="50dp"
android:layout_height="50dp"
android:src="@drawable/circle" />
<Button
android:id="@+id/start_anim"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="开始动画" />
</LinearLayout>
4、自定义转换器FallingBallEvaluator
package com.example.objectanim;
import android.animation.TypeEvaluator;
import android.graphics.Point;
public class FallingBallEvaluator implements TypeEvaluator<Point> {
private Point point = new Point();
@Override
public Point evaluate(float fraction, Point startValue, Point endValue) {
point.x = (int) (startValue.x + fraction * (endValue.x - startValue.x));
if (fraction * 2 <= 1) {
point.y = (int) (startValue.y + fraction * 2 * (endValue.y - startValue.y));
} else {
point.y = endValue.y;
}
return point;
}
}
5、使用动画
package com.example.objectanim;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.graphics.Point;
import android.os.Bundle;
import android.view.View;
public class FallingBallActivity extends Activity {
private FallingBallImageView ball_img;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.falling_ball_activity);
ball_img = findViewById(R.id.ball_img);
findViewById(R.id.start_anim).setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
/*
只设置一个参数过渡值时,会使用方法参数类型的默认值,如果没有默认值就会报错。
ofObject(xxx)没有默认值,但在FallingBallImageView中定义了getFallingPos(),因此此处不会报错。
*/
// ObjectAnimator animator = ObjectAnimator.ofObject(ball_img, "fallingPos", new FallingBallEvaluator(), new Point(500, 500));
// fallingPos为自定义属性,因此需要自定义对应的setFallingPos(xxx)方法,见FallingBallImageView
ObjectAnimator animator = ObjectAnimator.ofObject(ball_img, "fallingPos", new FallingBallEvaluator(), new Point(0, 0), new Point(500, 500));
animator.setDuration(2000);
animator.start();
}
});
}
}
效果图:
ObjectAnimator自定义属性注意事项:
1、当且仅当只给动画设置一个过渡值时,程序会调用属性对应的get()方法来获得动画初始值。如果动画没有初始值,就会使用系统默认值。
2、ObjectAnimator的ofInt(xxx)、ofFloat(xxx)方法有参数类型默认值0;而ofObject(xxx)由于需要手动指定动画的参数类型,故不一定有参数类型默认值。因此如果在调用ofXxx(xxx)方法是只传入一个参数过渡值,则可能会报错。
总结起来就是:当且仅当动画只有一个过渡值时,系统才会调用对应属性的get()方法来得到动画的初始值。当不存在get()方法时,则会取动画参数类型的默认值作为初始值;当无法取得动画参数类型的默认值时,会直接崩溃。
3.4 PropertyValuesHolder与Keyframe
ValueAnimator和ObjectAnimator都有一个ofPropertyValuesHolder(xxx)方法获取动画实例,两者用法类似,先来看下这两个方法:
/**
* ValueAnimator中的方法
*/
public static ValueAnimator ofPropertyValuesHolder(PropertyValuesHolder... values) {
ValueAnimator anim = new ValueAnimator();
anim.setValues(values);
return anim;
}
/**
* ObjectAnimator中的方法
*/
@NonNull
public static ObjectAnimator ofPropertyValuesHolder(Object target,
PropertyValuesHolder... values) {
ObjectAnimator anim = new ObjectAnimator();
anim.setTarget(target);
anim.setValues(values);
return anim;
}
再来看一下PropertyValuesHolder:
package android.animation;
public class PropertyValuesHolder implements Cloneable {
...
public static PropertyValuesHolder ofInt(String propertyName, int... values) {
return new IntPropertyValuesHolder(propertyName, values);
}
public static PropertyValuesHolder ofInt(Property<?, Integer> property, int... values) {
return new IntPropertyValuesHolder(property, values);
}
public static PropertyValuesHolder ofFloat(String propertyName, float... values) {
return new FloatPropertyValuesHolder(propertyName, values);
}
public static PropertyValuesHolder ofFloat(Property<?, Float> property, float... values) {
return new FloatPropertyValuesHolder(property, values);
}
public static PropertyValuesHolder ofObject(String propertyName, TypeEvaluator evaluator,
Object... values) {
PropertyValuesHolder pvh = new PropertyValuesHolder(propertyName);
pvh.setObjectValues(values);
pvh.setEvaluator(evaluator);
return pvh;
}
public static PropertyValuesHolder ofObject(String propertyName,
TypeConverter<PointF, ?> converter, Path path) {
PropertyValuesHolder pvh = new PropertyValuesHolder(propertyName);
pvh.mKeyframes = KeyframeSet.ofPath(path);
pvh.mValueType = PointF.class;
pvh.setConverter(converter);
return pvh;
}
@SafeVarargs
public static <V> PropertyValuesHolder ofObject(Property property,
TypeEvaluator<V> evaluator, V... values) {
PropertyValuesHolder pvh = new PropertyValuesHolder(property);
pvh.setObjectValues(values);
pvh.setEvaluator(evaluator);
return pvh;
}
@SafeVarargs
public static <T, V> PropertyValuesHolder ofObject(Property<?, V> property,
TypeConverter<T, V> converter, TypeEvaluator<T> evaluator, T... values) {
PropertyValuesHolder pvh = new PropertyValuesHolder(property);
pvh.setConverter(converter);
pvh.setObjectValues(values);
pvh.setEvaluator(evaluator);
return pvh;
}
public static <V> PropertyValuesHolder ofObject(Property<?, V> property,
TypeConverter<PointF, V> converter, Path path) {
PropertyValuesHolder pvh = new PropertyValuesHolder(property);
pvh.mKeyframes = KeyframeSet.ofPath(path);
pvh.mValueType = PointF.class;
pvh.setConverter(converter);
return pvh;
}
public static PropertyValuesHolder ofKeyframe(String propertyName, Keyframe... values) {
KeyframeSet keyframeSet = KeyframeSet.ofKeyframe(values);
return ofKeyframes(propertyName, keyframeSet);
}
public static PropertyValuesHolder ofKeyframe(Property property, Keyframe... values) {
KeyframeSet keyframeSet = KeyframeSet.ofKeyframe(values);
return ofKeyframes(property, keyframeSet);
}
static PropertyValuesHolder ofKeyframes(String propertyName, Keyframes keyframes) {
...
}
static PropertyValuesHolder ofKeyframes(Property property, Keyframes keyframes) {
...
}
public void setIntValues(int... values) {
mValueType = int.class;
mKeyframes = KeyframeSet.ofInt(values);
}
public void setFloatValues(float... values) {
mValueType = float.class;
mKeyframes = KeyframeSet.ofFloat(values);
}
public void setKeyframes(Keyframe... values) {
...
}
public void setObjectValues(Object... values) {
...
}
public void setEvaluator(TypeEvaluator evaluator) {
mEvaluator = evaluator;
mKeyframes.setEvaluator(evaluator);
}
public void setPropertyName(String propertyName) {
mPropertyName = propertyName;
}
public void setProperty(Property property) {
mProperty = property;
}
...
}
3.4.1 PropertyValuesHolder的ofInt(x)、ofFloat(x)
直接看示例
findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
PropertyValuesHolder rotationHolder = PropertyValuesHolder.ofFloat("Rotation", 60f, -60f, 40f, -40f, -20f, 20f, 10f, -10f, 0f);
PropertyValuesHolder alphaHolder = PropertyValuesHolder.ofFloat("alpha", 0.1f, 1f, 0.1f, 1f);
ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(mTextView, rotationHolder, alphaHolder);
animator.setDuration(3000);
animator.start();
}
});
3.4.2 PropertyValuesHolder.ofObject(x)
再次引用ValueAnimator的ofObject(x)改变字符的动画例子来展示PropertyValuesHolder.ofObject(x)的用法。
自定义一个CharEvaluator(与ValueAnimator例子中的一样)
package com.example.objectanim;
import android.animation.TypeEvaluator;
public class CharEvaluator implements TypeEvaluator<Character> {
@Override
public Character evaluate(float fraction, Character startValue, Character endValue) {
int startInt = (int) startValue;
int endInt = (int) endValue;
int curInt = (int) (startInt + fraction * (endInt - startInt));
char result = (char) curInt;
return result;
}
}
自定义一个MyTextView,注意,TextView中有setText(CharSequence text)方法,其参数不是Character,因此我们在MyTextView中要定义一个有Character参数的方法:
package com.example.objectanim;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.TextView;
public class MyTextView extends TextView {
public MyTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* 动画类中设置属性为CharText或charText,再通过反射调用此方法
*/
public void setCharText(Character character) {
setText(String.valueOf(character));
}
}
布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="start anim" />
<com.example.objectanim.MyTextView
android:id="@+id/mytv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@android:color/darker_gray"
android:padding="20dp" />
</LinearLayout>
PropertyValuesHolder.ofObject(x)用法:
MyTextView mMyTv = findViewById(R.id.mytv);
findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
// 属性设置为CharText或charText
PropertyValuesHolder charHolder = PropertyValuesHolder.ofObject("CharText", new CharEvaluator(), new Character('A'), new Character('Z'));
ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(mMyTv, charHolder);
animator.setDuration(3000);
animator.setInterpolator(new AccelerateInterpolator());
animator.start();
}
});
动画效果与《ValueAnimator的进阶用法ofObject》部分相同,可返回查看。
3.4.3 PropertyValuesHolder.ofKeyframe(x)
先来看下抽象类Keyframe:
package android.animation;
public abstract class Keyframe implements Cloneable {
...
private TimeInterpolator mInterpolator = null;
public static Keyframe ofInt(float fraction, int value) {
return new IntKeyframe(fraction, value);
}
public static Keyframe ofInt(float fraction) {
return new IntKeyframe(fraction);
}
public static Keyframe ofFloat(float fraction, float value) {
return new FloatKeyframe(fraction, value);
}
public static Keyframe ofFloat(float fraction) {
return new FloatKeyframe(fraction);
}
public static Keyframe ofObject(float fraction, Object value) {
return new ObjectKeyframe(fraction, value);
}
public static Keyframe ofObject(float fraction) {
return new ObjectKeyframe(fraction, null);
}
public abstract void setValue(Object value);
public void setInterpolator(TimeInterpolator interpolator) {
mInterpolator = interpolator;
}
...
}
Keyframe可译为关键帧,一个关键帧必须包含两个元素:时间和位置。即这个关键帧表示的是某个物体在哪个时间点时应该在哪个位置上。
仍然以上述改变字符的动画作为示例:
MyTextView mMyTv = findViewById(R.id.mytv);
findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
Keyframe frame0 = Keyframe.ofObject(0f, new Character('A'));
Keyframe frame1 = Keyframe.ofObject(0.1f, new Character('L'));
Keyframe frame2 = Keyframe.ofObject(1, new Character('Z'));
PropertyValuesHolder frameHolder = PropertyValuesHolder.ofKeyframe("CharText", frame0, frame1, frame2);
frameHolder.setEvaluator(new CharEvaluator());
ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(mMyTv, frameHolder);
animator.setDuration(3000);
animator.start();
}
});
对比PropertyValuesHolder.ofObject(x)和PropertyValuesHolder.ofKeyframe(x)可以发现,用法大同小异。
动画效果与《ValueAnimator的进阶用法ofObject》部分相同,可返回查看。
用Keyframe实现动画的注意事项:
1、如果去掉第0帧,则将以第一个关键帧作为起始位置。
2、如果去掉结束帧,则将以最后一个关键帧作为结束位置。
3、使用Keyframe来构建动画,至少要有2帧。
要点:
不使用AnimatorSet,借助Keyframe也可以实现多个动画同时播放。Keyframe是ObjectAnimator中唯一的一个能实现多动画同时播放的方法,其它的如ObjectAnimator.ofInt(x)、ObjectAnimator.ofFloat(x)、ObjectAnimator.ofObject(x)等都只能实现针对一个属性做动画的操作。
4 组合动画AnimatorSet
ValueAnimator和ObjectAnimator只能单独实现一个动画,若想实现组合动画就需要用到AnimatorSet。
package android.animation;
public final class AnimatorSet extends Animator implements AnimationHandler.AnimationFrameCallback {
...
public void playTogether(Animator... items) {
if (items != null) {
Builder builder = play(items[0]);
for (int i = 1; i < items.length; ++i) {
builder.with(items[i]);
}
}
}
public void playTogether(Collection<Animator> items) {
if (items != null && items.size() > 0) {
Builder builder = null;
for (Animator anim : items) {
if (builder == null) {
builder = play(anim);
} else {
builder.with(anim);
}
}
}
}
public void playSequentially(Animator... items) {
if (items != null) {
if (items.length == 1) {
play(items[0]);
} else {
for (int i = 0; i < items.length - 1; ++i) {
play(items[i]).before(items[i + 1]);
}
}
}
}
public void playSequentially(List<Animator> items) {
if (items != null && items.size() > 0) {
if (items.size() == 1) {
play(items.get(0));
} else {
for (int i = 0; i < items.size() - 1; ++i) {
play(items.get(i)).before(items.get(i + 1));
}
}
}
}
@Override
public void setTarget(Object target) {
int size = mNodes.size();
for (int i = 0; i < size; i++) {
Node node = mNodes.get(i);
Animator animation = node.mAnimation;
if (animation instanceof AnimatorSet) {
((AnimatorSet)animation).setTarget(target);
} else if (animation instanceof ObjectAnimator) {
((ObjectAnimator)animation).setTarget(target);
}
}
}
@Override
public void setInterpolator(TimeInterpolator interpolator) {
mInterpolator = interpolator;
}
/**
* 获取Builder对象的唯一途径
*/
public Builder play(Animator anim) {
if (anim != null) {
return new Builder(anim);
}
return null;
}
@SuppressWarnings("unchecked")
@Override
public void cancel() {
if (Looper.myLooper() == null) {
throw new AndroidRuntimeException("Animators may only be run on Looper threads");
}
if (isStarted() || mStartListenersCalled) {
notifyListeners(AnimatorCaller.ON_CANCEL, false);
callOnPlayingSet(Animator::cancel);
mPlayingSet.clear();
endAnimation();
}
}
@Override
public boolean isRunning() {
if (mStartDelay == 0) {
return mStarted;
}
return mLastFrameTime > 0;
}
@Override
public boolean isStarted() {
return mStarted;
}
@Override
public void setStartDelay(long startDelay) {
...
}
@Override
public AnimatorSet setDuration(long duration) {
if (duration < 0) {
throw new IllegalArgumentException("duration must be a value of zero or greater");
}
mDependencyDirty = true;
// Just record the value for now - it will be used later when the AnimatorSet starts
mDuration = duration;
return this;
}
@Override
public AnimatorSet setDuration(long duration) {
if (duration < 0) {
throw new IllegalArgumentException("duration must be a value of zero or greater");
}
mDependencyDirty = true;
// Just record the value for now - it will be used later when the AnimatorSet starts
mDuration = duration;
return this;
}
...
private static class Node implements Cloneable {
...
}
private static class AnimationEvent {
...
}
private class SeekState {
...
}
public class Builder {
private Node mCurrentNode;
Builder(Animator anim) {
mDependencyDirty = true;
mCurrentNode = getNodeForAnimation(anim);
}
public Builder with(Animator anim) {
Node node = getNodeForAnimation(anim);
mCurrentNode.addSibling(node);
return this;
}
public Builder before(Animator anim) {
Node node = getNodeForAnimation(anim);
mCurrentNode.addChild(node);
return this;
}
public Builder after(Animator anim) {
Node node = getNodeForAnimation(anim);
mCurrentNode.addParent(node);
return this;
}
public Builder after(long delay) {
// setup a ValueAnimator just to run the clock
ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
anim.setDuration(delay);
after(anim);
return this;
}
}
}
AnimatorSet中的两个方法playSequentially(xxx)和playTogether(xxx)都可以实现组合动画效果,前者表示所有动画依次播放,后者表示所有动画一起开始(注意,只是一起开始)。
1、playSequentially(xxx)和playTogether(xxx)方法在开始动画时,只是把每个控件的动画激活,至于每个控件自身的动画是否延时、是否无限循环,只与控件本身的动画设定有关,与方法本身无关,因为方法本身只负责到时间激活动画。
2、playSequentially(xxx)方法只有在上一个控件完成动画以后才会激活下一个控件的动画。如果上一个控件的动画是无限循环的,则下一个控件就不指望做动画了。
playSequentially(xxx)和playTogether(xxx)不能自由的组合动画,而AnimatorSet.Builder类能实现两个控件一起开始的动画。
AnimatorSet继承自Animator,因此可以使用Animator中的监听器,但是:
1、AnimatorSet的监听方法只能监听AnimatorSet的状态,与其中的动画无关
2、AnimatorSet中没有设置循环的方法,所以动画执行一次就结束了,永远无法执行到onAnimationRepeat(xxx)方法。
用法示例:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:padding="10dp"
android:text="start anim" />
<TextView
android:id="@+id/tv_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_marginRight="30dp"
android:background="@android:color/darker_gray"
android:padding="10dp"
android:text="TextView-1" />
<TextView
android:id="@+id/tv_2"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:background="@android:color/darker_gray"
android:gravity="center"
android:padding="10dp"
android:text="TextView-2" />
</RelativeLayout>
package com.example.objectanim;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
public class playSequentiallyActivity extends Activity {
private Button mButton;
private TextView mTv1, mTv2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.play_sequentially_activity);
mButton = findViewById(R.id.btn);
mTv1 = findViewById(R.id.tv_1);
mTv2 = findViewById(R.id.tv_2);
mButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
// playSequentially(xxx)
ObjectAnimator tv1BgAnimator = ObjectAnimator.ofInt(mTv1, "BackgroundColor", 0xffff00ff, 0xffffff00, 0xffff00ff);
ObjectAnimator tv1TranslateY = ObjectAnimator.ofFloat(mTv1, "translationY", 0, 300, 0);
ObjectAnimator tv2TranslateY = ObjectAnimator.ofFloat(mTv2, "translationY", 0, 400, 0);
AnimatorSet animatorSet1 = new AnimatorSet();
animatorSet1.playSequentially(tv1BgAnimator, tv1TranslateY, tv2TranslateY);
animatorSet1.setDuration(1000);
animatorSet1.start();
// playTogether(xxx)
ObjectAnimator tv1BgAnimator2 = ObjectAnimator.ofInt(mTv1, "BackgroundColor", 0xffff00ff, 0xffffff00, 0xffff00ff);
ObjectAnimator tv1TranslateY2 = ObjectAnimator.ofFloat(mTv1, "translationY", 0, 400, 0);
ObjectAnimator tv2TranslateY2 = ObjectAnimator.ofFloat(mTv2, "translationY", 0, 400, 0);
AnimatorSet animatorSet2 = new AnimatorSet();
animatorSet2.playTogether(tv1BgAnimator2, tv1TranslateY2, tv2TranslateY2);
animatorSet2.setDuration(1000);
animatorSet2.start();
// 使用AnimatorSet.Builder类
ObjectAnimator tv1BgAnimator3 = ObjectAnimator.ofInt(mTv1, "BackgroundColor", 0xffff00ff, 0xffffff00, 0xffff00ff);
ObjectAnimator tv1TranslateY3 = ObjectAnimator.ofFloat(mTv1, "translationY", 0, 400, 0);
ObjectAnimator tv2TranslateY3 = ObjectAnimator.ofFloat(mTv2, "translationY", 0, 400, 0);
AnimatorSet animatorSet3 = new AnimatorSet();
AnimatorSet.Builder builder = animatorSet3.play(tv1TranslateY3);
builder.with(tv2TranslateY3);
builder.after(tv1BgAnimator3);
// 等价于上述三行代码
//animatorSet3.play(tv1TranslateY3).with(tv2TranslateY3).after(tv1BgAnimator3);
animatorSet3.setDuration(2000);
animatorSet3.start();
}
});
}
}
效果图省略。
5 XML实现Animator
在XML中与Animator对应的三个标签:
< animator />对应ValueAnimator;
< objectAnimator />对应ObjectAnimator;
< set />对应AnimatorSet。
要用XML方式实现Animator需要用到的类方法是AnimatorInflater.loadAnimator(xxx),该方法返回Animator,根据需要将其强转为目标动画类型即可。
注意:
View Animation(视图动画)中的Tween Animation(补间动画)的XML实现方式是调用类方法AnimationUtils.loadAnimation(xxx),该方法返回值是Animation。
package android.animation;
public class AnimatorInflater {
...
public static Animator loadAnimator(Context context, @AnimatorRes int id)
throws NotFoundException {
return loadAnimator(context.getResources(), context.getTheme(), id);
}
public static Animator loadAnimator(Resources resources, Theme theme, int id)
throws NotFoundException {
return loadAnimator(resources, theme, id, 1);
}
public static Animator loadAnimator(Resources resources, Theme theme, int id,
float pathErrorScale) throws NotFoundException {
...
}
private static ValueAnimator loadAnimator(Resources res, Theme theme,
AttributeSet attrs, ValueAnimator anim, float pathErrorScale)
throws NotFoundException {
...
}
...
}
用法示例:
通用布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<Button
android:id="@+id/start_anim"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="start anim" />
<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/darker_gray"
android:padding="10dp"
android:text="textview" />
</LinearLayout>
在res目录下新建anim文件夹,存放对应的动画的xml文件。
value_animator.xml:
<?xml version="1.0" encoding="utf-8"?>
<animator xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="1000"
android:interpolator="@android:anim/bounce_interpolator"
android:valueFrom="0"
android:valueTo="300"
android:valueType="intType" />
object_animator.xml:
<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="2000"
android:interpolator="@android:anim/accelerate_interpolator"
android:propertyName="TranslationY"
android:repeatCount="1"
android:repeatMode="reverse"
android:startOffset="2000"
android:valueFrom="0.0"
android:valueTo="400.0"
android:valueType="floatType" />
animator_set.xml:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:ordering="together">
<animator
android:duration="500"
android:propertyName="x"
android:valueFrom="0"
android:valueTo="400"
android:valueType="floatType" />
<objectAnimator
android:duration="500"
android:propertyName="y"
android:valueFrom="0"
android:valueTo="300"
android:valueType="floatType" />
</set>
在代码中使用各标签:
package com.example.objectanim;
import android.animation.AnimatorInflater;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
public class AnimXMLActivity extends Activity {
private TextView mTv1;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.value_anim_activity);
mTv1 = findViewById(R.id.tv);
findViewById(R.id.start_anim).setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
// 在代码中使用<animator/>,根据需要强转为目标动画类型
ValueAnimator valueAnimator = (ValueAnimator) AnimatorInflater.loadAnimator(AnimXMLActivity.this, R.animator.value_animator);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
public void onAnimationUpdate(ValueAnimator animation) {
int offset = (Integer) animation.getAnimatedValue();
mTv1.layout(offset, offset, mTv1.getWidth() + offset, mTv1.getHeight() + offset);
}
});
valueAnimator.start();
// 在代码中使用<objectAnimator/>,根据需要强转为目标动画类型
ObjectAnimator animator = (ObjectAnimator) AnimatorInflater.loadAnimator(AnimXMLActivity.this, R.animator.object_animator);
animator.setTarget(mTv1);
animator.start();
// 在代码中使用<set/>,根据需要强转为目标动画类型
AnimatorSet set = (AnimatorSet) AnimatorInflater.loadAnimator(AnimXMLActivity.this, R.animator.animator_set);
set.setTarget(mTv1);
set.start();
}
});
}
}
效果图略。
6 ViewPropertyAnimator
ViewPropertyAnimator在API Level 12(Android 3.1)时引入,位于包android.view中,是一个单独的类,不继承其它类,也不实现其它接口,仅包含三个具体内部类,其类图(Class Diagram)如图所示:
package android.view;
public class ViewPropertyAnimator {
...
final View mView;
private long mDuration;
private boolean mDurationSet = false;
private TimeInterpolator mInterpolator;
private Animator.AnimatorListener mListener = null;
private ValueAnimator.AnimatorUpdateListener mUpdateListener = null;
private ValueAnimator mTempValueAnimator;
private AnimatorEventListener mAnimatorEventListener = new AnimatorEventListener();
static final int NONE = 0x0000;
static final int TRANSLATION_X = 0x0001;
static final int TRANSLATION_Y = 0x0002;
static final int TRANSLATION_Z = 0x0004;
static final int SCALE_X = 0x0008;
static final int SCALE_Y = 0x0010;
static final int ROTATION = 0x0020;
static final int ROTATION_X = 0x0040;
static final int ROTATION_Y = 0x0080;
static final int X = 0x0100;
static final int Y = 0x0200;
static final int Z = 0x0400;
static final int ALPHA = 0x0800;
public @NonNull ViewPropertyAnimator setDuration(long duration) {
if (duration < 0) {
throw new IllegalArgumentException("Animators cannot have negative duration: " +
duration);
}
mDurationSet = true;
mDuration = duration;
return this;
}
public @NonNull ViewPropertyAnimator setStartDelay(long startDelay) {
if (startDelay < 0) {
throw new IllegalArgumentException("Animators cannot have negative start " +
"delay: " + startDelay);
}
mStartDelaySet = true;
mStartDelay = startDelay;
return this;
}
public @NonNull ViewPropertyAnimator setInterpolator(TimeInterpolator interpolator) {
mInterpolatorSet = true;
mInterpolator = interpolator;
return this;
}
public @NonNull ViewPropertyAnimator setListener(@Nullable Animator.AnimatorListener listener) {
mListener = listener;
return this;
}
public @NonNull ViewPropertyAnimator setUpdateListener(
@Nullable ValueAnimator.AnimatorUpdateListener listener) {
mUpdateListener = listener;
return this;
}
public void start() {
mView.removeCallbacks(mAnimationStarter);
startAnimation();
}
public @NonNull ViewPropertyAnimator x(float value) {
animateProperty(X, value);
return this;
}
public @NonNull ViewPropertyAnimator xBy(float value) {
animatePropertyBy(X, value);
return this;
}
public @NonNull ViewPropertyAnimator y(float value) {
animateProperty(Y, value);
return this;
}
public @NonNull ViewPropertyAnimator yBy(float value) {
animatePropertyBy(Y, value);
return this;
}
public @NonNull ViewPropertyAnimator z(float value) {
animateProperty(Z, value);
return this;
}
public @NonNull ViewPropertyAnimator zBy(float value) {
animatePropertyBy(Z, value);
return this;
}
public @NonNull ViewPropertyAnimator rotation(float value) {
animateProperty(ROTATION, value);
return this;
}
public @NonNull ViewPropertyAnimator rotationBy(float value) {
animatePropertyBy(ROTATION, value);
return this;
}
public @NonNull ViewPropertyAnimator rotationX(float value) {
animateProperty(ROTATION_X, value);
return this;
}
public @NonNull ViewPropertyAnimator rotationXBy(float value) {
animatePropertyBy(ROTATION_X, value);
return this;
}
public @NonNull ViewPropertyAnimator rotationY(float value) {
animateProperty(ROTATION_Y, value);
return this;
}
public @NonNull ViewPropertyAnimator rotationYBy(float value) {
animatePropertyBy(ROTATION_Y, value);
return this;
}
public @NonNull ViewPropertyAnimator translationX(float value) {
animateProperty(TRANSLATION_X, value);
return this;
}
public @NonNull ViewPropertyAnimator translationXBy(float value) {
animatePropertyBy(TRANSLATION_X, value);
return this;
}
public @NonNull ViewPropertyAnimator translationY(float value) {
animateProperty(TRANSLATION_Y, value);
return this;
}
public @NonNull ViewPropertyAnimator translationYBy(float value) {
animatePropertyBy(TRANSLATION_Y, value);
return this;
}
public @NonNull ViewPropertyAnimator translationZ(float value) {
animateProperty(TRANSLATION_Z, value);
return this;
}
public @NonNull ViewPropertyAnimator translationZBy(float value) {
animatePropertyBy(TRANSLATION_Z, value);
return this;
}
public @NonNull ViewPropertyAnimator scaleX(float value) {
animateProperty(SCALE_X, value);
return this;
}
public @NonNull ViewPropertyAnimator scaleXBy(float value) {
animatePropertyBy(SCALE_X, value);
return this;
}
public @NonNull ViewPropertyAnimator scaleY(float value) {
animateProperty(SCALE_Y, value);
return this;
}
public @NonNull ViewPropertyAnimator scaleYBy(float value) {
animatePropertyBy(SCALE_Y, value);
return this;
}
public @NonNull ViewPropertyAnimator alpha(@FloatRange(from = 0.0f, to = 1.0f) float value) {
animateProperty(ALPHA, value);
return this;
}
public @NonNull ViewPropertyAnimator alphaBy(float value) {
animatePropertyBy(ALPHA, value);
return this;
}
private void startAnimation() {
...
}
private void animateProperty(int constantName, float toValue) {
float fromValue = getValue(constantName);
float deltaValue = toValue - fromValue;
animatePropertyBy(constantName, fromValue, deltaValue);
}
private void animatePropertyBy(int constantName, float byValue) {
float fromValue = getValue(constantName);
animatePropertyBy(constantName, fromValue, byValue);
}
/**
* 实现动画的核心逻辑
*
* Utility function, called by animateProperty() and animatePropertyBy(), which handles the
* details of adding a pending animation and posting the request to start the animation.
*
* @param constantName The specifier for the property being animated
* @param startValue The starting value of the property
* @param byValue The amount by which the property will change
*/
private void animatePropertyBy(int constantName, float startValue, float byValue) {
// First, cancel any existing animations on this property
if (mAnimatorMap.size() > 0) {
Animator animatorToCancel = null;
Set<Animator> animatorSet = mAnimatorMap.keySet();
for (Animator runningAnim : animatorSet) {
PropertyBundle bundle = mAnimatorMap.get(runningAnim);
if (bundle.cancel(constantName)) {
// property was canceled - cancel the animation if it's now empty
// Note that it's safe to break out here because every new animation
// on a property will cancel a previous animation on that property, so
// there can only ever be one such animation running.
if (bundle.mPropertyMask == NONE) {
// the animation is no longer changing anything - cancel it
animatorToCancel = runningAnim;
break;
}
}
}
if (animatorToCancel != null) {
animatorToCancel.cancel();
}
}
NameValuesHolder nameValuePair = new NameValuesHolder(constantName, startValue, byValue);
mPendingAnimations.add(nameValuePair);
mView.removeCallbacks(mAnimationStarter);
mView.postOnAnimation(mAnimationStarter);
}
private static class PropertyBundle {
...
}
static class NameValuesHolder {
int mNameConstant;
float mFromValue;
float mDeltaValue;
NameValuesHolder(int nameConstant, float fromValue, float deltaValue) {
mNameConstant = nameConstant;
mFromValue = fromValue;
mDeltaValue = deltaValue;
}
}
private class AnimatorEventListener
implements Animator.AnimatorListener, ValueAnimator.AnimatorUpdateListener {
@Override
public void onAnimationStart(@NonNull Animator animation) {
...
}
@Override
public void onAnimationCancel(@NonNull Animator animation) {
if (mListener != null) {
mListener.onAnimationCancel(animation);
}
if (mAnimatorOnEndMap != null) {
mAnimatorOnEndMap.remove(animation);
}
}
@Override
public void onAnimationRepeat(@NonNull Animator animation) {
if (mListener != null) {
mListener.onAnimationRepeat(animation);
}
}
@Override
public void onAnimationEnd(@NonNull Animator animation) {
...
}
@Override
public void onAnimationUpdate(@NonNull ValueAnimator animation) {
...
}
}
...
}
ViewPropertyAnimator通过与View类关联来实现动画效果:
package android.view;
@UiThread
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {
...
protected Animation mCurrentAnimation = null;
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
private ViewPropertyAnimator mAnimator = null;
public void startAnimation(Animation animation) {
animation.setStartTime(Animation.START_ON_FIRST_FRAME);
setAnimation(animation);
invalidateParentCaches();
invalidate(true);
}
/**
* View子类可通过该方法获取ViewPropertyAnimator对象
*/
public ViewPropertyAnimator animate() {
if (mAnimator == null) {
mAnimator = new ViewPropertyAnimator(this);
}
return mAnimator;
}
...
}
通过ViewPropertyAnimator实现动画效果非常简单,如布局文件中定义一个TextView,直接链式调用animate()方法即可,且链式调用后也可直接设置监听(其内部有两个设置监听的方法):
findViewById(R.id.tv).animate().x(50).scaleX(100).alphaBy(0.5f)
.setListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(@NonNull Animator animation) {
}
@Override
public void onAnimationEnd(@NonNull Animator animation) {
}
@Override
public void onAnimationCancel(@NonNull Animator animation) {
}
@Override
public void onAnimationRepeat(@NonNull Animator animation) {
}
});
注意事项
1、animate():动画从调用View的animate()方法开始,它返回一个ViewPropertyAnimator对象,可通过这个对象的其它方法设置需要实现动画的属性。
2、自动开始:这种方式启动动画是隐式的,不需要显示调用start()方法。但实际上,这个动画会在下一次界面刷新的时候启动。ViewPropertyAnimator正是通过这个机制将所有动画结合在一起的,如果继续声明动画,它会继续将这些动画添加到将在下一帧开始的动画列表中,当声明完毕并结束对UI线程的控制后,事件队列机制开始起作用,动画就开始了。
3、流畅:ViewPropertyAnimator允许将多个方法串行起来进行链式调用,且每个方法都返回ViewPropertyAnimator实例。
7 性能对比
ViewPropertyAnimator并没有像ObjectAnimator一样使用反射或JNI技术,ViewPropertyAnimator会根据预设的每一个动画帧计算出对应的所有属性值,并设置给控件,然后再调用一次invalidate()方法进行重绘,从而解决了在使用ObjectAnimator时每个属性单独计算、单独重绘的问题。因此ViewPropertyAnimator相对于ObjectAnimator和组合动画,性能有所提升。
ObjectAnimator可以灵活、方便地为任何对象和属性做动画,但当需要同时为View的多个属性(SDK提供的、非自定义扩展的)做动画时,ViewPropertyAnimator会更方便。
使用ObjectAnimator并不需要担心性能问题,使用反射和JNI等技术所带来的开销相对于整个程序来说影响很小。且ViewPropertyAnimator的优势也不是性能提升,而是代码的可读性较好。
另外,实现一个组合动画(动画不一定是同时播放)有三种方式:
1、AnimatorSet
ObjectAnimator animX = ObjectAnimator.ofFloat(myView, "x", 50f);
ObjectAnimator animY = ObjectAnimator.ofFloat(myView, "y", 100f);
AnimatorSet animSet = new AnimatorSet();
animSet.playTogether(animX, animY);
animSet.start();
2、PropertyValuesHolder
PropertyValuesHolder pvhX = PropertyValuesHolder.ofFloat("x", 50f);
PropertyValuesHolder pvhY = PropertyValuesHolder.ofFloat("y", 100f);
ObjectAnimator.ofPropertyValuesHolder(myView, pvhX, pvhY);
3、ViewPropertyAnimator
使用ViewPropertyAnimator的相关方法直接链式调用即可。
8 为ViewGroup内的组件添加动画
ViewAnimator、ObjectAnimator、AnimatorSet都只能针对一个控件做动画,如果想对ViewGroup内部控件做统一入场动画、出场动画,如给ListView的每个Item添加入场、出场动画是无法实现的。
为ViewGroup内的组件添加动画,Android提供了4种方法:
1、layoutAnimation标签与LayoutAnimationController
2、gridLayoutAnimation标签与GridLayoutAnimationController
3、android:animateLayoutChanges属性
4、LayoutTransition
前两种是在API 1时引入的,layoutAnimation标签是专门用于新建ListView时添加入场动画的,且动画可以自定义,但不能对之后再次添加的item实现入场动画。gridLayoutAnimation标签是专门用于新建GridView时添加入场动画的,其与layoutAnimation标签的性质和用法相似。这两种有bug不做演示。
后两种是在API 11时引入的,为了支持ViewGroup类控件在添加或移除其中的控件时自动添加动画,在XML中设置属性android:animateLayoutChanges="true/false"就能实现在添加或删除其中的控件时带有默认动画,所有ViewGroup子类均具有此属性,缺点是该动画不能自定义。
LayoutTransition可实现在ViewGroup动态添加或删除其中的控件时指定动画,且动画可以自定义。
8.1 android:animateLayoutChanges属性
开启该动画后是默认的哦,且不能自定义。
示例布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/add_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="添加控件" />
<Button
android:id="@+id/remove_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="移除控件" />
</LinearLayout>
<LinearLayout
android:id="@+id/linearlayoutContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:orientation="vertical" />
</LinearLayout>
示例代码:
public class AnimateLayoutChangesActivity extends Activity {
private LinearLayout linearLayoutContainer;
private int i = 0;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.animate_layout_changes_activity);
linearLayoutContainer = (LinearLayout) findViewById(R.id.linearlayoutContainer);
findViewById(R.id.add_btn).setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
addButtonView();
}
});
findViewById(R.id.remove_btn).setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
removeButtonView();
}
});
}
private void addButtonView() {
i++;
Button button = new Button(this);
button.setText("button " + i);
button.setAllCaps(false);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
);
button.setLayoutParams(params);
linearLayoutContainer.addView(button, 0);
}
private void removeButtonView() {
if (i > 0) {
linearLayoutContainer.removeViewAt(0);
}
i--;
}
}
效果图:
8.2 LayoutTransition
该动画可以自定义。
ViewGroup中有一个setLayoutTransition(x)方法,源码:
package android.view;
@UiThread
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
...
private LayoutTransition mTransition;
public void setLayoutTransition(LayoutTransition transition) {
if (mTransition != null) {
LayoutTransition previousTransition = mTransition;
previousTransition.cancel();
previousTransition.removeTransitionListener(mLayoutTransitionListener);
}
mTransition = transition;
if (mTransition != null) {
mTransition.addTransitionListener(mLayoutTransitionListener);
}
}
private LayoutTransition.TransitionListener mLayoutTransitionListener =
new LayoutTransition.TransitionListener() {
@Override
public void startTransition(LayoutTransition transition, ViewGroup container,
View view, int transitionType) {
// We only care about disappearing items, since we need special logic to keep
// those items visible after they've been 'removed'
if (transitionType == LayoutTransition.DISAPPEARING) {
startViewTransition(view);
}
}
@Override
public void endTransition(LayoutTransition transition, ViewGroup container,
View view, int transitionType) {
if (mLayoutCalledWhileSuppressed && !transition.isChangingLayout()) {
requestLayout();
mLayoutCalledWhileSuppressed = false;
}
if (transitionType == LayoutTransition.DISAPPEARING && mTransitioningViews != null) {
endViewTransition(view);
}
}
};
...
}
而LayoutTransition是一个单独的类,源码:
package android.animation;
public class LayoutTransition {
/**
* 5个transitionType
*/
public static final int CHANGE_APPEARING = 0;
public static final int CHANGE_DISAPPEARING = 1;
public static final int APPEARING = 2;
public static final int DISAPPEARING = 3;
public static final int CHANGING = 4;
...
private static long DEFAULT_DURATION = 300;
public void setDuration(long duration) {
mChangingAppearingDuration = duration;
mChangingDisappearingDuration = duration;
mChangingDuration = duration;
mAppearingDuration = duration;
mDisappearingDuration = duration;
}
public void setStartDelay(int transitionType, long delay) {
...
}
public void setDuration(int transitionType, long duration) {
...
}
public void setStagger(int transitionType, long duration) {
...
}
public void setInterpolator(int transitionType, TimeInterpolator interpolator) {
...
}
public void setAnimator(int transitionType, Animator animator) {
...
}
public void layoutChange(ViewGroup parent) {
if (parent.getWindowVisibility() != View.VISIBLE) {
return;
}
if ((mTransitionTypes & FLAG_CHANGING) == FLAG_CHANGING && !isRunning()) {
runChangeTransition(parent, null, CHANGING);
}
}
public void addTransitionListener(TransitionListener listener) {
if (mListeners == null) {
mListeners = new ArrayList<TransitionListener>();
}
mListeners.add(listener);
}
public void removeTransitionListener(TransitionListener listener) {
if (mListeners == null) {
return;
}
mListeners.remove(listener);
}
public interface TransitionListener {
public void startTransition(LayoutTransition transition, ViewGroup container,
View view, int transitionType);
public void endTransition(LayoutTransition transition, ViewGroup container,
View view, int transitionType);
}
...
}
LayoutTransition的用法步骤:
1、创建LayoutTransition实例
LayoutTransition transition = new LayoutTransition();
2、创建并设置Animator动画
ObjectAnimator animIn = ObjectAnimator.ofFloat(null, "rotationY", 0f, 360f, 0f);
transition.setAnimator(LayoutTransition.APPEARING, animIn);
3、将LayoutTransition设置到ViewGroup
linearLayoutContainer.setLayoutTransition(transition);
布局文件同上,示例代码:
package com.example.myapplication;
import android.animation.Keyframe;
import android.animation.LayoutTransition;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
public class AnimateLayoutChangesActivity extends Activity {
private LinearLayout linearLayoutContainer;
private int i = 0;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.animate_layout_changes_activity);
linearLayoutContainer = (LinearLayout) findViewById(R.id.linearlayoutContainer);
LayoutTransition transition = new LayoutTransition();
// 入场动画:view在这个容器中消失时触发的动画
ObjectAnimator animIn = ObjectAnimator.ofFloat(null, "rotationY", 0f, 360f, 0f);
transition.setAnimator(LayoutTransition.APPEARING, animIn);
// 出场动画:view显示时的动画
ObjectAnimator animOut = ObjectAnimator.ofFloat(null, "rotation", 0f, 90f, 0f);
transition.setAnimator(LayoutTransition.DISAPPEARING, animOut);
transition.addTransitionListener(new LayoutTransition.TransitionListener() {
@Override
public void startTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) {
Log.d(
"rizhi",
"开始" + "transitionType: " + transitionType + ", count: " + container.getChildCount() + ", view: " + view.getClass().getName()
);
}
@Override
public void endTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) {
Log.d(
"rizhi",
"结束" + "transitionType: " + transitionType + ",count: " + container.getChildCount() + ",view: " + view.getClass().getName()
);
}
});
linearLayoutContainer.setLayoutTransition(transition);
findViewById(R.id.add_btn).setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
addButtonView();
}
});
findViewById(R.id.remove_btn).setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
removeButtonView();
}
});
}
private void addButtonView() {
i++;
Button button = new Button(this);
button.setText("button " + i);
button.setAllCaps(false);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
);
button.setLayoutParams(params);
linearLayoutContainer.addView(button, 0);
}
private void removeButtonView() {
if (i > 0) {
linearLayoutContainer.removeViewAt(0);
}
i--;
}
}
效果图:
源码中标注的transitionType共有5个,上述示例测试的是LayoutTransition.APPEARING和LayoutTransition.DISAPPEARING,而LayoutTransition.CHANGE_APPEARING和LayoutTransition.CHANGE_DISAPPEARING的用法有很多注意事项,它两必须使用PropertyValuesHolder所构造的动画才有效果,各自使用实例如下,具体不再演示了。LayoutTransition.CHANGING也不演示。
测试LayoutTransition.CHANGE_APPEARING,其余代码相同:
PropertyValuesHolder pvhLeft = PropertyValuesHolder.ofInt("left", 0, 0);
PropertyValuesHolder pvhTop = PropertyValuesHolder.ofInt("top", 0, 0);
PropertyValuesHolder pvhScaleX = PropertyValuesHolder.ofFloat("scaleX", 1f, 0f, 1f);
Animator changeAppearAnimator = ObjectAnimator.ofPropertyValuesHolder(
linearLayoutContainer,
pvhLeft,
pvhTop,
pvhScaleX
);
transition.setAnimator(LayoutTransition.CHANGE_APPEARING, changeAppearAnimator);
测试LayoutTransition.CHANGE_DISAPPEARING,其余代码相同:
PropertyValuesHolder outLeft = PropertyValuesHolder.ofInt("left", 0, 0);
PropertyValuesHolder outTop = PropertyValuesHolder.ofInt("top", 0, 0);
Keyframe frame0 = Keyframe.ofFloat(0f, 0);
Keyframe frame1 = Keyframe.ofFloat(0.1f, -20f);
Keyframe frame2 = Keyframe.ofFloat(0.2f, 20f);
Keyframe frame3 = Keyframe.ofFloat(0.3f, -20f);
Keyframe frame4 = Keyframe.ofFloat(0.4f, 20f);
Keyframe frame5 = Keyframe.ofFloat(0.5f, -20f);
Keyframe frame6 = Keyframe.ofFloat(0.6f, 20f);
Keyframe frame7 = Keyframe.ofFloat(0.7f, -20f);
Keyframe frame8 = Keyframe.ofFloat(0.8f, 20f);
Keyframe frame9 = Keyframe.ofFloat(0.9f, -20f);
Keyframe frame10 = Keyframe.ofFloat(1, 0);
PropertyValuesHolder mPropertyValuesHolder = PropertyValuesHolder.ofKeyframe(
"rotation",
frame0,
frame1,
frame2,
frame3,
frame4,
frame5,
frame6,
frame7,
frame8,
frame9,
frame10
);
ObjectAnimator mObjectAnimatorChangeDisAppearing = ObjectAnimator.ofPropertyValuesHolder(
this,
outLeft,
outTop,
mPropertyValuesHolder
);
transition.setAnimator(LayoutTransition.CHANGE_DISAPPEARING, mObjectAnimatorChangeDisAppearing);
参考文献:
[1] UML中的类图及类图之间的关系
[2] 启舰.Android自定义控件开发入门与实战[M].北京:电子工业出版社,2018
微信公众号:TechU