Animation

Android动画分三种:View动画、帧动画、属性动画

image-20231027151841094

是的, 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中继承下来的,在alpharotatescaletranslate标签中都可以直接使用。
利用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">
<!--set里面的duration如果有值,会覆盖子标签的duration-->

<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
//view动画使用,方式一:xml,建议使用。
Animation animation = AnimationUtils.loadAnimation(this, R.anim.animation_test);
textView1.startAnimation(animation);

rotatescale动画的android:pivotXandroid:pivotY属性、translate动画的android:toXDeltaandroid:toYDelta属性的取值都可以是都可以数值、百分数、百分数p,比如:5050%50%p,他们取值的代表的意义各不相同:
50表示以View左上角为原点沿坐标轴正方向(x轴向右,y轴向下)偏移50px的位置;
50%表示以View左上角为原点沿坐标轴正方向(x轴向右,y轴向下)偏移View宽度或高度的50%处的位置;
50%p表示以View左上角为原点沿坐标轴正方向(x轴向右,y轴向下)偏移父控件宽度或高度的50%处的位置(p表示相对于ParentView的位置)。

“50”:img

“50%”img

“50%p”img

代码动态实现

在平常的业务逻辑中也可以直接用Java代码来实现Veiw动画,Android系统给我们提供了AlphaAnimationRotateAnimationScaleAnimationTranslateAnimation四个动画类分别来实现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);

注意点:

  1. startAnimation方法是立刻播放动画;setAnimation是设置要播放的下一个动画。
  2. 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
//代码设置LayoutAnimation,实现ViewGroup的child的出场动画
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

image-20231026114011752

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);

//属性动画使用,方式二:xml。 竖移
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);
//注意,这里如果不设置 那么颜色就是跳跃的,设置ArgbEvaluator 就是连续过度的颜色变化
textColor.setEvaluator(new ArgbEvaluator());

//animatorSet
mAnimatorSet = new AnimatorSet();
mAnimatorSet
.play(animatorUpAndDown)
.with(textColor)
.after(translationX);

mAnimatorSet.start();


/**
* 设置属性动画的监听
* @param translationX
*/
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"?>
<!--属性动画test,一般建议采用代码实现,不用xml-->
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:ordering="sequentially">


<!--repeatCount:默认是0,-1是无限循环-->
<!--repeatMode:重复模式:restart-从头来一遍、reverse-反向来一遍-->
<!--valueType:指定propertyName的类型可选intType、floatType-->

<!--android:pathData=""
android:propertyXName=""
android:propertyYName=""-->
<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" />

<!--animator对用vueAnimator,比objectAnimator少了propertyName-->
<!--<animator-->
<!--android:duration="2000"-->
<!--android:valueFrom=""-->
<!--android:valueTo=""-->
<!--android:startOffset=""-->
<!--android:repeatCount=""-->
<!--android:repeatMode=""-->
<!--android:valueType=""-->
<!--android:interpolator=""-->
<!--android:pathData=""-->
<!--android:propertyXName=""-->
<!--android:propertyYName=""/>-->

</set>

translationX是实现横移,animatorUpAndDown是实现竖移、textColor是实现文字颜色变化。其中animatorUpAndDown是使用xml定义,标签含义也很好理解。 最后使用AnimatorSet的play、with、after 实现 先横移,然后 竖移和颜色变化 同时的动画集合效果。

注意点

  1. 关于View动画和属性动画的平移属性动画改变属性值setTranslationX 的视图效果像view动画的平移一样,都是view实际的layout位置没变,只改变了视图位置;不同点是属性动画 给触摸点生效区域增加了位移(而view动画仅改变了视图位置)。
  2. 插值器:Interpolator,根据 时间流逝的百分比,计算当前属性值改变的百分比。 例如duration是1000,start后过了200,那么时间百分比是0.2,那么如果差值器是LinearInterpolator线性差值器,那么属性值改变的百分比也是0.2
  3. 估值器:Evaluator,就是根据 差值器获取的 属性值百分比,计算改变后的属性值。 ofInt、onFloat内部会自动设置IntEvaluator、FloatEvaluator。如果使用ofInt且是颜色相关的属性,就要设置ArgbEvaluator。 上面例子中 文字颜色变化动画 设置了ArgbEvaluator:textColor.setEvaluator(new ArgbEvaluator())。
  4. 动画监听:主要是两个监听接口,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 的任意属性做动画 要求两个条件:

  1. object有 对应属性 的set方法,动画中没设置初始值 还要有get方法,系统要去取初始值(不满足则会crash)。
  2. 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”时,上面执行的属性动画是生效的。)

那么,当不满足条件时,如何解决此问题呢? 有如下处理方法:

  1. 给object添加set、get方法,如果有权限。(一般不行,如TextView是SDK里面的不能直接改)
  2. 给Object包装一层,在包装类中提供set、get方法。
  3. 使用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() {
//Button width 属性动画:如果xml中宽度是wrap_content,那么动画有效。
// 如果设置button确切的dp值,那么无效,因为对应属性"width"的setWidth()方法就是 在wrap_content是才有效。
ObjectAnimator width1 = ObjectAnimator.ofInt(button, "width", 1000);
width1.setDuration(2000);
// width1.start();

//那么,想要在button原本有确切dp值时,要能对width动画,怎么做呢?
//方法一,包一层,然后用layoutParams
ViewWrapper wrapper = new ViewWrapper(button);
ObjectAnimator width2 = ObjectAnimator.ofInt(wrapper, "width", 1000);
width2.setDuration(2000);
// width2.start();

//方法二,使用ValueAnimator,每一帧自己显示宽度的变化
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);

// IntEvaluator intEvaluator = new IntEvaluator();
//// 获取属性值改变比例、计算属性值
// float animatedFraction = animation.getAnimatedFraction();
// Integer evaluate = intEvaluator.evaluate(animatedFraction, 300, 600);
// Log.i("hfy", "onAnimationUpdate: evaluate="+evaluate);


if (button != null) {
button.getLayoutParams().width = animatedValue;
button.requestLayout();
}
}
});

valueAnimator.setDuration(4000).start();

}

/**
* 包一层,提供对应属性的set、get方法
*/
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) {
// 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());
}
}
}
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。因为图片多。

  2. 属性动画 如果有循环动画,在页面退出时要及时停止,避免内存泄漏。

  3. 使用View动画后,调用setVisibility(View.GONE)失效时,使用view.clearAnimation()可解决。

  4. 属性动画,可能会由于View属性变化导致频繁触发重新measure layout,注意性能

Author

white crow

Posted on

2023-10-26

Updated on

2024-04-17

Licensed under