Android动画分三种:View动画、帧动画、属性动画
是的, check, √
View动画
View动画定义了渐变Alpha、旋转Rotate、缩放Scale、平移Translate四种基本动画,并且通过这四种基本动画的组合使用,可以实现多种交互效果。
View动画使用非常简单,不仅可以通过XML文件来定义动画,同样可以通过Java代码来实现动画过程。
原理:
首先view的绘制是 drawBackground() -> onDraw() -> dispatchDraw() -> onDrawForeground() 的顺序,
//android/view/View.java中的boolean draw(Canvas canvas, ViewGroup parent, long drawingTime)方法
View.setAnimation会将旋转、缩放、平移等动画存下来,动画启动后通过invalidate() ,每一帧中在draw的时候通过canvas.translate、canvas.scale、cavas.setLayerAlpha等方式,执行动画。
故而view动画只会影响view的视觉效果,而不影响起事件响应区域,因为只有draw中处理了,measure和layout都没动
Xml文件实现
通过xml来定义View动画涉及到一些公有的属性(在AndroidStudio上不能提示):
1 2 3 4 5 6 7
| android:duration 动画持续时间 android:fillAfter 为true动画结束时,View将保持动画结束时的状态 android:fillBefore 为true动画结束时,View将还原到开始开始时的状态 android:repeatCount 动画重复执行的次数 android:repeatMode 动画重复模式 ,重复播放时restart重头开始,reverse重复播放时倒叙回放,该属性需要和android:repeatCount一起使用 android:repeatCount 默认是0,-1是无限循环 android:interpolator 插值器,相当于变速器,改变动画的不同阶段的执行速度
|
这些属性是从Animation中继承下来的,在alpha
、rotate
、scale
、translate
标签中都可以直接使用。
利用xml文件定义View动画需要在工程的res目录下创建anim文件夹,所有的xml定义的View动画都要放在anim目录下。其中标签 translate、scale、alpha、rotate,就是对应四种动画。set标签是动画集合,对应AnimationSet类,有多个动画构成。
其中android:duration是指动画时间,fillAfter为true是动画结束后保持,false会回到初始状态。interpolator是指动画的执行速度,默认是先加速后减速。其他标签及属性较简单可自行研究验证。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| <?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android" android:duration="5000" android:fillAfter="true" android:interpolator="@android:anim/accelerate_decelerate_interpolator">
<translate android:duration="1000" android:fromXDelta="0" android:toXDelta="400" /> <scale android:duration="2000" android:fromXScale="0.5" android:fromYScale="0.5" android:toXScale="1" android:toYScale="1" /> <alpha android:duration="3000" android:fromAlpha="0.2" android:toAlpha="1" />
<rotate android:fromDegrees="0" android:toDegrees="90" /> </set>
|
定义好动画后,使用也很简单,调用view的startAnimation方法即可。
1 2 3
| Animation animation = AnimationUtils.loadAnimation(this, R.anim.animation_test); textView1.startAnimation(animation);
|
rotate
、scale
动画的android:pivotX
和android:pivotY
属性、translate
动画的android:toXDelta
和android:toYDelta
属性的取值都可以是都可以数值、百分数、百分数p
,比如:50
、50%
、50%p
,他们取值的代表的意义各不相同:
50
表示以View左上角为原点沿坐标轴正方向(x
轴向右,y
轴向下)偏移50px
的位置;
50%
表示以View左上角为原点沿坐标轴正方向(x
轴向右,y
轴向下)偏移View宽度或高度的50%处的位置;
50%p
表示以View左上角为原点沿坐标轴正方向(x
轴向右,y
轴向下)偏移父控件宽度或高度的50%处的位置(p
表示相对于ParentView
的位置)。
“50”: ;
“50%”;
“50%p”
代码动态实现
在平常的业务逻辑中也可以直接用Java代码来实现Veiw动画,Android系统给我们提供了AlphaAnimation
、RotateAnimation
、ScaleAnimation
、TranslateAnimation
四个动画类分别来实现View的渐变、旋转、缩放、平移动画。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| //view动画使用,方式二:new 动画对象 AnimationSet animationSet = new AnimationSet(false); animationSet.setDuration(3000); animationSet.addAnimation(new TranslateAnimation(0, 100, 0, 0)); animationSet.addAnimation(new ScaleAnimation(0.1f, 1f, 0.1f, 1f)); animationSet.setFillAfter(true); textView2.startAnimation(animationSet);
//view动画使用,方式二:new 动画对象,使用setAnimation AnimationSet animationSet2 = new AnimationSet(false); animationSet2.setDuration(3000); animationSet2.addAnimation(new TranslateAnimation(0, 100, 0, 0)); animationSet2.addAnimation(new ScaleAnimation(0.1f, 1f, 0.1f, 1f)); animationSet2.setFillAfter(true); animationSet2.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) {
} @Override public void onAnimationEnd(Animation animation) { MyToast.showMsg(AnimationTestActivity.this, "View动画:代码 set:View动画结束~"); } @Override public void onAnimationRepeat(Animation animation) {
} }); textView3.setAnimation(animationSet2);
|
注意点:
- startAnimation方法是立刻播放动画;setAnimation是设置要播放的下一个动画。
- setAnimationListener可以监听动画的开始、结束、重复。
自定义动画
[3D旋转动画]
Like:ProgressBarAnimation
1 2 3 4 5 6 7 8
| class ProgressBarAnimation(private val progressBar: ProgressBar, private val from: Int, private val to: Int) : Animation() { override fun applyTransformation(interpolatedTime: Float, t: Transformation?) { super.applyTransformation(interpolatedTime, t) val value = from + (to - from) * interpolatedTime progressBar.progress = value.toInt() } }
|
布局动画
LayoutTransition
使用LayoutAnimation给ViewGroup指定child的出场动画,方法如下:
1.先用xml定义标签LayoutAnimation:
- android:animation设置child的出场动画
- android:animationOrder设置child的出场顺序,normal就是顺序
- delay是指:每个child延迟(在android:animation中指定的动画时间)0.8倍后播放动画。如果android:animation中的动画时间是100ms,那么每个child都会延迟800ms后播放动画。 如果不设置delay,那么所有child同时执行动画。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <?xml version="1.0" encoding="utf-8"?> <layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android" android:animation="@anim/enter_from_left_for_child_of_group" android:animationOrder="normal" android:delay="0.8"> </layoutAnimation> R.anim.enter_from_left_for_child_of_group
<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android"> <translate android:duration="1000" android:fromXDelta="-100%p" android:toXDelta="0"/>
</set>
|
2.把LayoutAnimation设置给ViewGroup
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <LinearLayout android:id="@+id/ll_layout_animation" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:layoutAnimation="@anim/layout_animation"> <TextView android:layout_width="50dp" android:layout_height="wrap_content" android:textColor="#ff0000" android:text="呵呵呵"/> <TextView android:layout_width="60dp" android:layout_height="wrap_content" android:textColor="#ff0000" android:text="qq" android:background="@color/colorPrimary"/> <TextView android:layout_width="30dp" android:layout_height="wrap_content" android:textColor="#ff0000" android:text="啊啊"/> </LinearLayout>
|
除了xml,当然也可以使用LayoutAnimationController 指定:
1 2 3 4 5 6
| Animation enterAnim = AnimationUtils.loadAnimation(this, R.anim.enter_from_left_for_child_of_group); LayoutAnimationController controller = new LayoutAnimationController(enterAnim); controller.setDelay(0.8f); controller.setOrder(LayoutAnimationController.ORDER_NORMAL); llLayoutAnimation.setLayoutAnimation(controller);
|
animateLayoutChanges用处及原理
1
| android:animateLayoutChanges="true"
|
animateLayoutChanges的实际实现就是LayoutTransition,
android.view.ViewGroup.java
Dialog/Activity转场动画
Activity转场
overridePendingTransition
1 2 3 4 5 6 7 8 9
| class XXActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_checkout_rec) overridePendingTransition( R.anim.slide_in_down, R.anim.slide_in_down ) } }
|
1 2 3 4 5 6 7
| <?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" android:interpolator="@android:anim/decelerate_interpolator" > <translate android:duration="200" android:fromYDelta="100%p" android:toYDelta="0%p" /> </set>
|
1 2 3 4 5 6 7
| <?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" android:interpolator="@android:anim/decelerate_interpolator" > <translate android:duration="200" android:fromYDelta="0%p" android:toYDelta="100%p" /> </set>
|
Dialog转场
Window?.setWindowAnimatinos()
1 2 3 4 5 6 7 8 9
| class XXDialog(context: Context, val anim: PointBean.PointAnimation) :Dialog(context) { init { setContentView(R.layout.threshold_dialog) setCanceledOnTouchOutside(true) window?.setGravity(Gravity.CENTER) window?.setLayout(MATCH_PARENT, AndroidUtil.getScreenWidth(context)) window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)); window?.setWindowAnimations(R.style.XXDialogAnim) }
|
1 2 3 4
| <style name="XXDialogAnim" mce_bogus="1" parent="android:Animation"> <item name="android:windowEnterAnimation">@anim/cart_threshold_dialog_enter_anim</item> <item name="android:windowExitAnimation">@anim/cart_threshold_dialog_exit_anim</item> </style>
|
属性动画
属性动画本质上是 使用反射调用对象的setXX()、getXX()方法,根据插值器的值变化曲线修改对象属性,所以是视图实实在在的位置、尺寸等属性发生变化并会触发measure、layout,因此点击区域也就发生变化。
属性动画可对任意对象做动画,不仅仅是View。默认动画时间是300ms,10ms/帧。具体理解就是:可在给定的时间间隔内 实现 对象的某属性值 从 value1 到 value2的改变。
使用很简单,可以直接代码实现(推荐),也可xml实现,举例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| ObjectAnimator translationX = ObjectAnimator .ofFloat(textView6, "translationX", 0, 200) .setDuration(1000); translationX.setInterpolator(new LinearInterpolator()); setAnimatorListener(translationX);
Animator animatorUpAndDown = AnimatorInflater.loadAnimator(this, R.animator.animator_test); animatorUpAndDown.setTarget(textView6);
ObjectAnimator textColor = ObjectAnimator .ofInt(textView6, "textColor", 0xffff0000, 0xff00ffff) .setDuration(1000); textColor.setRepeatCount(ValueAnimator.INFINITE); textColor.setRepeatMode(ValueAnimator.REVERSE); textColor.setEvaluator(new ArgbEvaluator());
mAnimatorSet = new AnimatorSet(); mAnimatorSet .play(animatorUpAndDown) .with(textColor) .after(translationX);
mAnimatorSet.start();
private void setAnimatorListener(ObjectAnimator translationX) { translationX.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { } }); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { translationX.addPauseListener(new AnimatorListenerAdapter() { @Override public void onAnimationResume(Animator animation) { super.onAnimationResume(animation); } }); }
translationX.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); } }); }
|
R.animator.animator_test,是放在res/animator中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| <?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" android:ordering="sequentially">
<objectAnimator android:propertyName="translationY" android:duration="1000" android:valueFrom="0" android:valueTo="120" android:startOffset="0" android:repeatCount="0" android:repeatMode="reverse" android:valueType="floatType" android:interpolator="@android:interpolator/accelerate_decelerate" />
</set>
|
translationX是实现横移,animatorUpAndDown是实现竖移、textColor是实现文字颜色变化。其中animatorUpAndDown是使用xml定义,标签含义也很好理解。 最后使用AnimatorSet的play、with、after 实现 先横移,然后 竖移和颜色变化 同时的动画集合效果。
注意点:
- 关于View动画和属性动画的平移,属性动画改变属性值setTranslationX 的视图效果像view动画的平移一样,都是view实际的layout位置没变,只改变了视图位置;不同点是属性动画 给触摸点生效区域增加了位移(而view动画仅改变了视图位置)。
- 插值器:Interpolator,根据 时间流逝的百分比,计算当前属性值改变的百分比。 例如duration是1000,start后过了200,那么时间百分比是0.2,那么如果差值器是LinearInterpolator线性差值器,那么属性值改变的百分比也是0.2
- 估值器:Evaluator,就是根据 差值器获取的 属性值百分比,计算改变后的属性值。 ofInt、onFloat内部会自动设置IntEvaluator、FloatEvaluator。如果使用ofInt且是颜色相关的属性,就要设置ArgbEvaluator。 上面例子中 文字颜色变化动画 设置了ArgbEvaluator:textColor.setEvaluator(new ArgbEvaluator())。
- 动画监听:主要是两个监听接口,AnimatorUpdateListener、AnimatorListenerAdapter。AnimatorUpdateListener的回调方法在每帧更新时都会调用一次;AnimatorListenerAdapter可以监听开始、结束、暂停、继续、重复、取消,重写你要关注的方法即可。
对任意属性做动画
一个问题,针对下面的Button,如何实现 的宽度逐渐拉长的动画,即文字不变,仅拉长背景宽度?
1 2 3 4 5
| <Button android:id="@+id/button_animator_test" android:layout_width="180dp" android:layout_height="wrap_content" android:text="任意属性动画-宽度拉长"/>
|
首先,View动画的ScaleAnimation是无法实现的,因为view的scale是把view的视图放大,这样文字也会拉长变形。那么属性动画呢?试试~
1 2 3
| ObjectAnimator width1 = ObjectAnimator.ofInt(button, "width", 1000); width1.setDuration(2000); width1.start();
|
但是发现,没有效果!这是为啥呢?解释如下.
对object 的任意属性做动画 要求两个条件:
- object有 对应属性 的set方法,动画中没设置初始值 还要有get方法,系统要去取初始值(不满足则会crash)。
- set方法要对object有所改变,如UI的变化。不满足则会没有动画效果
上面Button没有动画效果,就是没有满足第二条。看下Button的setWidth方法:
1 2 3 4 5 6
| public void setWidth(int pixels) { mMaxWidth = mMinWidth = pixels; mMaxWidthMode = mMinWidthMode = PIXELS; requestLayout(); invalidate(); }
|
实际就是TextView的setWidth方法,看到设置进去的值仅影响了宽度最大值和最小值。按照官方注释和实测,发现只有当Button/TextView在xml中设置android:layout_width为”wrap_content”时,才会setWidth改变宽度;而当Button/TextView在xml中设置android:layout_width为固定dp值时,setWidth无效。 而我们上面给出的Button xml中确实是固定值180dp,所以是属性”width”的setWidth是无效的,即不满足第二条要求,就没有动画效果了。(当修改Button xml中设置android:layout_width为”wrap_content”时,上面执行的属性动画是生效的。)
那么,当不满足条件时,如何解决此问题呢? 有如下处理方法:
- 给object添加set、get方法,如果有权限。(一般不行,如TextView是SDK里面的不能直接改)
- 给Object包装一层,在包装类中提供set、get方法。
- 使用ValueAnimator,监听Value变化过程,自己实现属性的改变。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| private void testAnimatorAboutButtonWidth() { ObjectAnimator width1 = ObjectAnimator.ofInt(button, "width", 1000); width1.setDuration(2000);
ViewWrapper wrapper = new ViewWrapper(button); ObjectAnimator width2 = ObjectAnimator.ofInt(wrapper, "width", 1000); width2.setDuration(2000);
ValueAnimator valueAnimator = ValueAnimator.ofInt(button.getLayoutParams().width, 1000); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { int animatedValue = (Integer) animation.getAnimatedValue(); Log.i("hfy", "onAnimationUpdate: animatedValue=" + animatedValue);
if (button != null) { button.getLayoutParams().width = animatedValue; button.requestLayout(); } } });
valueAnimator.setDuration(4000).start();
}
private class ViewWrapper {
private final View mView;
public ViewWrapper(View view) { mView = view; }
public int getWidth() { return mView.getLayoutParams().width; }
public void setWidth(int width) { ViewGroup.LayoutParams layoutParams = mView.getLayoutParams(); layoutParams.width = width; mView.setLayoutParams(layoutParams); mView.requestLayout(); } }
|
属性动画的原理
属性动画,要求对象有这个属性的set方法,执行时会根据传入的 属性初始值、最终值,在每帧更新时调用set方法设置当前时刻的 属性值。随着时间推移,set的属性值会接近最终值,从而达到动画效果。如果没传入初始值,那么对象还要有get方法,用于获取初始值。
在获取初始值、set属性值时,都是使用 反射 的方式,进行 get、set方法的调用。 见PropertyValuesHolder的setupValue、setAnimatedValue方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| 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) { 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()); } } } 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。因为图片多。
属性动画 如果有循环动画,在页面退出时要及时停止,避免内存泄漏。
使用View动画后,调用setVisibility(View.GONE)失效时,使用view.clearAnimation()可解决。
属性动画,可能会由于View属性变化导致频繁触发重新measure layout,注意性能