Android 动画知识总结
动画
概述
Android中的动画可用分为 三 类: View 动画、帧动画、属性动画。
View动画 包括平移、缩放、旋转、透明度。支持自定义View滑动。
帧动画 通过播放一系列图像从而产生动画的效果。如果图片过大,很容易发生 oom 。
属性动画 通过改变View的属性而达到动画的效果(API 11 的新特性 3.0)
View动画
View动画分为四类:
- TranslateAnimation(位移动画)
- ScaleAnimation(缩放动画)
- RotateAnimation(旋转动画)
- AlphaAnimation(透明动画)
这四种动画 既可以通过xml 的形式定义,也可以通过 代码来动态的创建。
xml 的形式定义 需要在 res文件夹下新建 anim文件夹,然后创建xml文件。 即 res/anim/filename.xml
下面是四种动画的定义格式 以及 set 动画集。
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration=" "
android:shareInterpolator="[true | false]">
<translate
android:fromXDelta="float"
android:toXDelta="float"
android:fromYDelta="float"
android:toYDelta="float"
/>
<scale
android:fromXScale="float"
android:toXScale="float"
android:fromYScale="float"
android:toYScale="float"
android:pivotX="float"
android:pivotY="float"
/>
<rotate
android:fromDegrees="float"
android:toDegrees="float"
android:pivotX="float"
android:pivotY="float"
/>
<alpha android:fromAlpha="float"
android:toAlpha="float"
/>
</set>
android:interpolator 表示动画集合所使用的插值器。
android:shareInterpolator 表示是否共享一个插值器。如果集合不指定插值器,那么子动画就需要单独指定所需的插值器或 使用默认值。
平移 ——> TranslateAnimation类。使一个View 在水平和竖直方向上完成平移的动画效果。属性含义:
- android:fromXDelta=”float” 表示x 的起始值,比如0;
- android:toXDelta=”float” 表示 y 的起始值,比如100;
- android:fromYDelta=”float” 表示 y 的 其实值;
- android:toYDelta=”float” 表示 y 的结束值
缩放动画 ——> ScaleAnimation。使一个View 具有放大或者缩小的动画效果。 属性含义:
- android:fromXScale=”float” 表示水平方向缩放的起始值,比如 0.5;
- android:toXScale=”float” 表示水平方向缩放的结束值,比如 1.2;
- android:fromYScale=”float” 表示竖直方向缩放的起始值;
- android:toYScale=”float” 表示竖直方向缩放的结束值;
- android:pivotX=”float” 表示缩放轴点的x坐标。会影响缩放效果
- android:pivotY=”float” 表示缩放轴点的y坐标. 会影响缩放效果
默认是 View的中心点。
旋转动画 ——> RotateAnimation 使一个View 具有旋转的动画效果。属性含义:
- android:fromDegrees=”float” 旋转的开始的角度,比如 0
- android:toDegrees=”float” 旋转的结束角度,比如 180
- android:pivotX=”float” 旋转的轴点的x坐标
- android:pivotY=”float” 旋转的轴点的y坐标
旋转的轴点会影响到旋转的具体效果。轴点即 旋转轴,View围绕这旋转轴旋转。
透明度动画 ——> AlphaAnimation 改变View的透明度。 属性含义:
- android:fromAlpha=”float” 透明度的起始值,比如 0.1
- android:toAlpha=”float” 透明度的结束值,比如 1.
android:duration=” “ 表动画的时长。
android:fillAfter=”true” 动画执行完毕是否保持。
代码中如何加载呢??
通过AnimationUtils.loadAnimation(this,R.anim.xxx)
来加载。并调用控件的startAnimation来开启一个动画。
除了使用Xml 的形式也可以通过 代码去动态的创建一个动画。 传递的参数和属性一样。不再赘述。
动画的相关Listener回调
public static interface AnimationListener {
void onAnimationStart(Animation animation);
void onAnimationEnd(Animation animation);
void onAnimationRepeat(Animation animation);
}
自定义View动画
需要继承自 Animation 这个抽象类。 重写它的 initialize 和 applyTransformation 方法。
在 applyTransformation
中进行相应的矩阵变换。很多时候采用 Camera 来简化矩阵的变换过程。
可以参看 ApiDemo 中的动画实现。 比如 Rotate3dAnimation.
帧动画
帧动画 —— 顺序播放一组预先定义好的图片。类似电影播放。 AnimationDrawable. 在res/drawable 下创建 xx.xml 文件
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="xx" android:duration="200"/>
<item android:drawable="xx" android:duration="200"/>
<item android:drawable="xx" android:duration="200"/>
<item android:drawable="xx" android:duration="200"/>
</animation-list>
将Drawable 作为 View 的背景并通过 Drawable 来播放动画即可。
imageView.setBackgroundResource(R.drawable.anim_drawable);
AnimationDrawable drawable = (AnimationDrawable) imageView.getBackground();
drawable.start();
帧动画的使用比较简单,但是比较容易引起 OOM。 所以在使用帧动画时尽量表面使用过多尺寸较大的图片。
View动画的特殊使用场景。
1.LayoutAnimation
这个动画作用于 ViewGroup。 为ViewGroup 指定一个动画, 这样子控件出场时就会有 动画效果。
LayoutAnimation 也是一种View动画。
步骤:
1.定义LayoutAnimation 在 anim/ 目录下。
<?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/xx.xml"
/>
delay 属性表示子元素的开始动画的时间延迟。比如子元素的入场动画时间为300ms,那么 0.5表示每个子元素都需要延迟 150ms 才能播放入场动画。 第一个延迟150ms 第二个 延迟300ms。
animationOrder 动画播放的顺序, normal 、reverse、 random。
animation 指定子元素的具体入场动画
2.为子元素指定具体的入场动画。
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration=" "
android:shareInterpolator="[true | false]">
<translate
android:fromXDelta="float"
android:toXDelta="float"
android:fromYDelta="float"
android:toYDelta="float"
/>
<scale
android:fromXScale="float"
android:toXScale="float"
android:fromYScale="float"
android:toYScale="float"
android:pivotX="float"
android:pivotY="float"
/>
</set>
3.为 viewGroup 指定 android:layoutAnimation 属性。
除了在 xml 中指定 LayoutAnimation。 还可以通过 LayoutAnimationController 来实现。
ListView listView = (ListView) layout.findViewById(R.id.list);
Animation animation = AnimationUtils.loadAnimation(this, R.anim.anim_item);
LayoutAnimationController controller = new LayoutAnimationController(animation);
controller.setDelay(0.5f);
controller.setOrder(LayoutAnimationController.ORDER_NORMAL);
listView.setLayoutAnimation(controller);
2.Activity的切换效果
通过overridePendingTransition(enterAnim,exitAnim) 必须在 startActivity 和 finish 之后调用。
属性动画
Api 11 退出的,Android3.0。
属性动画可以对任何对象做动画,也可以没有对象。属性动画有 ValueAnimator、ObjectAnimator、AnimatorSet等概念。
属性动画的默认时间是300ms,默认帧率是 10ms/帧。 属性动画的效果是: 在一个时间间隔内完成对象从一个属性值到另一个属性值的改变。
如果需要做动画的兼容,可以使用nineoldandroids类库。 但是该动画库,在android3.0及以上的系统是使用属性动画,但在3.0以下任然是View动画。
常用的几个动画类: ValueAnimator
、 ObjectAnimator
和 AnimatorSet
.
ObjectAnimator
继承自 ValueAnimator
。 AnimatorSet 是动画集合,可以定义一组动画。
1.如下 该控件在 5s 内 沿着 y 轴方向移动 500的距离。
ObjectAnimator.ofFloat(mButton3,"translationY",500).setDuration(5000).start();
2.在5s内颜色从 GREEN 到 RED 的变换。
ObjectAnimator.ofInt(mButton3,"backgroundColor",Color.GREEN,Color.RED).setDuration(5000).start();
3.动画集合,对控件的一系列操作操作。
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(
ObjectAnimator.ofFloat(mButton3, "translationY", 500),
ObjectAnimator.ofInt(mButton3, "backgroundColor", Color.GREEN, Color.RED),
ObjectAnimator.ofFloat(mButton3, "rotationX", 0, 360),
ObjectAnimator.ofFloat(mButton3, "rotationY", 0, 180)
);
animatorSet.setDuration(5000).start();
属性动画除了通过代码实现外, 还可以通过 XML 来定义。
属性动画的定义需要在 res/animator 目录下。
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:ordering="sequentially | together"
>
<objectAnimator
android:propertyName="string"
android:duration="int"
android:valueFrom="float|int|color"
android:valueTo="float|int|color"
android:startOffset="int"
android:repeatCount="int"
android:repeatMode="reverse | reverse"
android:valueType="floatType | intType"
/>
<animator
android:duration="int"
android:valueFrom="float|int|color"
android:valueTo="float|int|color"
android:startOffset="int"
android:repeatCount="int"
android:repeatMode="reverse | reverse"
android:valueType="floatType | intType"
/>
</set>
Xml 定义了 ValueAnimator、ObjectAnimator 以及 AnimatorSet. 标签对应 AnimatorSet。 标签对应 ValueAnimator, 对应 ObjectAnimator.
标签的 ordering属性有两个值: together 和 sequentially。 together 表示动画一起同时播放。
sequentially 表示按照前后顺序播放。默认是 together。
对于repeatCount 属性表示动画循环的次数, 默认值为0, 其中 -1 表示无限循环
repeatMode 表示动画循环的模式, 有两个选项: repeat 和 reverse, 表示连续重复还是逆向重复 连续重复 即每次都是从头开始。 逆向重复 表示第一次播放完以后,第二次会倒着播放,第三次再重头开始…
代码中如何加载动画呢??? 如下:
Animator animator = AnimatorInflater.loadAnimator(context, R.anim.xxx);
animator.setTarget(imageView);
animator.start();
在实际的开发中建议采用 代码动态实现属性动画, 实现比较简单 而且要灵活一些。 很多时候一个属性的起始值是无法提前确定的等等。
插值器 和 估值器
TimeInterpolator
是插值器的超类, 翻译过来是 时间插值器。 它的作用是根据时间流逝的百分比来计算出当前属性值改变的百分比。系统预置的有 LinearInterpolator(线性插值器: 匀速动画)、AccelerateDecelerateInterpolator
(加减速插值器,开始和结束慢,中间快), DecelerateInterpolator
(减速插值器, 动画越来越慢。)
TypeEvaluator
中文翻译为类型估值算法。 也称作估值器. 它的作用是根据当前属性改变的百分比来计算改变后的属性值。
public class IntEvaluator implements TypeEvaluator<Integer> {
public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
int startInt = startValue;
return (int)(startInt + fraction * (endValue - startInt));
}
}
evaluate 的三个参数分别表示 估值小数、 开始值 和 结束值。
可以自定义插值器和 估值器。
自定义插值器需要实现 Interpolator 或者 TimeInterpolator.
自定义估值算法需要实现 TypeEvaluator. 如果对其他类型(非 int、float、Color)做动画,那么必要实现自定义类型估值算法。
属性动画的监听器
有两个接口:
- AnimatorUpdateListener
- AnimatorListener
AnimatorListener的定义如下,包括开始、结束、取消、以及重复播放:
public static interface AnimatorListener {
void onAnimationStart(Animator animation);
void onAnimationEnd(Animator animation);
void onAnimationCancel(Animator animation);
void onAnimationRepeat(Animator animation);
}
为了方便开发系统还提供了 AnimatorListenerAdapter 这个类。它是 AnimatorListener 的适配器类。实现了 AnimatorListener,并做了空实现,这样我们就可以选择性的实现上面的方法了。
AnimatorUpdateListener 的定义如下:
public static interface AnimatorUpdateListener {
void onAnimationUpdate(ValueAnimator animation);
}
AnimatorUpdateListener 会监听整个动画的过程,动画是由许多帧组成的,每播放一帧, onAnimationUpdate 就会被调用一次。
对任意属性做动画
比如 要对 object 的属性 abc 做动画, 如果想让动画生效,必须满足两个条件:
1.object 必须要提供setAbc 方法, 如果动画没有传递初始值, 那么还需要提供 getAbc方法, 因为系统要去取 abc属性的初始值(如果这条不满足,程序直接Crash)
2.object的 setAbc 对属性abc 所做的改变必须能够通过某种方法反应出来。比如会带来 ui 之类的改变(如果不满足,动画无效果,不会crash)
比如对 Button 的 setWidth 做动画, 则没有效果。 这是因为虽然内部提供了 setWidth 和 getWidth ,但并不是改变视图的大小。
解决方案:
1.给你的对象加上 get 和 set 方法,如果你有权限的话。
2.用一个类来包装原始对象,间接为期提供 get 和 set 方法。
3.采用ValueAnimator 监听动画过程,自己实现属性的改变。
属性动画的工作原理
ok, 我们从 ObjectAnimator 的 start() 方法开始。
@Override
public void start() {
// See if any of the current active/pending animators need to be canceled
AnimationHandler handler = sAnimationHandler.get();
if (handler != null) {
int numAnims = handler.mAnimations.size();
for (int i = numAnims - 1; i >= 0; i--) {
if (handler.mAnimations.get(i) instanceof ObjectAnimator) {
ObjectAnimator anim = (ObjectAnimator) handler.mAnimations.get(i);
if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
anim.cancel();
}
}
}
numAnims = handler.mPendingAnimations.size();
for (int i = numAnims - 1; i >= 0; i--) {
if (handler.mPendingAnimations.get(i) instanceof ObjectAnimator) {
ObjectAnimator anim = (ObjectAnimator) handler.mPendingAnimations.get(i);
if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
anim.cancel();
}
}
}
numAnims = handler.mDelayedAnims.size();
for (int i = numAnims - 1; i >= 0; i--) {
if (handler.mDelayedAnims.get(i) instanceof ObjectAnimator) {
ObjectAnimator anim = (ObjectAnimator) handler.mDelayedAnims.get(i);
if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
anim.cancel();
}
}
}
}
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();
}
首先判断当前动画、等待的动画、和延迟的动画 中有和当前动画相同的动画,那么就把相同的动画给取消掉。 接着就调用了父类的 super.start() 方法。 即 ValueAnimator 的 start 方法。
private void start(boolean playBackwards) {
if (Looper.myLooper() == null) {
throw new AndroidRuntimeException("Animators may only be run on Looper threads");
}
mPlayingBackwards = playBackwards;
mCurrentIteration = 0;
mPlayingState = STOPPED;
mStarted = true;
mStartedDelay = false;
mPaused = false;
updateScaledDuration(); // in case the scale factor has changed since creation time
AnimationHandler animationHandler = getOrCreateAnimationHandler();
animationHandler.mPendingAnimations.add(this);
if (mStartDelay == 0) {
// This sets the initial value of the animation, prior to actually starting it running
setCurrentPlayTime(0);
mPlayingState = STOPPED;
mRunning = true;
notifyStartListeners();
}
animationHandler.start();
}
首先校验了Looper。 说明属性动画 需要运行在有 Looper 的线程中。 上述代码最后会调用 AnimationHandler 的start 方法。 这个 AnimationHandler 并不是 Handler 实质上是一个 Runnable.
protected static class AnimationHandler implements Runnable {}
在 AnimationHandler 的 start() 方法中 进行了一系列的调用, 调到了 native 层。 Jni 层最终还是要调回来的。 它的 run 会被调用, 这个Runnable 涉及和底层的交互。 我们直接忽略这部分。 c 层 会调用这个的 run 方法,如下所示:
// Called by the Choreographer.
@Override
public void run() {
mAnimationScheduled = false;
doAnimationFrame(mChoreographer.getFrameTime());
}
直接看重点, ValueAnimator 的 doAnimationFrame 方法:
/**
* Processes a frame of the animation, adjusting the start time if needed.
*
* @param frameTime The frame time.
* @return true if the animation has ended.
*/
final boolean doAnimationFrame(long frameTime) {
if (mPlayingState == STOPPED) {
mPlayingState = RUNNING;
if (mSeekTime < 0) {
mStartTime = frameTime;
} else {
mStartTime = frameTime - mSeekTime;
// Now that we're playing, reset the seek time
mSeekTime = -1;
}
}
if (mPaused) {
if (mPauseTime < 0) {
mPauseTime = frameTime;
}
return false;
} else if (mResumed) {
mResumed = false;
if (mPauseTime > 0) {
// Offset by the duration that the animation was paused
mStartTime += (frameTime - mPauseTime);
}
}
// 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);
return animationFrame(currentTime);
}
上述代码的末尾调用了 animationFrame 方法, 而 animationFrame 内部调用了 animateValue 下面看 animateValue的代码:
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);
}
}
}
calculateValue 会计算每帧动画所对应的属性值。
下面着重看一下到底在哪里调用 属性的 get 和 set 方法。 如果没有设置初始值 则get方法将会被调用。
我们来看 PropertyValuesHolder 的 setupValue方法。
private void setupValue(Object target, Keyframe kf) {
if (mProperty != null) {
Object value = convertBack(mProperty.get(target));
kf.setValue(value);
}
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 方法会将新的属性值 设置给对象。通过 反射来进行设置的。
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());
}
}
}
使用动画的注意事项
1.OOM 问题
主要出现在帧动画中,图片数量多时且图片较大 就容易出现 OOM 需要注意。避免使用 帧动画。
2.内存泄漏
属性动画中 无限的循环动画,在Activity退出时要及时停止。否则导致 Activity 无法释放。
3.兼容性问题
在3.0一下系统的兼容性问题。
4.View动画问题
View动画是对 View 的影像动画。 并不是真正地改变View的状态。因此有时候会出现 动画完成后 View 无法隐藏的现象, 即 setVisibility 失效了。 只需要调用 clearAnimation 清除 View动画即可。
5.不要使用px
要尽量使用 dp 。使用 Px 会导致在不同的设备上有不同的效果。