Android 动画分为 3 种,View 动画、帧动画、属性动画。在本章中,首先简单介绍View动画以及自定义View动画的方式,接着介绍View动画的一些特殊的使用场景,最后对属性动画做一个全面性的介绍,另外还介绍使用动画的一些注意事项。
View 动画
作用对象是 View。支持 4 种 动画效果。
- 平移动画
- 缩放动画
- 旋转动画
- 透明度动画
View 动画的种类
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@[package:]anim/interpolator_resource"
android:shareInterpolator=["true" | "false"] >
<alpha
android:fromAlpha="float"
android:toAlpha="float"/>
<scale
android:fromXScale="float"
android:toXScale="float"
android:fromYScale="float"
android:toYScale="float"
android:pivotX="float"
android:pivotY="float"/>
<translate
android:fromXDelta="float"
android:toXDelta="float"
android:fromYDelta="float"
android:toYDelta="float"/>
<rotate
android:fromDegrees="float"
android:toDegrees="float"
android:pivotX="float"
android:pivotY="float"/>
</set>
复制代码
android:interpolater
集合所采用的插值器,默认为
@android:anim/accelerate_decelerate_interpolator
android:shareInterpolater
集合中的动画是否和集合共享一个插值器。
自定义 View 动画
继承 Animation
。
public class CustomAnimation extends Animation {
@Override public void initialize(int width, int height, int parentWidth, int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
// 做一些初始化工作
}
@Override protected void applyTransformation(float interpolatedTime, Transformation t) {
super.applyTransformation(interpolatedTime, t);
// 进行相应的矩阵变化
}
}
复制代码
帧动画
顺序播放一组预先定义好的图片。系统提供了 AnimationDrawable
来使用帧动画。
frame_animation.xml
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
<item
android:drawable="@mipmap/ic_launcher"
android:duration="500"/>
<item
android:drawable="@mipmap/ic_launcher_round"
android:duration="500"/>
<item
android:drawable="@mipmap/egg"
android:duration="500"/>
</animation-list>
复制代码
textView.setBackgroundResource(R.drawable.frame_animation);
AnimationDrawable background = (AnimationDrawable) textView.getBackground();
background.start();
复制代码
帧动画的使用比较简单,但是比较容易引起OOM,所以在使用帧动画时应尽量避免使用过多尺寸较大的图片。
View 动画的特殊使用场景
LayoutAnimation
作用于 ViewGroup, 为 ViewGroup 指定一个动画,当它的子元素出场时都会具有这种效果。
layout_animation.xml
<?xml version="1.0" encoding="utf-8"?>
<layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
android:delay="0.5"
android:animationOrder="normal"
android:animation="@anim/anim_item"
>
</layoutAnimation>
复制代码
anim_item.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="300"
android:interpolator="@android:anim/accelerate_interpolator"
android:shareInterpolator="true"
>
<alpha
android:fromAlpha="0.0"
android:toAlpha="1.0"
/>
<translate
android:fromXDelta="500"
android:toXDelta="0"
/>
</set>
复制代码
android:delay
子元素开始动画的时间延迟,如果子元素入场动画的时间周期为300ms,0.5 代表每个子元素都要延迟150ms才能播放入场动画。总体来说,第一个子元素延迟150ms开始播放入场动画,第2个子元素延迟300ms开始播放入场动画,依次类推。
android:animationOrder
normal表示顺序显示,reverse表示逆向显示,random则是随机播放入场动画。
android:animation
为元素指定具体的入场动画
指定android:layoutAnimation
属性
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerview"
android:layoutAnimation="@anim/layout_animation"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
复制代码
Activity 的切换效果
Github项目解析(九)-->实现Activity跳转动画的五种方式
/**
* Call immediately after one of the flavors of {@link #startActivity(Intent)}
* or {@link #finish} to specify an explicit transition animation to
* perform next.
*
* <p>As of {@link android.os.Build.VERSION_CODES#JELLY_BEAN} an alternative
* to using this with starting activities is to supply the desired animation
* information through a {@link ActivityOptions} bundle to
* {@link #startActivity(Intent, Bundle)} or a related function. This allows
* you to specify a custom animation even when starting an activity from
* outside the context of the current top activity.
*
* @param enterAnim A resource ID of the animation resource to use for
* the incoming activity. Use 0 for no animation.
* @param exitAnim A resource ID of the animation resource to use for
* the outgoing activity. Use 0 for no animation.
*/
public void overridePendingTransition(int enterAnim, int exitAnim) {
try {
ActivityManagerNative.getDefault().overridePendingTransition(
mToken, getPackageName(), enterAnim, exitAnim);
} catch (RemoteException e) {
}
}
复制代码
在 startActiviy 或者 finish 之后立刻执行。
属性动画
API11 之后加入。 API11 之前使用 nineoldandroids 代替。
使用属性动画
对任意对象的属性进行动画。
默认帧率是 10ms/帧。
常用的动画类:
- ValueAnimator
- ObjectAnimator : 继承自ValueAnimator
- AnimatorSet : 动画集合
/**
* 改变一个对象(myObject)的translationY属性,让其沿着Y轴向上平移一段距离:它的高度,该动画在
* 默认时间内完成,动画的完成时间是可以定义的。
*/
private void example1(View view) {
ObjectAnimator.ofFloat(view, "translationY", view.getHeight()).start();
}
/**
* 改变一个对象的背景色属性,典型的情形是改变View的背景色,下面的动画可以让背景色在3秒内实现从0xFFFF8080到
* 0xFF8080FF的渐变,动画会无限循环而且会有反转的效果。
*/
private void example2(View view) {
ObjectAnimator animator = ObjectAnimator.ofInt(view, "backgroundColor", 0xFFFF8080, 0xFF8080FF);
animator.setDuration(3000);
animator.setEvaluator(new ArgbEvaluator());
animator.setRepeatCount(ValueAnimator.INFINITE); // 无限循环
animator.setRepeatMode(ValueAnimator.REVERSE);
animator.start();
}
/**
* 动画集合,5秒内对View的旋转、平移、缩放和透明度都进行了改变。
*/
private void example3(View view) {
AnimatorSet set = new AnimatorSet();
set.playSequentially(
ObjectAnimator.ofFloat(view,"rotationX",0,360),
ObjectAnimator.ofFloat(view,"rotationY",0,180),
ObjectAnimator.ofFloat(view,"translationX",0,90),
ObjectAnimator.ofFloat(view,"translationY",0,90),
ObjectAnimator.ofFloat(view,"scaleX",1,1.5f),
ObjectAnimator.ofFloat(view,"scaleY",1,0.5f),
ObjectAnimator.ofFloat(view,"alpha",1,0.25f,1)
);
set.setDuration(5000).start();
}
复制代码
插值器和估值器
TimeInterpolate: 根据时间流逝的百分比来计算当前的属性值改变的百分比。
系统预置了:
- LinearInterpolate (线性插值器:匀速动画)
- AccelerateDecelerateInterpolator (加减速插值器,动画两头慢中间快)
- DecelerateInterpolater (减速插值器:动画越来越慢)
TypeEvaluator:类型估值算法。根据当前属性改变的百分比来计算改变后的属性值。
系统预置的有
- IntEvaluator
- FloatEvaluator
- ArgbEvaluator (针对Color属性)
android.view.animation.LinearInterpolator
/**
* An interpolator where the rate of change is constant
*/
@HasNativeInterpolator
public class LinearInterpolator extends BaseInterpolator implements NativeInterpolatorFactory {
public LinearInterpolator() {
}
public LinearInterpolator(Context context, AttributeSet attrs) {
}
/**
* 输入值和返回值一样
*/
public float getInterpolation(float input) {
return input;
}
/** @hide */
@Override
public long createNativeInterpolator() {
return NativeInterpolatorFactoryHelper.createLinearInterpolator();
}
}
复制代码
android.animation.IntEvaluator
/**
* This evaluator can be used to perform type interpolation between <code>int</code> values.
*/
public class IntEvaluator implements TypeEvaluator<Integer> {
/**
* This function returns the result of linearly interpolating the start and end values, with
* <code>fraction</code> representing the proportion between the start and end values. The
* calculation is a simple parametric calculation: <code>result = x0 + t * (v1 - v0)</code>,
* where <code>x0</code> is <code>startValue</code>, <code>x1</code> is <code>endValue</code>,
* and <code>t</code> is <code>fraction</code>.
*
* @param fraction The fraction from the starting to the ending values 估值小数
* @param startValue The start value; should be of type <code>int</code> or
* <code>Integer</code> 开始值
* @param endValue The end value; should be of type <code>int</code> or <code>Integer</code>结束值
* @return A linear interpolation between the start and end values, given the
* <code>fraction</code> parameter.
*/
public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
int startInt = startValue;
return (int)(startInt + fraction * (endValue - startInt));
}
}
复制代码
属性动画要求对象的属性必须有 set 和 get 方法。
自定义插值器需要实现 Interpolator 或者 TimeInterpolater,自定义估值算法需要实现 TypeEvaluator。
如果要对其他类型(非 int 、float、Color)做动画,必须要自定义类型估值算法。
属性动画的监听器
主要有 2 个接口: AnimatorUpdateListener
和 AnimatorListener
。
android.animation.Animator.AnimatorListener
public static interface AnimatorListener {
void onAnimationStart(Animator animation);
void onAnimationEnd(Animator animation);
void onAnimationCancel(Animator animation);
void onAnimationRepeat(Animator animation);
}
复制代码
android.animation.ValueAnimator.AnimatorUpdateListener
// 监听整个动画过程,每播放一帧就会回调一次。
public static interface AnimatorUpdateListener {
/**
* <p>Notifies the occurrence of another frame of the animation.</p>
*
* @param animation The animation which was repeated.
*/
void onAnimationUpdate(ValueAnimator animation);
}
复制代码
对任意属性做动画
Q:给Button加一个动画,让这个Button的宽度从当前宽度增加到500px。
A:View动画只支持4种类型:平移(Translate),旋转(Rotate),缩放(Scale),透明度(Alpha)。无法达到需求。
属性动画的原理:需要动画对象提供 get,set 方法,属性动画根据外界传递的该属性的初始值和最终值,以动画效果多次去调用 set 方法。
总结:对 Object 的属性 abc 做动画,需要同时满足 2 个条件:
- Object 必须要提供 setAbc() 方法,如果动画的时候没有传递初始值,那么还需要提供 getAbc() 方法。因为系统需要去获取 adc 属性的初始值(若不满足,app 直接 crash)。
- Object 的 setAbc() 方法对属性 abc 所做的改变必须能够通过某种方法反映出来,比如会带来 Ui的改变。(若不满足,动画没有效果但不会 crash)。
如果直接对 Button 的 windth 使用属性动画就会没有动画效果。
set方法在 TextView 中
android.widget.TextView#setWidth
// 设置最大宽度和最小宽度
public void setWidth(int pixels) {
mMaxWidth = mMinWidth = pixels;
mMaxWidthMode = mMinWidthMode = PIXELS;
requestLayout();
invalidate();
}
复制代码
get方法在 View 中。
android.view.View#getWidth
// 获取 view 的宽度
public final int getWidth() {
return mRight - mLeft;
}
复制代码
解决方法
-
给对象加上 set,get 方法
内部SDK没有权限。
-
包装类
private class ViewWraper {
private View mTarget;
public ViewWraper(View target) {
mTarget = target;
}
public int getWidth() {
return mTarget.getWidth();
}
public void setWidth(int width) {
mTarget.getLayoutParams().width = width;
mTarget.requestLayout();
}
}
复制代码
- 监听动画过程,自行实现属性的改变。
ValueAnimator 本身不作用于任何对象,直接使用它没有任何动画效果。它可以对一个值做动画,然后我们可以监听其动画过程。
属性动画的工作原理
属性动画要求动画作用的对象提供该属性的set方法,属性动画根据你传递的该属性的初始值和最终值,以动画的效果多次去调用set方法。每次传递给set方法的值都不一样,确切来说是随着时间的推移,所传递的值越来越接近最终值。如果动画的时候没有传递初始值,那么还要提供get方法,因为系统要去获取属性的初始值。
android.animation.ObjectAnimator#start
@Override
public void start() {
// 如果当前动画、等待的动画(Pending)和延迟的动画(delay)中有何当前相同的动画,就把相同的动画给取消掉。
AnimationHandler.getInstance().autoCancelBasedOn(this);
if (DBG) {
Log.d(LOG_TAG, "Anim target, duration: " + getTarget() + ", " + getDuration());
for (int i = 0; i < mValues.length; ++i) {
PropertyValuesHolder pvh = mValues[i];
Log.d(LOG_TAG, " Values[" + i + "]: " +
pvh.getPropertyName() + ", " + pvh.mKeyframes.getValue(0) + ", " +
pvh.mKeyframes.getValue(1));
}
}
// 调用父类的 start
super.start();
}
复制代码
android.animation.ValueAnimator#start
private void start(boolean playBackwards) {
// 需要运行在有 looper 的线程中
if (Looper.myLooper() == null) {
throw new AndroidRuntimeException("Animators may only be run on Looper threads");
}
mReversing = playBackwards;
// Special case: reversing from seek-to-0 should act as if not seeked at all.
if (playBackwards && mSeekFraction != -1 && mSeekFraction != 0) {
if (mRepeatCount == INFINITE) {
// Calculate the fraction of the current iteration.
float fraction = (float) (mSeekFraction - Math.floor(mSeekFraction));
mSeekFraction = 1 - fraction;
} else {
mSeekFraction = 1 + mRepeatCount - mSeekFraction;
}
}
mStarted = true;
mPaused = false;
mRunning = false;
mAnimationEndRequested = false;
// Resets mLastFrameTime when start() is called, so that if the animation was running,
// calling start() would put the animation in the
// started-but-not-yet-reached-the-first-frame phase.
mLastFrameTime = 0;
AnimationHandler animationHandler = AnimationHandler.getInstance();
animationHandler.addAnimationFrameCallback(this, (long) (mStartDelay * sDurationScale));
if (mStartDelay == 0 || mSeekFraction >= 0) {
// If there's no start delay, init the animation and notify start listeners right away
// to be consistent with the previous behavior. Otherwise, postpone this until the first
// frame after the start delay.
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);
}
}
}
复制代码
android.animation.ValueAnimator#doAnimationFrame
public final void doAnimationFrame(long frameTime) {
AnimationHandler handler = AnimationHandler.getInstance();
if (mLastFrameTime == 0) {
// First frame
handler.addOneShotCommitCallback(this);
if (mStartDelay > 0) {
startAnimation();
}
if (mSeekFraction < 0) {
mStartTime = frameTime;
} else {
long seekTime = (long) (getScaledDuration() * mSeekFraction);
mStartTime = frameTime - seekTime;
mSeekFraction = -1;
}
mStartTimeCommitted = false; // allow start time to be compensated for jank
}
mLastFrameTime = frameTime;
if (mPaused) {
mPauseTime = frameTime;
handler.removeCallback(this);
return;
} else if (mResumed) {
mResumed = false;
if (mPauseTime > 0) {
// Offset by the duration that the animation was paused
mStartTime += (frameTime - mPauseTime);
mStartTimeCommitted = false; // allow start time to be compensated for jank
}
handler.addOneShotCommitCallback(this);
}
// The frame time might be before the start time during the first frame of
// an animation. The "current time" must always be on or after the start
// time to avoid animating frames at negative time intervals. In practice, this
// is very rare and only happens when seeking backwards.
final long currentTime = Math.max(frameTime, mStartTime);
boolean finished = animateBasedOnTime(currentTime);
if (finished) {
endAnimation();
}
}
复制代码
在初始化的时候,如果属性的初始值没有提供,则get方法会被调用,get方法是用反射来调用的。
android.animation.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());
}
}
}
复制代码
当下一帧到来的时候,PropertyValuesHolder 中的 setAnimatedValue 会将新的属性值设置给对象。set 也是通过反射来调用的。
android.animation.PropertyValuesHolder#setAnimatedValue
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());
}
}
}
复制代码
注意事项
-
OOM
主要出现在帧动画中,尽量避免使用帧动画。 -
内存泄漏
当有无限循环的动画时,需要在 Activity 退出时及时停止。View 动画不存在此问题。 -
兼容性
在 3.0 以下需要适配。 -
View 动画的问题
View 动画是对 View 的影像做动画,并不是真正的改变 View 的状态,因此有时会出现动画完成后 View 无法隐藏的现象,即view.setVisible(GONE)
失效了,这时只需要调用view.clearAnimation()
清除 View 动画就可以解决。 -
不要使用 px
尽量使用 dp -
动画元素的交互
将view移动(平移)后,在Android 3.0以前的系统上,不管是View动画还是属性动画,新位置均无法触发单击事件,同时,老位置仍然可以触发单击事件。尽管View已经在视觉上不存在了,将View移回原位置以后,原位置的单击事件继续生效。从3.0开始,属性动画的单击事件触发位置为移动后的位置,但是View动画仍然在原位置。 -
硬件加速
建议开启,提高动画流程性