动画Animation
breewf

概述

Android动画大致可以分为帧动画(Frame Animation)补间动画(Tween Animation)属性动画(Property Animation)转场动画(Transition Animation)

帧动画

逐帧动画是一种常见的动画形式(Frame By Frame),其原理是在“连续的关键帧”中分解动画动作,也就是在时间轴的每帧上逐帧绘制不同的内容,使其连续播放而成动画。 因为逐帧动画的帧序列内容不一样,不但给制作增加了负担而且最终输出的文件量也很大,但它的优势也很明显:逐帧动画具有非常大的灵活性,几乎可以表现任何想表现的内容,而它类似与电影的播放模式,很适合于表演细腻的动画。例如:人物或动物急剧转身、 头发及衣服的飘动、走路、说话以及精致的3D效果等等。

在Android开发中,使用帧动画,他的核心类是 AnimationDrawable

有两种方式可以使用帧动画,一种是写入静态 xml 获得 AnimationDrawable,还有一种是创建 AnimationDrawable 对象以后,对其添加关键帧。

xml 资源文件创建

在 xml 中的 drawable 新建一个根节点为 animation-list 动画的资源,再将每一帧动画按顺序摆放。

例如,新建一个 anim_frame.xml

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
<!-- oneshot 是否只播放一次 -->
<!-- duration 持续时间 单位毫秒 -->
<item android:drawable="@drawable/farm_0" android:duration="100" />
<item android:drawable="@drawable/farm_1" android:duration="100" />
<item android:drawable="@drawable/farm_2" android:duration="100" />

</animation-list>

属性值含义:

参数 含义
oneshot 是否只播放一次 false: 只播放一次; true:循环播放
drawable 图片资源
duration 播放时间

添加到 ImageView 中:

1
2
3
4
imageView = findViewById(R.id.image);
imageView.setImageResource(R.drawable.anim_frame);
AnimationDrawable animationDrawable = (AnimationDrawable) imageView.getDrawable();
animationDrawable.start();

代码创建

1
2
3
4
5
6
7
8
9
10
11
12
13
imageView = findViewById(R.id.image);
AnimationDrawable animationDrawable1 = new AnimationDrawable();
animationDrawable1.addFrame(getResources().getDrawable(R.drawable.iron_1 ),200);
animationDrawable1.addFrame(getResources().getDrawable(R.drawable.iron_2 ),200);
animationDrawable1.addFrame(getResources().getDrawable(R.drawable.iron_3 ),200);
animationDrawable1.addFrame(getResources().getDrawable(R.drawable.iron_4 ),200);
animationDrawable1.addFrame(getResources().getDrawable(R.drawable.iron_5 ),200);
animationDrawable1.addFrame(getResources().getDrawable(R.drawable.iron_6 ),200);
animationDrawable1.addFrame(getResources().getDrawable(R.drawable.iron_7 ),200);
animationDrawable1.addFrame(getResources().getDrawable(R.drawable.iron_8 ),200);
animationDrawable1.setOneShot(true);
imageView.setImageDrawable(animationDrawable1);
animationDrawable1.start();

优缺点

  • 优点:使用简单、方便
  • 缺点:因为是多张图片逐帧播放的,对于内存的消耗比较大,容易出现卡顿,甚至OOM( java.lang.OutOfMemoryError ),因为会使用大量 & 尺寸较大的图片资源

补间动画

补间动画指的是做FLASH动画时,在两个关键帧中间需要做“补间动画”,才能实现图画的运动;插入补间动画后两个关键帧之间的插补帧是由计算机自动运算而得到的。

在Android中则是通过使用 Animation 对 view 执行一系列转换来创建动画,通过对 view 执行旋转、淡出、平移和缩放等转换,达到动画的效果。

补间动画的分类

类型 含义 对应 Animation 子类 对应的 xml 属性
Translate 平移动画 TranslateAnimation translate
Scale 缩放动画 ScaleAnimation scale
Rotate 旋转动画 RotateAnimation rotate
Alpha 透明度动画 AlphaAnimation alpha
Set 组合动画 AnimationSet set

如何使用

补间动画的使用方式有两种:xml 和 代码动态设置

  • xml 方式

需要在 res/anim 文件夹下面创建对应的静态动画资源,再使用 AnimationUtils.loadAnimation(this, 资源ID) 的方式获取到 Animation 对象, 最后使用view.startAnimation(anim)的方式设置给需要添加动画效果的View。

  • 代码设置

使用 Animation 的继承类,如 TranslateAnimation 等,设置需要的属性,最后使用view.startAnimation(anim)的方式设置给需要添加动画效果的View。

透明度动画

  • android:fromAlpha 动画开始的透明度,从0.0 - 1.0 ,0.0表示全透明,1.0表示完全不透明
  • android:toAlpha 动画结束时的透明度,也是从0.0 - 1.0 ,0.0表示全透明,1.0表示完全不透明
1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="1000"
android:fromAlpha="1.0"
android:toAlpha="0.0">
</alpha>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 btn_alpha.setOnClickListener {

// 方式1 代码设置
/**
* 参数 1 fromAlpha 动画开始时的透明度
* 参数 2 toAlpha 动画结束时的透明度
*/
val anim = AlphaAnimation(1f, 0f)
anim.duration = 1000
view_anim.startAnimation(anim)

// 方式2 xml 设置
val animation = AnimationUtils.loadAnimation(this, R.anim.alpha)
view_anim_2.startAnimation(animation)
}

旋转动画

  • android:fromDegrees 开始旋转的角度位置,正值代表顺时针方向度数,负值代表逆时针方向度数
  • android:toDegrees 结束时旋转到的角度位置,正值代表顺时针方向度数,负值代表逆时针方向度数
  • android:pivotX 缩放起点X轴坐标,可以是数值、百分数、百分数p 三种样式,比如 50、50%、50%p
  • android:pivotY 缩放起点Y轴坐标,可以是数值、百分数、百分数p 三种样式,比如 50、50%、50%p

当为数值时,表示在当前View的左上角,即原点处加上50px,做为起始点;如果是50%,表示在当前控件的左上角加上自己宽度的50%做为起始点;如果是50%p,那么就是表示在当前的左上角加上父控件宽度的50%做为起始点

取值 含义
10 距离动画所在view自身左边缘10像素
10% 距离动画所在view自身左边缘的距离是整个view宽度的10%
10%p 距离动画所在view父控件左边缘的距离是整个view宽度的10%
1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="1000"
android:fromDegrees="0"
android:pivotX="0"
android:pivotY="0"
android:toDegrees="360">

</rotate>
1
2
3
4
5
6
7
8
9
10
11
12
 //旋转
btn_rotate.setOnClickListener {
// 方式1 代码设置
val anim = RotateAnimation(0f, 360f,0f,0f)
anim.duration = 1000
view_anim.startAnimation(anim)


// 方式2 xml 设置
val animation = AnimationUtils.loadAnimation(this, R.anim.rotate)
view_anim_2.startAnimation(animation)
}

缩放动画

  • android:fromXScale 起始的X方向上相对自身的缩放比例,浮点值,比如1.0代表自身无变化,0.5代表起始时缩小一倍,2.0代表放大一倍
  • android:toXScale 结尾的X方向上相对自身的缩放比例,浮点值
  • android:fromYScale 起始的Y方向上相对自身的缩放比例,浮点值
  • android:toYScale 结尾的Y方向上相对自身的缩放比例,浮点值
  • android:pivotX 缩放起点X轴坐标,可以是数值、百分数、百分数p 三种样式,比如 50、50%、50%p
  • android:pivotY 缩放起点Y轴坐标,取值及意义跟android:pivotX一样
1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<scale xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="1000"
android:fromYScale="1.0"
android:toYScale="0.0"
android:fromXScale="1.0"
android:toXScale="0.0"
android:pivotX="50%"
android:pivotY="50%"
>

</scale>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

//缩放
btn_scale.setOnClickListener {
// 方式1 代码设置
val anim = ScaleAnimation(
1.0f, 0f,
1.0f, 0f,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f)
anim.duration = 1000
view_anim.startAnimation(anim)

// 方式2 xml 设置
val animation = AnimationUtils.loadAnimation(this, R.anim.scale)
view_anim_2.startAnimation(animation)
}

平移动画

  • android:fromXDelta 起始点X轴坐标,可以是数值、百分数、百分数p 三种样式,比如 50、50%、50%p
  • android:fromYDelta 起始点Y轴从标,可以是数值、百分数、百分数p 三种样式
  • android:toXDelta 结束点X轴坐标
  • android:toYDelta 结束点Y轴坐标
1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="1000"
android:fromXDelta="0"
android:toXDelta="50%p"
android:fromYDelta="0"
android:toYDelta="0"
>
</translate>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//平移
btn_translate.setOnClickListener {

// 方式1 代码设置
val anim = TranslateAnimation(
Animation.RELATIVE_TO_PARENT, 0f, Animation.RELATIVE_TO_PARENT, 0.5f,
Animation.RELATIVE_TO_PARENT, 0f, Animation.RELATIVE_TO_PARENT, 0f
)
anim.duration = 1000
view_anim.startAnimation(anim)

// 方式2 xml 设置
val animation = AnimationUtils.loadAnimation(this, R.anim.translate)
view_anim_2.startAnimation(animation)
}

组合动画

set标签自已是没有属性的,他的属性都是从Animation继承而来,但当它们用于Set标签时,就会对Set标签下的所有子控件都产生作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:duration="1000"
android:fromAlpha="1.0"
android:toAlpha="0.0" />

<translate
android:duration="1000"
android:fromXDelta="0"
android:fromYDelta="0"
android:toXDelta="50%p"
android:toYDelta="0" />

</set>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 //组合动画
btn_set.setOnClickListener {
// 方式1 代码设置
// true : 所有子元素中共用同一插值器
val anim = AnimationSet(true)
anim.addAnimation(
TranslateAnimation(
Animation.RELATIVE_TO_PARENT, 0f, Animation.RELATIVE_TO_PARENT, 0.5f,
Animation.RELATIVE_TO_PARENT, 0f, Animation.RELATIVE_TO_PARENT, 0f
)
)
anim.addAnimation(AlphaAnimation(1f, 0f))
anim.duration = 1000
view_anim.startAnimation(anim)

// 方式2 xml 设置
val animation = AnimationUtils.loadAnimation(this, R.anim.set)
view_anim_2.startAnimation(animation)
}

Animation类属性

Animation类是所有动画(scale、alpha、translate、rotate)的基类。

  • android:duration 动画持续时间,以毫秒为单位
  • android:fillAfter 如果设置为true,控件动画结束时,将保持动画最后时的状态
  • android:fillBefore 如果设置为true,控件动画结束时,还原到开始动画前的状态
  • android:fillEnabled 与android:fillBefore 效果相同,都是在动画结束时,将控件还原到初始化状态
  • android:repeatCount 重复次数
  • android:repeatMode 重复类型,有reverserestart两个值,reverse表示倒序回放,restart表示重新放一遍,必须与repeatCount一起使用才能看到效果。因为这里的意义是重复的类型,即回放时的动作
  • android:interpolator 设定插值器,控制动画的变化速率

监听器

使用 setAnimationListener 方法即可添加监视器。

1
public void setAnimationListener(AnimationListener listener) 
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

val anim = TranslateAnimation(
Animation.RELATIVE_TO_PARENT, 0f, Animation.RELATIVE_TO_PARENT, 0.7f,
Animation.RELATIVE_TO_PARENT, 0f, Animation.RELATIVE_TO_PARENT, 0f
)
anim.repeatCount = ValueAnimator.INFINITE
anim.repeatMode = ValueAnimator.REVERSE
anim.duration = 2000
anim.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationStart(animation: Animation?) {
view_anim.text = "补间动画开始"
}

override fun onAnimationEnd(animation: Animation?) {
view_anim.text = "补间动画结束"
}

override fun onAnimationRepeat(animation: Animation?) {
view_anim.text = "补间动画重复"
}
})
view_anim.startAnimation(anim)


// 点击view触发 cancel 方法
view_anim.setOnClickListener {
anim.cancel()
}

补间动画的特点

值得注意的是:补间动画执行之后并未改变View的真实布局属性

假设现在Activity中有一个 Button在屏幕上方,设置了平移动画移动到屏幕下方然后保持动画最后执行状态呆在屏幕下方。如果点击屏幕下方动画执行之后的Button是没有任何反应,而点击原来屏幕上方没有Button的地方却响应的是点击Button的事件,这一点是需要注意的。

因为View动画是通过 ParentView 来不断调整 ChildView 的画布来实现的,比如平移,就是 ParentView 在 dispatchDraw 时发现这里有个平移动画,就调用了画布的 traslate 方法。也由于只是针对 ChildView 的画布进行操作,对 ChildView 的位置,大小等属性没有任何修改,所以才可以在View动画执行后,ChildView 的原位置响应点击事件。

属性动画

属性动画,通过修改 View 本身的属性,达到动画的效果。例如修改 view 的位置让他实现移动的效果,修改 view 的 alpha 实现透明度的效果。和补间动画有着本质的区别。

自Android 3.0版本开始,系统给我们提供了一种全新的动画模式,属性动画(property animation),它的功能非常强大,弥补了之前补间动画的一些缺陷,几乎是可以完全替代掉补间动画了。

ValueAnimator

ValueAnimator是整个属性动画机制当中最核心的一个类,属性动画的运行机制是通过不断地对值进行操作来实现的,而初始值和结束值之间的动画过渡就是由ValueAnimator这个类来负责计算的。它的内部使用一种时间循环的机制来计算值与值之间的动画过渡,我们只需要将初始值和结束值提供给ValueAnimator,并且告诉它动画所需运行的时长,那么ValueAnimator就会自动帮我们完成从初始值平滑地过渡到结束值这样的效果。除此之外,ValueAnimator还负责管理动画的播放次数、播放模式、以及对动画设置监听器等,确实是一个非常重要的类。

但是ValueAnimator的用法却一点都不复杂,我们先从最简单的功能看起吧,比如说想要将一个值从0平滑过渡到1,时长300毫秒,就可以这样写:

1
2
3
ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
anim.setDuration(300);
anim.start();

调用ValueAnimator的ofFloat()方法就可以构建出一个ValueAnimator的实例,ofFloat()方法当中允许传入多个float类型的参数,这里传入0和1就表示将值从0平滑过渡到1,然后调用ValueAnimator的setDuration()方法来设置动画运行的时长,最后调用start()方法启动动画。

动画监听器:

1
2
3
4
5
6
7
8
9
10
ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
anim.setDuration(300);
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float currentValue = (float) animation.getAnimatedValue();
Log.d("TAG", "cuurent value is " + currentValue);
}
});
anim.start();

另外ofFloat()方法当中是可以传入任意多个参数的,因此我们还可以构建出更加复杂的动画逻辑,比如说将一个值在5秒内从0过渡到5,再过渡到3,再过渡到10,就可以这样写:

1
2
3
ValueAnimator anim = ValueAnimator.ofFloat(0f, 5f, 3f, 10f);
anim.setDuration(5000);
anim.start();

当然也许你并不需要小数位数的动画过渡,可能你只是希望将一个整数值从0平滑地过渡到100,那么也很简单,只需要调用ValueAnimator的ofInt()方法就可以了,如下所示:

1
ValueAnimator anim = ValueAnimator.ofInt(0, 100);

ValueAnimator当中最常用的应该就是ofFloat()和ofInt()这两个方法了,另外还有一个ofObject()方法:

1
ValueAnimator ofObject(TypeEvaluator evaluator, Object... values)

除此之外,我们还可以调用**setStartDelay()方法来设置动画延迟播放的时间,调用setRepeatCount()setRepeatMode()**方法来设置动画循环播放的次数以及循环播放的模式,循环模式包括RESTART和REVERSE两种,分别表示重新播放和倒序播放的意思。

ObjectAnimator

相比于ValueAnimator,ObjectAnimator可能才是我们最常接触到的类,因为ValueAnimator只不过是对值进行了一个平滑的动画过渡。而ObjectAnimator则就不同了,它是可以直接对对象的属性进行动画操作的,比如说View的alpha属性。

虽说ObjectAnimator会更加常用一些,但是它其实是继承自ValueAnimator的,底层的动画实现机制也是基于ValueAnimator来完成的,因此ValueAnimator仍然是整个属性动画当中最核心的一个类。那么既然是继承关系,说明ValueAnimator中可以使用的方法在ObjectAnimator中也是可以正常使用的,它们的用法也非常类似,这里如果我们想要将一个TextView在5秒中内从常规变换成全透明,再从全透明变换成常规,就可以这样写:

1
2
3
ObjectAnimator animator = ObjectAnimator.ofFloat(textview, "alpha", 1f, 0f, 1f);
animator.setDuration(5000);
animator.start();

学会了这一个用法之后,其它的用法我们就可以举一反三了,那比如说我们想要将TextView进行一次360度的旋转,就可以这样写:

1
2
3
ObjectAnimator animator = ObjectAnimator.ofFloat(textview, "rotation", 0f, 360f);
animator.setDuration(5000);
animator.start();

那么如果想要将TextView先向左移出屏幕,然后再移动回来,就可以这样写:

1
2
3
4
float curTranslationX = textview.getTranslationX();
ObjectAnimator animator = ObjectAnimator.ofFloat(textview, "translationX", curTranslationX, -500f, curTranslationX);
animator.setDuration(5000);
animator.start();

然后我们还可以TextView进行缩放操作,比如说将TextView在垂直方向上放大3倍再还原,就可以这样写:

1
2
3
ObjectAnimator animator = ObjectAnimator.ofFloat(textview, "scaleY", 1f, 3f, 1f);
animator.setDuration(5000);
animator.start();

组合动画

实现组合动画功能主要需要借助AnimatorSet这个类,这个类提供了一个play()方法,如果我们向这个方法中传入一个Animator对象(ValueAnimator或ObjectAnimator)将会返回一个AnimatorSet.Builder的实例,AnimatorSet.Builder中包括以下四个方法:

  • after(Animator anim) 将现有动画插入到传入的动画之后执行
  • after(long delay) 将现有动画延迟指定毫秒后执行
  • before(Animator anim) 将现有动画插入到传入的动画之前执行
  • with(Animator anim) 将现有动画和传入的动画同时执行

好的,有了这四个方法,我们就可以完成组合动画的逻辑了,那么比如说我们想要让TextView先从屏幕外移动进屏幕,然后开始旋转360度,旋转的同时进行淡入淡出操作,就可以这样写:

1
2
3
4
5
6
7
ObjectAnimator moveIn = ObjectAnimator.ofFloat(textview, "translationX", -500f, 0f);
ObjectAnimator rotate = ObjectAnimator.ofFloat(textview, "rotation", 0f, 360f);
ObjectAnimator fadeInOut = ObjectAnimator.ofFloat(textview, "alpha", 1f, 0f, 1f);
AnimatorSet animSet = new AnimatorSet();
animSet.play(rotate).with(fadeInOut).after(moveIn);
animSet.setDuration(5000);
animSet.start();

可以看到,这里我们先是把三个动画的对象全部创建出来,然后new出一个AnimatorSet对象之后将这三个动画对象进行播放排序,让旋转和淡入淡出动画同时进行,并把它们插入到了平移动画的后面,最后是设置动画时长以及启动动画。运行一下上述代码,效果如下图所示:

Animator监听器

在很多时候,我们希望可以监听到动画的各种事件,比如动画何时开始,何时结束,然后在开始或者结束的时候去执行一些逻辑处理。这个功能是完全可以实现的,Animator类当中提供了一个addListener()方法,这个方法接收一个AnimatorListener,我们只需要去实现这个AnimatorListener就可以监听动画的各种事件了。

ObjectAnimator是继承自ValueAnimator的,而ValueAnimator又是继承自Animator的,因此不管是ValueAnimator还是ObjectAnimator都是可以使用addListener()这个方法的。另外AnimatorSet也是继承自Animator的,因此addListener()这个方法算是个通用的方法。

添加一个监听器的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
anim.addListener(new AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}

@Override
public void onAnimationRepeat(Animator animation) {
}

@Override
public void onAnimationEnd(Animator animation) {
}

@Override
public void onAnimationCancel(Animator animation) {
}
});

可以看到,我们需要实现接口中的四个方法,onAnimationStart()方法会在动画开始的时候调用,onAnimationRepeat()方法会在动画重复执行的时候调用,onAnimationEnd()方法会在动画结束的时候调用,onAnimationCancel()方法会在动画被取消的时候调用。

但是也许很多时候我们并不想要监听那么多个事件,可能我只想要监听动画结束这一个事件,那么每次都要将四个接口全部实现一遍就显得非常繁琐。没关系,为此Android提供了一个适配器类,叫作AnimatorListenerAdapter,使用这个类就可以解决掉实现接口繁琐的问题了,如下所示:

1
2
anim.addListener(new AnimatorListenerAdapter() {
});

那么如果我想监听动画结束这个事件,就只需要单独重写这一个方法就可以了,如下所示:

1
2
3
4
5
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
}
});

PropertyValuesHolder

ValueAnimator和ObjectAnimator除了通过ofInt(),ofFloat(),ofObject()创建实例外,还都有一个 ofPropertyValuesHolder() 方法来创建实例:

1
2
3
4
5
6
7
8
/**
* valueAnimator的
*/
public static ValueAnimator ofPropertyValuesHolder(PropertyValuesHolder... values)
/**
* ObjectAnimator的
*/
public static ObjectAnimator ofPropertyValuesHolder(Object target,PropertyValuesHolder... values)
1
2
3
4
public static PropertyValuesHolder ofFloat(String propertyName, float... values)
public static PropertyValuesHolder ofInt(String propertyName, int... values)
public static PropertyValuesHolder ofObject(String propertyName, TypeEvaluator evaluator,Object... values)
public static PropertyValuesHolder ofKeyframe(String propertyName, Keyframe... values)

ObjectAnimator给我们提供了一个设置PropertyValuesHolder实例的入口:

1
public static ObjectAnimator ofPropertyValuesHolder(Object target,PropertyValuesHolder... values) 
  • target:指需要执行动画的控件
  • values:是一个可变长参数,可以传进去多个PropertyValuesHolder实例,由于每个PropertyValuesHolder实例都会针对一个属性做动画,所以如果传进去多个PropertyValuesHolder实例,将会对控件的多个属性同时做动画操作。

用法示例:

1
2
3
4
5
6
PropertyValuesHolder pvhXx = PropertyValuesHolder.ofFloat("x", startXx, endXx);
PropertyValuesHolder pvhYy = PropertyValuesHolder.ofFloat("y", startYy, startYy);

ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(danMuView, pvhXx, pvhYy);
animator.setDuration(600);
animator.start();

Keyframe

关键帧。

1
public static Keyframe ofFloat(float fraction, float value)
  • fraction:表示当前的显示进度,即从加速器中getInterpolation()函数的返回值;
  • value:表示当前应该在的位置

比如Keyframe.ofFloat(0, 0)表示动画进度为0时,动画所在的数值位置为0;Keyframe.ofFloat(0.25f, -20f)表示动画进度为25%时,动画所在的数值位置为-20;Keyframe.ofFloat(1f,0)表示动画结束时,动画所在的数值位置为0;
在理解了KeyFrame.ofFloat()的参数以后,我们来看看PropertyValuesHolder是如何使用KeyFrame对象的:

1
public static PropertyValuesHolder ofKeyframe(String propertyName, Keyframe... values)
  • propertyName:动画所要操作的属性名
  • values:Keyframe的列表,PropertyValuesHolder会根据每个Keyframe的设定,定时将指定的值输出给动画。

完整的KeyFrame的使用代码应该是这样的:

1
2
3
4
5
6
7
Keyframe frame0 = Keyframe.ofFloat(0f, 0);
Keyframe frame1 = Keyframe.ofFloat(0.1f, -20f);
Keyframe frame2 = Keyframe.ofFloat(1, 0);
PropertyValuesHolder frameHolder = PropertyValuesHolder.ofKeyframe("rotation",frame0,frame1,frame2);
Animator animator = ObjectAnimator.ofPropertyValuesHolder(mImage,frameHolder);
animator.setDuration(1000);
animator.start();
  • 第一步:生成Keyframe对象;
  • 第二步:利用PropertyValuesHolder.ofKeyframe()生成PropertyValuesHolder对象
  • 第三步:ObjectAnimator.ofPropertyValuesHolder()生成对应的Animator

使用XML编写动画

通过XML来编写动画可能会比通过代码来编写动画要慢一些,但是在重用方面将会变得非常轻松,比如某个将通用的动画编写到XML里面,我们就可以在各个界面当中轻松去重用它。

如果想要使用XML来编写动画,首先要在res目录下面新建一个animator文件夹,所有属性动画的XML文件都应该存放在这个文件夹当中。然后在XML文件中我们一共可以使用如下三种标签:

  • 对应代码中的ValueAnimator
  • 对应代码中的ObjectAnimator
  • 对应代码中的AnimatorSet

那么比如说我们想要实现一个从0到100平滑过渡的动画,在XML当中就可以这样写:

1
2
3
4
<animator xmlns:android="http://schemas.android.com/apk/res/android"
android:valueFrom="0"
android:valueTo="100"
android:valueType="intType"/>

而如果我们想将一个视图的alpha属性从1变成0,就可以这样写:

1
2
3
4
5
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:valueFrom="1"
android:valueTo="0"
android:valueType="floatType"
android:propertyName="alpha"/>

另外,我们也可以使用XML来完成复杂的组合动画操作,比如将一个视图先从屏幕外移动进屏幕,然后开始旋转360度,旋转的同时进行淡入淡出操作,就可以这样写:

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
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:ordering="sequentially" >

<objectAnimator
android:duration="2000"
android:propertyName="translationX"
android:valueFrom="-500"
android:valueTo="0"
android:valueType="floatType" >
</objectAnimator>

<set android:ordering="together" >
<objectAnimator
android:duration="3000"
android:propertyName="rotation"
android:valueFrom="0"
android:valueTo="360"
android:valueType="floatType" >
</objectAnimator>

<set android:ordering="sequentially" >
<objectAnimator
android:duration="1500"
android:propertyName="alpha"
android:valueFrom="1"
android:valueTo="0"
android:valueType="floatType" >
</objectAnimator>
<objectAnimator
android:duration="1500"
android:propertyName="alpha"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" >
</objectAnimator>
</set>
</set>

</set>

XML文件是编写好了,那么我们如何在代码中把文件加载进来并将动画启动呢?只需调用如下代码即可:

1
2
3
Animator animator = AnimatorInflater.loadAnimator(context, R.animator.anim_file);
animator.setTarget(view);
animator.start();

调用AnimatorInflaterloadAnimator来将XML动画文件加载进来,然后再调用setTarget()方法将这个动画设置到某一个对象上面,最后再调用start()方法启动动画就可以了,就是这么简单。

ValueAnimator的高级用法以及TypeEvaluator

TypeEvaluator的作用到底是什么呢?简单来说,就是告诉动画系统如何从初始值过度到结束值。我们在上一篇文章中学到的ValueAnimator.ofFloat()方法就是实现了初始值与结束值之间的平滑过度,那么这个平滑过度是怎么做到的呢?其实就是系统内置了一个FloatEvaluator,它通过计算告知动画系统如何从初始值过度到结束值,我们来看一下FloatEvaluator的代码实现:

1
2
3
4
5
6
public class FloatEvaluator implements TypeEvaluator {
public Object evaluate(float fraction, Object startValue, Object endValue) {
float startFloat = ((Number) startValue).floatValue();
return startFloat + fraction * (((Number) endValue).floatValue() - startFloat);
}
}

可以看到,FloatEvaluator实现了TypeEvaluator接口,然后重写evaluate()方法。evaluate()方法当中传入了三个参数,第一个参数fraction非常重要,这个参数用于表示动画的完成度的,我们应该根据它来计算当前动画的值应该是多少,第二第三个参数分别表示动画的初始值和结束值。那么上述代码的逻辑就比较清晰了,用结束值减去初始值,算出它们之间的差值,然后乘以fraction这个系数,再加上初始值,那么就得到当前动画的值了。

ValueAnimator中还有一个ofObject()方法,是用于对任意对象进行动画操作的。但是相比于浮点型或整型数据,对象的动画操作明显要更复杂一些,因为系统将完全无法知道如何从初始对象过度到结束对象,因此这个时候我们就需要实现一个自己的TypeEvaluator来告知系统如何进行过度。

下面来先定义一个Point类,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Point {

private float x;

private float y;

public Point(float x, float y) {
this.x = x;
this.y = y;
}

public float getX() {
return x;
}

public float getY() {
return y;
}

}

Point类非常简单,只有x和y两个变量用于记录坐标的位置,并提供了构造方法来设置坐标,以及get方法来获取坐标。接下来定义PointEvaluator,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class PointEvaluator implements TypeEvaluator{

@Override
public Object evaluate(float fraction, Object startValue, Object endValue) {
Point startPoint = (Point) startValue;
Point endPoint = (Point) endValue;
float x = startPoint.getX() + fraction * (endPoint.getX() - startPoint.getX());
float y = startPoint.getY() + fraction * (endPoint.getY() - startPoint.getY());
Point point = new Point(x, y);
return point;
}

}

可以看到,PointEvaluator同样实现了TypeEvaluator接口并重写了evaluate()方法。其实evaluate()方法中的逻辑还是非常简单的,先是将startValue和endValue强转成Point对象,然后同样根据fraction来计算当前动画的x和y的值,最后组装到一个新的Point对象当中并返回。

这样我们就将PointEvaluator编写完成了,接下来我们就可以非常轻松地对Point对象进行动画操作了,比如说我们有两个Point对象,现在需要将Point1通过动画平滑过度到Point2,就可以这样写:

1
2
3
4
5
Point point1 = new Point(0, 0);
Point point2 = new Point(300, 300);
ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), point1, point2);
anim.setDuration(5000);
anim.start();

好的,这就是自定义TypeEvaluator的全部用法,掌握了这些知识之后,我们就可以来尝试一下如何通过对Point对象进行动画操作,从而实现整个自定义View的动画效果。

新建一个MyAnimView继承自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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class MyAnimView extends View {

public static final float RADIUS = 50f;

private Point currentPoint;

private Paint mPaint;

public MyAnimView(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.BLUE);
}

@Override
protected void onDraw(Canvas canvas) {
if (currentPoint == null) {
currentPoint = new Point(RADIUS, RADIUS);
drawCircle(canvas);
startAnimation();
} else {
drawCircle(canvas);
}
}

private void drawCircle(Canvas canvas) {
float x = currentPoint.getX();
float y = currentPoint.getY();
canvas.drawCircle(x, y, RADIUS, mPaint);
}

private void startAnimation() {
Point startPoint = new Point(RADIUS, RADIUS);
Point endPoint = new Point(getWidth() - RADIUS, getHeight() - RADIUS);
ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), startPoint, endPoint);
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
currentPoint = (Point) animation.getAnimatedValue();
invalidate();
}
});
anim.setDuration(5000);
anim.start();
}

}

首先在自定义View的构造方法当中初始化了一个Paint对象作为画笔,并将画笔颜色设置为蓝色,接着在onDraw()方法当中进行绘制。这里我们绘制的逻辑是由currentPoint这个对象控制的,如果currentPoint对象不等于空,那么就调用drawCircle()方法在currentPoint的坐标位置画出一个半径为50的圆,如果currentPoint对象是空,那么就调用startAnimation()方法来启动动画。

我们来观察一下startAnimation()方法中的代码,其实大家应该很熟悉了,就是对Point对象进行了一个动画操作而已。这里我们定义了一个startPoint和一个endPoint,坐标分别是View的左上角和右下角,并将动画的时长设为5秒。然后有一点需要大家注意的,就是我们通过监听器对动画的过程进行了监听,每当Point值有改变的时候都会回调onAnimationUpdate()方法。在这个方法当中,我们对currentPoint对象进行了重新赋值,并调用了invalidate()方法,这样的话onDraw()方法就会重新调用,并且由于currentPoint对象的坐标已经改变了,那么绘制的位置也会改变,于是一个平移的动画效果也就实现了。

下面我们只需要在布局文件当中引入这个自定义控件:

1
2
3
4
5
6
7
8
9
10
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>

<com.example.tony.myapplication.MyAnimView
android:layout_width="match_parent"
android:layout_height="match_parent" />

</RelativeLayout>

最后运行一下程序,效果如下图所示:

ObjectAnimator的高级用法

我们在吐槽补间动画的时候有提到过,补间动画是只能实现移动、缩放、旋转和淡入淡出这四种动画操作的,功能限定死就是这些,基本上没有任何扩展性可言。比如我们想要实现对View的颜色进行动态改变,补间动画是没有办法做到的。

但是属性动画就不会受这些条条框框的限制,它的扩展性非常强,对于动态改变View的颜色这种功能是完全可是胜任的,那么下面我们就来学习一下如何实现这样的效果。

ObjectAnimator内部的工作机制是通过寻找特定属性的get和set方法,然后通过方法不断地对值进行改变,从而实现动画效果的。因此我们就需要在MyAnimView中定义一个color属性,并提供它的get和set方法。这里我们可以将color属性设置为字符串类型,使用#RRGGBB这种格式来表示颜色值,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyAnimView extends View {

...

private String color;

public String getColor() {
return color;
}

public void setColor(String color) {
this.color = color;
mPaint.setColor(Color.parseColor(color));
invalidate();
}

...

}

注意在setColor()方法当中,我们编写了一个非常简单的逻辑,就是将画笔的颜色设置成方法参数传入的颜色,然后调用了invalidate()方法。这段代码虽然只有三行,但是却执行了一个非常核心的功能,就是在改变了画笔颜色之后立即刷新视图,然后onDraw()方法就会调用。在onDraw()方法当中会根据当前画笔的颜色来进行绘制,这样颜色也就会动态进行改变了。

那么接下来的问题就是怎样让setColor()方法得到调用了,毫无疑问,当然是要借助ObjectAnimator类,但是在使用ObjectAnimator之前我们还要完成一个非常重要的工作,就是编写一个用于告知系统如何进行颜色过度的TypeEvaluator。创建ColorEvaluator并实现TypeEvaluator接口,代码如下所示:

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
public class ColorEvaluator implements TypeEvaluator {

private int mCurrentRed = -1;

private int mCurrentGreen = -1;

private int mCurrentBlue = -1;

@Override
public Object evaluate(float fraction, Object startValue, Object endValue) {
String startColor = (String) startValue;
String endColor = (String) endValue;
int startRed = Integer.parseInt(startColor.substring(1, 3), 16);
int startGreen = Integer.parseInt(startColor.substring(3, 5), 16);
int startBlue = Integer.parseInt(startColor.substring(5, 7), 16);
int endRed = Integer.parseInt(endColor.substring(1, 3), 16);
int endGreen = Integer.parseInt(endColor.substring(3, 5), 16);
int endBlue = Integer.parseInt(endColor.substring(5, 7), 16);
// 初始化颜色的值
if (mCurrentRed == -1) {
mCurrentRed = startRed;
}
if (mCurrentGreen == -1) {
mCurrentGreen = startGreen;
}
if (mCurrentBlue == -1) {
mCurrentBlue = startBlue;
}
// 计算初始颜色和结束颜色之间的差值
int redDiff = Math.abs(startRed - endRed);
int greenDiff = Math.abs(startGreen - endGreen);
int blueDiff = Math.abs(startBlue - endBlue);
int colorDiff = redDiff + greenDiff + blueDiff;
if (mCurrentRed != endRed) {
mCurrentRed = getCurrentColor(startRed, endRed, colorDiff, 0,
fraction);
} else if (mCurrentGreen != endGreen) {
mCurrentGreen = getCurrentColor(startGreen, endGreen, colorDiff,
redDiff, fraction);
} else if (mCurrentBlue != endBlue) {
mCurrentBlue = getCurrentColor(startBlue, endBlue, colorDiff,
redDiff + greenDiff, fraction);
}
// 将计算出的当前颜色的值组装返回
String currentColor = "#" + getHexString(mCurrentRed)
+ getHexString(mCurrentGreen) + getHexString(mCurrentBlue);
return currentColor;
}

/**
* 根据fraction值来计算当前的颜色。
*/
private int getCurrentColor(int startColor, int endColor, int colorDiff,
int offset, float fraction) {
int currentColor;
if (startColor > endColor) {
currentColor = (int) (startColor - (fraction * colorDiff - offset));
if (currentColor < endColor) {
currentColor = endColor;
}
} else {
currentColor = (int) (startColor + (fraction * colorDiff - offset));
if (currentColor > endColor) {
currentColor = endColor;
}
}
return currentColor;
}

/**
* 将10进制颜色值转换成16进制。
*/
private String getHexString(int value) {
String hexString = Integer.toHexString(value);
if (hexString.length() == 1) {
hexString = "0" + hexString;
}
return hexString;
}

}

首先在evaluate()方法当中获取到颜色的初始值和结束值,并通过字符串截取的方式将颜色分为RGB三个部分,并将RGB的值转换成十进制数字,那么每个颜色的取值范围就是0-255。接下来计算一下初始颜色值到结束颜色值之间的差值,这个差值很重要,决定着颜色变化的快慢,如果初始颜色值和结束颜色值很相近,那么颜色变化就会比较缓慢,而如果颜色值相差很大,比如说从黑到白,那么就要经历255*3这个幅度的颜色过度,变化就会非常快。

那么控制颜色变化的速度是通过getCurrentColor()这个方法来实现的,这个方法会根据当前的fraction值来计算目前应该过度到什么颜色,并且这里会根据初始和结束的颜色差值来控制变化速度,最终将计算出的颜色进行返回。

最后,由于我们计算出的颜色是十进制数字,这里还需要调用一下getHexString()方法把它们转换成十六进制字符串,再将RGB颜色拼装起来之后作为最终的结果返回。

好了,ColorEvaluator写完之后我们就把最复杂的工作完成了,剩下的就是一些简单调用的问题了,比如说我们想要实现从蓝色到红色的动画过度,历时5秒,就可以这样写:

1
2
3
4
ObjectAnimator anim = ObjectAnimator.ofObject(myAnimView, "color", new ColorEvaluator(), 
"#0000FF", "#FF0000");
anim.setDuration(5000);
anim.start();

接下来我们需要将上面一段代码移到MyAnimView类当中,让它和刚才的Point移动动画可以结合到一起播放,这就要借助我们刚才学到的组合动画的技术了。修改MyAnimView中的代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MyAnimView extends View {

...

private void startAnimation() {
Point startPoint = new Point(RADIUS, RADIUS);
Point endPoint = new Point(getWidth() - RADIUS, getHeight() - RADIUS);
ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), startPoint, endPoint);
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
currentPoint = (Point) animation.getAnimatedValue();
invalidate();
}
});
ObjectAnimator anim2 = ObjectAnimator.ofObject(this, "color", new ColorEvaluator(),
"#0000FF", "#FF0000");
AnimatorSet animSet = new AnimatorSet();
animSet.play(anim).with(anim2);
animSet.setDuration(5000);
animSet.start();
}

}

现在重新运行一下代码,效果如下图所示:

Interpolator的用法

Interpolator:差值器。它的主要作用是可以控制动画的变化速率,比如去实现一种非线性运动的动画效果。那么什么叫做非线性运动的动画效果呢?就是说动画改变的速率不是一成不变的,像加速运动以及减速运动都属于非线性运动。

Interpolator并不是属性动画中新增的技术,实际上从Android 1.0版本开始就一直存在Interpolator接口了,而之前的补间动画当然也是支持这个功能的。只不过在属性动画中新增了一个TimeInterpolator接口,这个接口是用于兼容之前的Interpolator的,这使得所有过去的Interpolator实现类都可以直接拿过来放到属性动画当中使用,那么我们来看一下现在TimeInterpolator接口的所有实现类,如下图所示:

TimeInterpolator接口已经有非常多的实现类了,这些都是Android系统内置好的并且我们可以直接使用的Interpolator。每个Interpolator都有它各自的实现效果,比如说AccelerateInterpolator就是一个加速运动的Interpolator,而DecelerateInterpolator就是一个减速运动的Interpolator。

Java类 资源ID 作用
AccelerateInterpolator @android:anim/accelerate_interpolator 加速进行
DecelerateInterpolator @android:anim/decelerate_interpolator 减速进行
LinearInterpolator @android:anim/linear_interpolator 匀速进行
AccelerateDecelerateInterpolator @android:anim/accelerate_decelerate_interpolator 先加速再减速
OvershootInterpolator @android:anim/overshoot_interpolator 快速完成动画,超出再回到结束样式
AnticipateInterpolator @android:anim/anticipate_interpolator 先退后再加速前进
AnticipateOvershootInterpolator @android:anim/anticipate_overshoot_interpolator 先退后再加速前进,超出终点后再回终点
BounceInterpolator @android:anim/bounce_interpolator 最后阶段弹球效果
CycleInterpolator @android:anim/cycle_interpolator 周期运动

使用属性动画时,系统默认的Interpolator其实就是一个先加速后减速的Interpolator,对应的实现类就是AccelerateDecelerateInterpolator

当然,我们也可以很轻松地修改这一默认属性,将它替换成任意一个系统内置好的Interpolator。以上边的代码来举例,MyAnimView中的startAnimation()方法是开启动画效果的入口,这里我们对Point对象的坐标稍做一下修改,让它变成一种垂直掉落的效果,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void startAnimation() {
Point startPoint = new Point(getWidth() / 2, RADIUS);
Point endPoint = new Point(getWidth() / 2, getHeight() - RADIUS);
ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), startPoint, endPoint);
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
currentPoint = (Point) animation.getAnimatedValue();
invalidate();
}
});
anim.setDuration(2000);
anim.start();
}

这里主要是对Point构造函数中的坐标值进行了一下改动,那么现在小球运动的动画效果应该是从屏幕正中央的顶部掉落到底部。但是现在默认情况下小球的下降速度肯定是先加速后减速的,这不符合物理的常识规律,如果把小球视为一个自由落体的话,那么下降的速度应该是越来越快的。我们怎样才能改变这一默认行为呢?其实很简单,调用Animator的setInterpolator()方法就可以了,这个方法要求传入一个实现TimeInterpolator接口的实例,那么比如说我们想要实现小球下降越来越快的效果,就可以使用AccelerateInterpolator,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void startAnimation() {
Point startPoint = new Point(getWidth() / 2, RADIUS);
Point endPoint = new Point(getWidth() / 2, getHeight() - RADIUS);
ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), startPoint, endPoint);
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
currentPoint = (Point) animation.getAnimatedValue();
invalidate();
}
});
anim.setInterpolator(new AccelerateInterpolator(2f));
anim.setDuration(2500);
anim.start();
}

代码很简单,这里调用了setInterpolator()方法,然后传入了一个AccelerateInterpolator的实例,注意AccelerateInterpolator的构建函数可以接收一个float类型的参数,这个参数是用于控制加速度的。现在运行一下代码,效果如下图所示:

效果非常明显,说明我们已经成功替换掉了默认的Interpolator,AccelerateInterpolator确实是生效了。但是现在的动画效果看上去仍然是怪怪的,因为一个小球从很高的地方掉落到地面上直接就静止了,这也是不符合物理规律的,小球撞击到地面之后应该要反弹起来,然后再次落下,接着再反弹起来,又再次落下,以此反复,最后静止。这个功能我们当然可以自己去写,只不过比较复杂,所幸的是,Android系统中已经提供好了这样一种Interpolator,我们只需要简单地替换一下就可以完成上面的描述的效果,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void startAnimation() {
Point startPoint = new Point(getWidth() / 2, RADIUS);
Point endPoint = new Point(getWidth() / 2, getHeight() - RADIUS);
ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), startPoint, endPoint);
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
currentPoint = (Point) animation.getAnimatedValue();
invalidate();
}
});
anim.setInterpolator(new BounceInterpolator());
anim.setDuration(3000);
anim.start();
}

我们只是将设置的Interpolator换成了BounceInterpolator的实例,而BounceInterpolator就是一种可以模拟物理规律,实现反复弹起效果的Interpolator。另外还将整体的动画时间稍微延长了一点,因为小球反复弹起需要比之前更长的时间。现在重新运行一下代码,效果如下图所示:

下面我们就来看一下Interpolator的内部实现机制是什么样的,并且来尝试写一个自定义的Interpolator。

首先看一下TimeInterpolator的接口定义,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* A time interpolator defines the rate of change of an animation. This allows animations
* to have non-linear motion, such as acceleration and deceleration.
*/
public interface TimeInterpolator {

/**
* Maps a value representing the elapsed fraction of an animation to a value that represents
* the interpolated fraction. This interpolated value is then multiplied by the change in
* value of an animation to derive the animated value at the current elapsed animation time.
*
* @param input A value between 0 and 1.0 indicating our current point
* in the animation where 0 represents the start and 1.0 represents
* the end
* @return The interpolation value. This value can be more than 1.0 for
* interpolators which overshoot their targets, or less than 0 for
* interpolators that undershoot their targets.
*/
float getInterpolation(float input);
}

接口还是非常简单的,只有一个getInterpolation()方法。大家有兴趣可以通过注释来对这个接口进行详解的了解,这里我就简单解释一下,getInterpolation()方法中接收一个input参数,这个参数的值会随着动画的运行而不断变化,不过它的变化是非常有规律的,就是根据设定的动画时长匀速增加,变化范围是0到1。也就是说当动画一开始的时候input的值是0,到动画结束的时候input的值是1,而中间的值则是随着动画运行的时长在0到1之间变化的。

说到这个input的值,我觉得有不少朋友可能会联想到我们上边使用过的fraction值。那么这里的input和fraction有什么关系或者区别呢?答案很简单,input的值决定了fraction的值。input的值是由系统经过计算后传入到getInterpolation()方法中的,然后我们可以自己实现getInterpolation()方法中的算法,根据input的值来计算出一个返回值,而这个返回值就是fraction了。

因此,最简单的情况就是input值和fraction值是相同的,这种情况由于input值是匀速增加的,因而fraction的值也是匀速增加的,所以动画的运动情况也是匀速的。系统中内置的LinearInterpolator就是一种匀速运动的Interpolator,那么我们来看一下它的源码是怎么实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 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();
}
}

这里我们只看getInterpolation()方法,这个方法没有任何逻辑,就是把参数中传递的input值直接返回了,因此fraction的值就是等于input的值的,这就是匀速运动的Interpolator的实现方式。

编写自定义Interpolator最主要的难度都是在于数学计算方面的,由于我数学并不是很好,因此这里也就写一个简单点的Interpolator来给大家演示一下。既然属性动画默认的Interpolator是先加速后减速的一种方式,这里我们就对它进行一个简单的修改,让它变成先减速后加速的方式。新建DecelerateAccelerateInterpolator类,让它实现TimeInterpolator接口,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class DecelerateAccelerateInterpolator implements TimeInterpolator{

@Override
public float getInterpolation(float input) {
float result;
if (input <= 0.5) {
result = (float) (Math.sin(Math.PI * input)) / 2;
} else {
result = (float) (2 - Math.sin(Math.PI * input)) / 2;
}
return result;
}

}

这段代码是使用正弦函数来实现先减速后加速的功能的,因为正弦函数初始弧度的变化值非常大,刚好和余弦函数是相反的,而随着弧度的增加,正弦函数的变化值也会逐渐变小,这样也就实现了减速的效果。当弧度大于π/2之后,整个过程相反了过来,现在正弦函数的弧度变化值非常小,渐渐随着弧度继续增加,变化值越来越大,弧度到π时结束,这样从0过度到π,也就实现了先减速后加速的效果。

我们可以将这个算法的执行情况通过曲线图的方式绘制出来,结果如下图所示:

ViewPropertyAnimator的用法

我们知道,属性动画的机制已经不是再针对于View而进行设计的了,而是一种不断地对值进行操作的机制,它可以将值赋值到指定对象的指定属性上。但是,在绝大多数情况下,大家主要都还是对View进行动画操作的。Android开发团队也是意识到了这一点,没有为View的动画操作提供一种更加便捷的用法确实是有点太不人性化了,于是在Android 3.1系统当中补充了ViewPropertyAnimator这个机制。

ViewPropertyAnimator提供了更加易懂、更加面向对象的API,如下所示:

1
textview.animate().alpha(0f);

textview.animate()这个方法是怎么回事呢?animate()方法就是在Android 3.1系统上新增的一个方法,这个方法的返回值是一个ViewPropertyAnimator对象,也就是说拿到这个对象之后我们就可以调用它的各种方法来实现动画效果了,这里我们调用了alpha()方法并转入0,表示将当前的textview变成透明状态。

比起使用ObjectAnimator,ViewPropertyAnimator的用法明显更加简单易懂吧。除此之外,ViewPropertyAnimator还可以很轻松地将多个动画组合到一起,比如我们想要让textview运动到500,500这个坐标点上,就可以这样写:

1
textview.animate().x(500).y(500);

那么怎样去设定动画的运行时长呢?很简单,也是通过连缀的方式设定即可,比如我们想要让动画运行5秒钟,就可以这样写:

1
textview.animate().x(500).y(500).setDuration(5000);

关于ViewPropertyAnimator有几个细节还是值得大家注意一下的:

  • 整个ViewPropertyAnimator的功能都是建立在View类新增的animate()方法之上的,这个方法会创建并返回一个ViewPropertyAnimator的实例,之后的调用的所有方法,设置的所有属性都是通过这个实例完成的。
  • 在使用ViewPropertyAnimator时,我们自始至终没有调用过start()方法,这是因为新的接口中使用了隐式启动动画的功能,只要我们将动画定义完成之后,动画就会自动启动。并且这个机制对于组合动画也同样有效,只要我们不断地连缀新的方法,那么动画就不会立刻执行,等到所有在ViewPropertyAnimator上设置的方法都执行完毕后,动画就会自动启动。当然如果不想使用这一默认机制的话,我们也可以显式地调用start()方法来启动动画。

转场动画&过渡动画

过渡动画是Android 5.0 以后提供同的一套动画框架,那么什么是过渡动画呢?借助官网的解释:使用 过渡布局变化 添加 动画效果

Trasition 内部保存了所有关于动画的信息,当场景变换时动画将在目标对象上执行。Transition基于两个概念:场景(scenes)和变换(transitions),场景是UI当前状态,变换则定义了在不同场景之间动画变化的过程。

Transition分为三种类型(android5.0中使用):

  • 进入动画:Activity中的所有视图进入屏幕的动画
  • 退出动画:Activity中的所有视图退出屏幕的动画
  • 共享元素动画:利用共享的元素实现Activity的跳转动画

进入动画和退出动画合称Content Transition(内容变换动画),所以Transition分为内容变换动画共享元素动画

方法 说明
TransitionSet 组合效果
AutoTransition 默认过渡动画,fade out 渐隐, move 位移 和 resize 大小缩放,fade in 渐显 ,按顺序
ChangeBounds 检测view的位置边界创建移动和缩放动画
ChangeClipBounds 检测view的剪切区域的位置边界,和ChangeBounds类似。不过ChangeBounds针对的是view而ChangeClipBounds针对的是view的剪切区域(setClipBound(Rect rect) 中的rect)。如果没有设置则没有动画效果
ChangeImageTransform 检测ImageView(这里是专指ImageView)的尺寸,位置以及ScaleType,并创建相应动画 一般时候,我们都是和 ChangeBounds 一起使用,能够做出很漂亮的动画
ChangeTransform 检测view的scale和rotation创建缩放和旋转动画
ChangeScroll 改变滑动位置
Explode 分解
Fade 淡入淡出 有 fade_in,fade_out,fade_in_out
Slide 从哪边滑动出,有 left, top, right, bottom, start, end 模式

核心关键类 TransitionManager, TransitionManager.beginDelayedTransition(ViewGroup viewGroup, Transition transition); 作为动画的开始,传入需要做转场动画的父布局或根布局,随后改变 View 的相关属性,比如 setVisible(),便可自动完成转场动画效果。

默认实现的 AutoTransition,内部集成了基础动画:

1
2
3
4
5
6
private void init() {
setOrdering(ORDERING_SEQUENTIAL);
addTransition(new Fade(Fade.OUT)).
addTransition(new ChangeBounds()).
addTransition(new Fade(Fade.IN));
}

Slide、Fade 和 Explode

这三者作为 Visibility 的三个子类,通过控制 view.setVisible() 的方式来达到具体的效果。

Fade,淡出 出场,淡入 入场

Slide,向下离开屏幕出场,向上进入屏幕入场

Explode,四边散开出场,四边汇入入场

同样,可以通过:

1
2
3
4
Fade fade = new Fade();
Slide slide = new Slide();
TransitionSet set = new TransitionSet();
set.addTransition(fade).addTransition(slide).setOrdering(TransitionSet.ORDERING_TOGETHER);

达到组合的效果:

ChangeBounds

ChangeBounds 当 View 的位置或者大小发生变化时触发对应的转场效果。比如:

1
2
3
4
5
6
7
8
9
10
ChangeBounds transition = new ChangeBounds();
transition.setInterpolator(new AnticipateInterpolator());
TransitionManager.beginDelayedTransition(mRoot, transition);
ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) view3.getLayoutParams();
if (layoutParams.leftMargin == 400) {
layoutParams.leftMargin = 50;
} else {
layoutParams.leftMargin = 400;
}
view3.setLayoutParams(layoutParams);

ChangeClipBounds

当调用 view.setClipBounds() 时会触发转场效果:

1
2
3
4
5
6
7
8
9
10
11
12
ChangeClipBounds transition = new ChangeClipBounds();
transition.setInterpolator(new BounceInterpolator());
TransitionManager.beginDelayedTransition(mRoot, transition);
int width = view2.getWidth();
int height = view2.getHeight();
int gap = 140;
Rect rect = new Rect(0, gap, width, height - gap);
if (rect.equals(view2.getClipBounds())) {
view2.setClipBounds(null);
} else {
view2.setClipBounds(rect);
}

ChangeScroll

当调用 view.scrollTo() 会触发转场效果:

1
2
3
4
5
6
7
8
ChangeScroll transition = new ChangeScroll();
transition.setInterpolator(new AnticipateOvershootInterpolator());
TransitionManager.beginDelayedTransition(mRoot, transition);
if (view1.getScrollX() == -100 && view1.getScrollY() == -100) {
view1.scrollTo(0, 0);
} else {
view1.scrollTo(-100, -100);
}

ChangeTransform

这个就厉害了,View 的 translation、scale 和 rotation 发生改变时都会触发:

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
ChangeTransform transition = new ChangeTransform();
transition.setInterpolator(new OvershootInterpolator());
TransitionManager.beginDelayedTransition(mRoot, transition);
if (view1.getTranslationX() == 100 && view1.getTranslationY() == 100) {
view1.setTranslationX(0);
view1.setTranslationY(0);
} else {
view1.setTranslationX(100);
view1.setTranslationY(100);
}
if (view2.getRotationX() == 30f) {
view2.setRotationX(0);
} else {
view2.setRotationX(30);
}
if (view3.getRotationY() == 30f) {
view3.setRotationY(0);
} else {
view3.setRotationY(30);
}
if (view4.getScaleX() == 0.5f && view4.getScaleY() == 0.5f) {
view4.setScaleX(1f);
view4.setScaleY(1f);
} else {
view4.setScaleX(0.5f);
view4.setScaleY(0.5f);
}

使用场景动画做转场

在使用 Transition 的 activity 中需要启用 transition ,可以通过代码也可以通过设置主题:

代码方式,在setContentView之前调用

1
2
3
4
5
6
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 启用窗口内容过渡 要在 setContentView 之前设置
window.requestFeature(Window.FEATURE_CONTENT_TRANSITIONS)
setContentView(R.layout.activity_first)
}

主题xml方式:

1
2
3
4
<style name="BaseAppTheme" parent="android:Theme.Material">
<!-- enable window content transitions -->
<item name="android:windowActivityTransitions">true</item>
</style>

Fragment中使用Transition,和activity中使用大体相同,不同点如下:

  • Fragment在其FragmentTransaction执行added, removed, attached, detached, shown,hidden时触发动画
  • 在Fragment commit之前,共享元素需要通过调用addSharedElement(View, String) 方法来成为FragmentTransaction的一部分

Activity/Fragment 转场时候,界面其实是可以分成4个动画:

对应的方法为:

  • Window.setEnterTransition()
  • Window.setExitTransition()
  • Window.setReenterTransition()
  • Window.setReturnTransition()

Content Transition

Content transition决定了非共享view元素在activity和fragment切换期间是如何进入或者退出场景的,可以利用代码或者XML方式实现。

设置Content Transition的函数:

setEnterTransition() - ActivityA 跳转到 ActivityB,ActivityB中的View进入场景的transition。

setExitTransition() - ActivityA 跳转到 ActivityB,ActivityA中的View退出场景的transition。

setReturnTransition() - 从ActivityB 返回Activity A时,ActivityB中的View退出场景的transition。

setReenterTransition() - 从ActivityB 返回Activity A时,ActivityA中的View进入场景的transition

xml中的属性:

1
2
3
4
5
6
7
8
//对应setEnterTransition
<item name="android:windowEnterTransition"></item>
//对应setExitTransition
<item name="android:windowExitTransition"></item>
//对应setReturnTransition
<item name="android:windowReturnTransition"></item>
//对应setReenterTransition
<item name="android:windowReenterTransition"></item>

系统提供的动画效果(用于content Transition)

explode(分解):从屏幕中间进或出,移动视图。

slide(滑动):从屏幕边缘进或出,移动试图。

fade(淡出):通过改变屏幕上的视图的不透明度,达到添加或移除视图。

三种效果都有对应的实现类,分别为:

分解:Explode

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
<explode
android:duration="500"
android:interpolator="@android:interpolator/accelerate_decelerate"/>
</transitionSet>

滑动:Slide,setSlideEdge(int slideEdge) 设置从哪个边出现或隐藏,取值为
Gravity.LEFT, Gravity.TOP, Gravity.RIGHT, Gravity.BOTTOM, Gravity.START, Gravity.END。对应的xml中的属性为android:slideEdge

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
<slide
android:duration="500"
android:slideEdge="bottom"
android:interpolator="@android:interpolator/accelerate_decelerate" />
</transitionSet>

淡入淡出:Fade

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
<fade
android:duration="500"
android:interpolator="@android:interpolator/accelerate_decelerate"/>
</transitionSet>

xml中实现进入退出动画

Style中设置:

1
2
3
4
5
<item name="android:windowContentTransitions">true</item>
<item name="android:windowEnterTransition">@transition/explode</item>
<item name="android:windowExitTransition">@transition/fade</item>
<item name="android:windowReenterTransition">@transition/solid</item>
<item name="android:windowReturnTransition">@transition/explode</item>

调用:

1
2
3
ActivityOptionsCompat activityOptionsCompat = ActivityOptionsCompat.makeSceneTransitionAnimation(Main8Activity.this);
Intent intent = new Intent(Main8Activity.this, Main7Activity.class);
startActivity(intent,activityOptionsCompat.toBundle());

退出时调用:

1
2
3
4
5
@Override
public void onBackPressed() {
super.onBackPressed();
ActivityCompat.finishAfterTransition(this);
}

开始一个activity的content transition需要调用startActivity(Context, Bundle)方法,Bundle参数生成方法ActivityOptions.makeSceneTransitionAnimation(activity, pairs).toBundle()

在代码中触发通过**finishAfterTransition()**方法触发返回动画,而不是调用finish()方法

代码方式实现

1
2
3
4
5
6
7
8
9
10
11
@Override
protected void onCreate(Bundle savedInstanceState) {
getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS);
getWindow().setEnterTransition(new Explode());
getWindow().setExitTransition(new Fade());
Transition transition1 = TransitionInflater.from(Main8Activity.this).inflateTransition(android.R.transition.slide_bottom);
getWindow().setReenterTransition(transition1);
Transition transition2 = TransitionInflater.from(Main8Activity.this).inflateTransition(R.transition.explode);
getWindow().setReturnTransition(transition2);
//super.onCreate之前
super.onCreate(savedInstanceState);

如果从ActivityA启动ActivityB,界面切换的时候,ActivityA退出还没有完全展示退出动画时,ActivityB就进入了,如果想等ActivityA完全退出后ActivityB再进入,可以通过设置setAllowEnterTransitionOverlap(false)(默认是true),同样可以在xml中设置:

1
2
<item name="android:windowAllowEnterTransitionOverlap">false</item>
<item name="android:windowAllowReturnTransitionOverlap">false</item>

共享元素动画

共享元素动画需要对共享的元素上添加 android:transitionName 来设置transitionName ,它的值随意,但两个共享的元素这个值要保持一致。

1
2
3
4
5
6
7
8
9
10
11
12
ActivityA
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/hongbao"
android:transitionName="shareElement"/>
ActivityB
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/hongbao"
android:transitionName="shareElement"/>

利用startActivity(Intent intent, Bundle options) 启动Activity:

利用ActivityOptionsCompat的makeSceneTransitionAnimation函数生成。

1
2
3
Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(MainActivity.this,view,"shareElement").toBundle();
Intent intent = new Intent(MainActivity.this, MainActivity2.class);
startActivity(intent,bundle);

共享多个元素:

makeSceneTransitionAnimation函数还有一个重载函数,传入多个Pair对象

ActivityOptionsCompat makeSceneTransitionAnimation(@NonNull Activity activity, Pair… sharedElements)

1
2
3
4
5
Pair pair1 = new Pair(gongxiang,"shareElement");
Pair pair2 = new Pair(gongxiang2,"shareElement2");
Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(Main8Activity.this,pair1,pair2).toBundle();
Intent intent = new Intent(Main8Activity.this, Main7Activity.class);
startActivity(intent,bundle);

封装类:

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
/**
* 转场动画
*
* @param context activity
* @param intent intent
* @param sharedElement 共享view
* @param sharedElementName 共享view的name,和目标activity的目标view要有相同的sharedElementName
*/
public static void makeSceneTransitionAnimation(Context context, Intent intent,
View sharedElement, String sharedElementName) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
sharedElement.setTransitionName(sharedElementName);
ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation((Activity) context,
sharedElement, sharedElementName);
ActivityCompat.startActivity(context, intent, options.toBundle());
} else {
context.startActivity(intent);
}
}

/**
* 转场动画
*
* @param context activity
* @param intent intent
* @param sharedElements 共享view集
*/
public static void makeSceneTransitionAnimationPair(Context context, Intent intent,
Pair<View, String>... sharedElements) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation(
(Activity) context, sharedElements);
ActivityCompat.startActivity(context, intent, options.toBundle());
} else {
context.startActivity(intent);
}
}

调用:

1
2
3
4
public static void launchActivity(@NonNull Context context, View view) {
Intent intent = new Intent(context, UserSignActivity.class);
Utils.makeSceneTransitionAnimation(context, intent, view, context.getString(R.string.transition_mine_img));
}

5.0 以前设置转场动画

使用 overridePendingTransition 实现转场动画。

入场设置:

1
2
3
4
startActivity(intent)
//参数1 进场动画 (B Activity 出现的动画)
//参数2 退场动画 (A Activity 消失的动画)
overridePendingTransition(R.anim.enter_in, R.anim.enter_out)

退场设置:

1
2
3
4
finish()
//参数1 进场动画 (A Activity 出现的动画)
//参数2 退场动画 (B Activity 消失的动画)
overridePendingTransition(R.anim.exit_in, R.anim.exit_out)

其它动画

Lottie

Lottie 是 Airbnb 开发的一款能够为原生应用添加动画效果的开源工具。Lottie 目前提供了 iOS, Android, 和 React Native 版本,能够实时渲染 After Effects 动画特效。

Android : https://github.com/airbnb/lottie-android

1
2
// lottie
implementation "com.airbnb.android:lottie:3.4.1"

src/main/assets 存放 json 文件。

1
2
3
4
5
lottieAnimationView = findViewById(R.id.lottieAnimationView);
lottieAnimationView.setImageAssetsFolder("images");
lottieAnimationView.setAnimation("data.json");
lottieAnimationView.loop(true);
lottieAnimationView.playAnimation();
1
2
3
4
5
6
7
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lottieAnimationView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:lottie_fileName="hello-world.json"
app:lottie_loop="true"
app:lottie_autoPlay="true" />

监听动画进度 [0,1]

1
2
3
4
5
6
7
8
9
10
lottieAnimationView.addAnimatorUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
// 判断动画加载结束
if (valueAnimator.getAnimatedFraction() == 1f) {
if (dialog.isShowing() && getActivity() != null)
dialog.dismiss();
}
}
});

暂停/取消/播放

1
2
3
lottieAnimationView.pauseAnimation();
lottieAnimationView.cancelAnimation();
lottieAnimationView.playAnimation();

SpringAnimation

SpringAnimation详解-Android

ViewPager切换动画

ViewPager有个方法叫做:setPageTransformer(boolean reverseDrawingOrder, PageTransformer transformer) 用于设置ViewPager切换时的动画效果,并且google官方还给出了两个示例:DepthPageTransformer 和 ZoomOutPageTransformer。

DepthPageTransformer:

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
public class DepthPageTransformer implements ViewPager.PageTransformer {
private static final float MIN_SCALE = 0.75f;

public void transformPage(View view, float position) {
int pageWidth = view.getWidth();

if (position < -1) { // [-Infinity,-1)
// This page is way off-screen to the left.
view.setAlpha(0);

} else if (position <= 0) { // [-1,0]
// Use the default slide transition when moving to the left page
view.setAlpha(1);
view.setTranslationX(0);
view.setScaleX(1);
view.setScaleY(1);

} else if (position <= 1) { // (0,1]
// Fade the page out.
view.setAlpha(1 - position);

// Counteract the default slide transition
view.setTranslationX(pageWidth * -position);

// Scale the page down (between MIN_SCALE and 1)
float scaleFactor = MIN_SCALE
+ (1 - MIN_SCALE) * (1 - Math.abs(position));
view.setScaleX(scaleFactor);
view.setScaleY(scaleFactor);

} else { // (1,+Infinity]
// This page is way off-screen to the right.
view.setAlpha(0);
}
}
}

调用代码:

1
mViewPager.setPageTransformer(true, new DepthPageTransformer());

自定义PageTransformer。

1
2
3
4
5
6
7
8
9
10
11
public interface PageTransformer {
/**
* Apply a property transformation to the given page.
*
* @param page Apply the transformation to this page
* @param position Position of page relative to the current front-and-center
* position of the pager. 0 is front and center. 1 is one full
* page position to the right, and -1 is one page position to the left.
*/
public void transformPage(View page, float position);
}

直播间左滑切出页面的PageTransformer,原页面原地不动:

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
public class LivePagerTransformer implements ViewPager.PageTransformer {

@Override
public void transformPage(@NonNull View view, float position) {
onTransform(view, position);
}

protected void onTransform(@NonNull View view, float position) {
Context context = view.getContext();
if (context == null) {
return;
}
String tag = "";
if (view.getTag() != null) {
tag = view.getTag().toString();
}
if (BuildConfig.DEBUG) {
LogUtil.i("LivePagerTransformer", "onTransform-->>view:" + tag);
}

if (tag.equals(context.getString(R.string.live_room_tag))) {
float scale = position < 0 ? position + 1f : Math.abs(1f - position);
if (BuildConfig.DEBUG) {
LogUtil.i("LivePagerTransformer", "onTransform-->>scale:" + scale);
}
float width = view.getWidth();
float translationX = Math.abs(width - width * scale);
if (BuildConfig.DEBUG) {
LogUtil.i("LivePagerTransformer", "onTransform-->>translationX:" + translationX);
}
view.setTranslationX(translationX);
}
}
}

Android 实现个性的ViewPager切换动画 实战PageTransformer

其它

动画篇(一)——alpha、scale、translate、rotate、set的xml属性及用法

动画 | Android 补间动画 浅析

Android补间动画原理分析

Android 动画总结

PropertyValuesHolder与Keyframe

Android 转场动画

优雅的转场动画之 Transition

Lottie - 让复杂动画如此简单