Android 动画分类
作用对象是 View,支持四种动画效果:平移动画、缩放动画、旋转动画、透明度动画。
帧动画也属于 View 动画,但是它的表现形式和 View 动画四种变换效果不太一样。
四种变换效果对应着 Animation 的四个子类:TranslateAnimation、ScaleAnimation、RotateAnimation、AlphaAnimation。
这四种动画可以用 XML 定义,也可以用代码动态创建。(View 动画用 XML 定义比较好,可读性好)
创建动画 XML 文件在文件夹 res/anim/filename.xml
,语法如下所示:
View 动画既可以是单个动画,也可以由一系列动画组成
表示动画集合,对应 AnimationSet 类。可以包含若干动画,也可以在嵌套其他动画集合。
默认为 @android:anim/accelerate_decelerate_interpolator
,即加速减速插值器。
表示平移动画,对应 TranslateAnimation 类,可以使一个 View 在水平和竖直的方向完成平移的动画效果。
表示缩放动画,对应 ScaleAnimation,可以使 View 具有放大或缩小的动画效果。
默认情况下轴点坐标是 View 的
左上角(0,0),这时候进行水平缩放,就会在 轴点的右侧进行缩放;如果设置轴点在 View 的右边界,那缩放就会在轴点的左侧进行;如果设置轴点在 View 的中心点,那缩放就会在轴点的左右进行
表示旋转动画,对应于 RotateAnimation 类,使 View 具有旋转的动画效果
轴点,会影响到旋转效果,同样是旋转 90 度,延中心点旋转和延左上角坐标旋转明显不一样
表示透明度动画,对应 AlphaAnimation 类,它可以改变 View 的透明度。
View 动画还有一些常用属性
Animation 的 setAnimationListener 方法可以给 View 动画添加过程监听:
实际开发中很少用到自定义 View 动画
继承 Animation 类,重写 initialize 和 applyTransformation
方法。
initialize 方法做一些初始化的工作
applyTransformation 方法进行相应的矩阵变换
Rotate3dAnimation 示例,类似 3d 效果,围绕 y 轴旋转并沿着 z 轴平移:
是顺序播放一组预先定义好的图片,类似电影播放。
系统提供 AnimationDrawable 来使用帧动画。
使用:
1. XML 定义一个 AnimationDrawable
1. 设置为 View 背景并通过 AnimationDrawable 来播放
帧动画比较简单,但是容易引起 OOM。
尽量避免使用过多较大尺寸的图片
作用于 ViewGroup,为 ViewGroup 指定一个动画,这样当它的子元素出场时就会有这种动画效果。
LayoutAnimation 也是一个 View 动画,为了给 ViewGroup 的子元素加上出场效果,遵循如下几个步骤:
android:delay
子元素开始动画的时间延迟。
例:子元素入场动画时间设定 300ms,那么 0.5 表示都要延迟 150ms * n 才能播放入场动画。n 表示第 n 个要显示的子 View,不一定是按顺序的第几个,可能是随机的,这取决于 android:animationOrder
第一个子元素延迟 150ms,第二个子元素延迟 300ms 开始播放入场动画,依次类推。
android:animationOrder
表示子元素动画的顺序。
android:animation——为子元素制定具体的入场动画
为 ViewGroup 指定 android:layoutAnimation
属性。对于 ListView 或 RecyclerView(GridLayoutManager 需要特殊处理)来说,这样他们的 item 就有出场动画了,这种方式适用于所有 ViewGroup:
还可以通过 LayoutAnimationController 在代码里边实现:
拓展:为 RecyclerView 的 item 设置入场动画:RecyclerView——LayoutAnimation
Activity 有默认的切换效果,我们也可以自定义这个效果。用到 overridePendingTransition(int enterAnim, int exitAnim)
这个方法,它必须在 startActivity(Intent)
或者 finish()
后被调用才能生效,它的参数含义:
当启动一个 Activity 时,给将要启动的 Activity
添加切换效果:
Fragment 也可以添加切换动画。通过 FragmentTransaction 中的 setCustomAnimations() 方法来添加切换动画。最好是用 View 动画
API 11 加入的新特性,和 View 动画不同,它对作用对象进行了扩展,属性动画可以对任何对象做动画,甚至可以没有对象。效果也得到了加强,不再像 View 动画只支持四种简单的变换。
属性动画有 ValueAnimator、ObjectAnimator、AnimatorSet 等概念。
动画默认时间间隔 300ms,默认帧率 10ms/帧。
可以达到的效果:在一个时间间隔内完成对象从一个属性值到另一个属性值的改变。
属性动画几乎是无所不能的,只要对象有这个属性,他都能实现动画效果。
兼容性(现在基本都 API 17 起步了,基本没啥兼容性问题,用不到 nineoldandroids)
使用示例:
1. 改变一个对象(myObject)的 translationY 属性,让其沿着 Y 轴向上平移一段距离:它的高度。该动画在默认时间内完成。
属性动画也可以用 XML 来实现。 目录 res/animator/
如下所示:
标签对应 AnimatorSet
标签对应 ValueAnimator
标签对应 ObjectAnimator
android:propertyName
所指定的属性的类型,有 “intType” 和 “floatType” 两个可选项,分别表示属性的类型为整型和浮点型。如果 android:propertyName
制定的属性表示颜色,那么不需要指定 android:valueType
,系统会自己处理 示例,使用 XML 定义属性动画并作用在 View 上:
实际开发中建议用代码实现属性动画,用代码实现比较简单。而且很多时候属性的起始值是无法提前确定的,比如屏幕宽高
TimeInterpolator
根据时间流逝的百分比来计算出当前
属性值改变的百分比
TypeEvaluator
根据当前
属性值改变的百分比
来
计算改变后的属性值
属性动画中的估值器和插值器共同作用才计算出非匀速动画所需要的值,所以它俩很重要。
看 ValueAnimator 的源码:可以知道它先用 Interpolator 计算出属性改变的百分比,然后再用 Evaluator 计算出具体的属性改变值
示例,匀速动画采用线性插值器和整型估值算法,在 40ms 内,View 的 x 属性实现从 0 到 40 的变换,如下图:
动画默认刷新率 10ms/帧,所以该动画将分 5 帧进行。分析第 3 帧(x = 20,t = 20ms),当时间是 20ms,时间流逝百分比是 0.5。由于是线性插值器,所以 x 改变的百分比是 0.5 。由于是整型估值算法,所以最终改变值是 0+0.5*(40-0)=20
下面看一下线性插值器和整型估值算法的源码:
属性动画要求对象的属性有 set 方法和 get 方法(可选)。
插值器和估值器也可以自定义,然后就可以实现千奇百怪的效果了。
自定义插值器需要实现 Interpolator 或 TimeInterpolator;自定义估值算法需要实现 TypeEvaluator。
AnimatorListener
它可以监听动画的开始、结束、取消、重复播放。
为了开发方便,系统还提供了 AnimatorListenerAdapter
类,他是 AnimatorListener 的适配器类,可以有选择的实现上边的部分方法,不用全部都实现
AnimatorUpdateListener
public static interface AnimatorUpdateListener {
void onAnimationUpdate(ValueAnimator animation);
}
它监听整个动画过程,每播放一帧,它就会调用一次。利用这个特性可以做一些特殊的事情
对 object 的属性 abc 做动画,想让动画生效,要同时满足以下条件:
1. object 必须提供 setAbc 方法。如果动画的时候没有传递初始值,那么还需要 getAbc 方法,因为系统要去取初始值,如果没有 get 方法,直接 crash
1. object 的 setAbc 对属性 abc 所做的改变必须能够通过某种方式反映出来。比如 ui 的改变(如果这条不满足,动画无效果但不会 crash)
如果不满足上述两个条件,可以这样做:
采用 ValueAnimator,监听动画过程,自己实现属性的改变
属性动画要求动画作用的对象提供该属性的 set 方法,属性动画根据你传递的该属性的初始值和最终值,以动画的效果多次去调用 set 方法。每次传递给 set 方法的值都不一样,确切来说是随着时间的推移,所传递的值越来越接近最终值。如果动画的时候没有传递初始值,那么还要提供 get 方法,因为系统要去获取属性的初始值。
分析一下 ObjectAmnimator 的源码
ObjectAnimator.ofInt(mBotton, "width", 500).setDuration(5000).start()
@Override
public void start() {
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));
}
}
super.start();
}
上边代码做的工作就是:判断如果这个动画之前存在并且需要取消之前的,就取消掉;然后调用父类的 start() 方法。(父类是 ValueAnimator)
再看 ValueAnimator 的 start() 方法,它里边调用了自身的 start(boolean playBackwards) 方法:
/**
* Start the animation playing. This version of start() takes a boolean flag that indicates
* whether the animation should play in reverse. The flag is usually false, but may be set
* to true if called from the reverse() method.
*
* The animation started by calling this method will be run on the thread that called
* this method. This thread should have a Looper on it (a runtime exception will be thrown if
* this is not the case). Also, if the animation will animate
* properties of objects in the view hierarchy, then the calling thread should be the UI
* thread for that view hierarchy.
*
* @param playBackwards Whether the ValueAnimator should start playing in reverse.
*/
private void start(boolean playBackwards) {
if (Looper.myLooper() == null) {
throw new AndroidRuntimeException("Animators may only be run on Looper threads");
}
mReversing = playBackwards;
mSelfPulse = !mSuppressSelfPulseRequested;
// 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 = -1;
mFirstFrameTime = -1;
mStartTime = -1;
//调用 addAnimationCallback 方法创建并实现 AnimationHandler 里的 AnimationFrameCallback 接口,同时会把 callback 注册到 AnimationHandler 里,之后就可以接收到动画每一帧的回调了
addAnimationCallback(0);
if (mStartDelay == 0 || mSeekFraction >= 0 || mReversing) {
// 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);
}
}
}
private void addAnimationCallback(long delay) {
if (!mSelfPulse) {
return;
}
getAnimationHandler().addAnimationFrameCallback(this, delay);
}
从 AnimationHandler 的 addAnimationFrameCallback 一直分析调用链,很快就到了 JNI 层,从底层回来后,最终会调用到 ValueAnimator#doAnimationFrame(long frameTime) -> animateBasedOnTime(long currentTime):
/**
* This internal function processes a single animation frame for a given animation. The
* currentTime parameter is the timing pulse sent by the handler, used to calculate the
* elapsed duration, and therefore
* the elapsed fraction, of the animation. The return value indicates whether the animation
* should be ended (which happens when the elapsed time of the animation exceeds the
* animation's duration, including the repeatCount).
*
* @param currentTime The current time, as tracked by the static timing handler
* @return true if the animation's duration, including any repetitions due to
* repeatCount
has been exceeded and the animation should be ended.
*/
boolean animateBasedOnTime(long currentTime) {
boolean done = false;
if (mRunning) {
final long scaledDuration = getScaledDuration();
final float fraction = scaledDuration > 0 ?
(float)(currentTime - mStartTime) / scaledDuration : 1f;
final float lastFraction = mOverallFraction;
final boolean newIteration = (int) fraction > (int) lastFraction;
final boolean lastIterationFinished = (fraction >= mRepeatCount + 1) &&
(mRepeatCount != INFINITE);
if (scaledDuration == 0) {
// 0 duration animator, ignore the repeat count and skip to the end
done = true;
} else if (newIteration && !lastIterationFinished) {
// Time to repeat
if (mListeners != null) {
int numListeners = mListeners.size();
for (int i = 0; i < numListeners; ++i) {
mListeners.get(i).onAnimationRepeat(this);
}
}
} else if (lastIterationFinished) {
done = true;
}
mOverallFraction = clampFraction(fraction);
float currentIterationFraction = getCurrentIterationFraction(
mOverallFraction, mReversing);
animateValue(currentIterationFraction);
}
return done;
}
上边代码调用到了 void animateValue(float fraction)
,现在再回到 ObjectAnimator 源码中去,它重载了 ValueAnimator 的 animateValue 方法:
//ObjectAnimator#animateValue(float fraction)
/**
* This method is called with the elapsed fraction of the animation during every
* animation frame. This function turns the elapsed fraction into an interpolated fraction
* and then into an animated value (from the evaluator. The function is called mostly during
* animation updates, but it is also called when the end()
* function is called, to set the final value on the property.
*
* Overrides of this method must call the superclass to perform the calculation
* of the animated value.
*
* @param fraction The elapsed fraction of the animation.
*/
@CallSuper
@Override
void animateValue(float fraction) {
final Object target = getTarget();
if (mTarget != null && target == null) {
// We lost the target reference, cancel and clean up. Note: we allow null target if the
/// target has never been set.
cancel();
return;
}
super.animateValue(fraction);
int numValues = mValues.length;
for (int i = 0; i < numValues; ++i) {
mValues[i].setAnimatedValue(target);
}
}
上边代码,它先调用父类 animateValue 方法,然后再 调用 PropertyValuesHolder#setAnimatedValue 去设置属性的值:
//PropertyValuesHolder#setAnimatedValue
/**
* Internal function to set the value on the target object, using the setter set up
* earlier on this PropertyValuesHolder object. This function is called by ObjectAnimator
* to handle turning the value calculated by ValueAnimator into a value set on the object
* according to the name of the property.
* @param target The target object on which the value is set
*/
void setAnimatedValue(Object target) {
if (mProperty != null) {
mProperty.set(target, getAnimatedValue());
}
if (mSetter != null) {
try {
mTmpValueArray[0] = getAnimatedValue();
//反射调用 set 方法去设置值
mSetter.invoke(target, mTmpValueArray);
} catch (InvocationTargetException e) {
Log.e("PropertyValuesHolder", e.toString());
} catch (IllegalAccessException e) {
Log.e("PropertyValuesHolder", e.toString());
}
}
}
知道了动画更新属性值的流程,现在再找找动画调用 get 方法拿初始值的流程:
上边分析 ObjectAnimator 的 start 方法时的调用链 ObjectAnimator#start() -> ValueAnimator#start() -> ValueAnimator#start(false)
在 ValueAnimator#start(false) 方法里也调用了 startAnimation
方法:
ValueAnimator#startAnimation()
/**
* Called internally to start an animation by adding it to the active animations list. Must be
* called on the UI thread.
*/
private void startAnimation() {
if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, getNameForTrace(),
System.identityHashCode(this));
}
mAnimationEndRequested = false;
initAnimation();
mRunning = true;
if (mSeekFraction >= 0) {
mOverallFraction = mSeekFraction;
} else {
mOverallFraction = 0f;
}
if (mListeners != null) {
notifyStartListeners();
}
}
里边调用了 initAnimation()
方法,而 ObjectAnimator 重写了该方法
ObjectAnimator#initAnimation()
/**
* This function is called immediately before processing the first animation
* frame of an animation. If there is a nonzero startDelay
, the
* function is called after that delay ends.
* It takes care of the final initialization steps for the
* animation. This includes setting mEvaluator, if the user has not yet
* set it up, and the setter/getter methods, if the user did not supply
* them.
*
* Overriders of this method should call the superclass method to cause
* internal mechanisms to be set up correctly.
*/
@CallSuper
@Override
void initAnimation() {
if (!mInitialized) {
// mValueType may change due to setter/getter setup; do this before calling super.init(),
// which uses mValueType to set up the default type evaluator.
final Object target = getTarget();
if (target != null) {
final int numValues = mValues.length;
for (int i = 0; i < numValues; ++i) {
//调用 PropertyValuesHolder#setupSetterAndGetter(target) 来设置初始值
mValues[i].setupSetterAndGetter(target);
}
}
super.initAnimation();
}
}
调用 PropertyValuesHolder#setupSetterAndGetter(target) 来设置初始值
PropertyValuesHolder#setupSetterAndGetter(target)
/**
* Internal function (called from ObjectAnimator) to set up the setter and getter
* prior to running the animation. If the setter has not been manually set for this
* object, it will be derived automatically given the property name, target object, and
* types of values supplied. If no getter has been set, it will be supplied iff any of the
* supplied values was null. If there is a null value, then the getter (supplied or derived)
* will be called to set those null values to the current value of the property
* on the target object.
* @param target The object on which the setter (and possibly getter) exist.
*/
void setupSetterAndGetter(Object target) {
...
for (int i = 0; i < keyframeCount; i++) {
Keyframe kf = keyframes.get(i);
if (!kf.hasValue() || kf.valueWasSetOnStart()) {
...
try {
Object value = convertBack(mGetter.invoke(target));
kf.setValue(value);
kf.setValueWasSetOnStart(true);
} catch (InvocationTargetException e) {
Log.e("PropertyValuesHolder", e.toString());
} catch (IllegalAccessException e) {
Log.e("PropertyValuesHolder", e.toString());
}
}
}
...
}
里边也是用反射 mGetter.invoke(target)
去调用的 get 方法拿到初始值
OOM 问题
主要出现在帧动画中,图片过多过大时极易出现。尽量避免使用帧动画
内存泄漏
属性动画做的无限循环动画,需要在 Activity 退出时即时停止,否则将导致 Activity 无法释放造成内存泄漏。通过验证 View 动画不存在此问题
兼容性问题
在 android 3.0 以下的系统有兼容性问题。
View 动画的问题
View 动画是对 View 的影像做动画,并没有真正改变 View 的状态,因此有时会出现动画完成后 View 无法隐藏的现象,即 setVisibility(View.GONE) 失效,此时只要调用 view.clearAnimation() 清除动画就能解决此问题
不要使用 px
进行动画的过程中尽量使用 dp,使用 px 会导致不同设备上有不同的效果
动画元素的交互
将 view 移动后,在 android 3.0 以前的系统上,不管是 View 动画还是属性动画,新位置均无法触发点击事件,同时老位置仍然可以触发点击事件。尽管 View 已经在视觉上不存在了,将 View 移回原位置点击事件继续生效。从 android 3.0 开始,属性动画的单击事件触发位置为移动后的位置,但是 View 动画仍然在原位置。
硬件加速
使用动画过程中建议开启硬件加速,提高动画的流畅性