在属性动画出现之前,Android 系统提供的动画只有帧动画和 View 动画。View 动画我们都了解,它提供了 AlphaAnimation、RotateAnimation、TranslateAnimation、ScaleAnimation 这4种动画方式,并提供了 AnimationSet 动画集合来混合使用多种动画。随着属性动画的推出,View 动画不再风光。
相比属性动画,View 动画一个非常大的缺陷突显,其不具有交互性。当某个元素发生 View 动画后,其响应事件的位置依然在动画进行前的地方,所以 View 动画只能做普通的动画效果,要避免涉及交互操作。但是它的优点也非常明显:效率比较高,使用也方便。由于之前已有的动画框架 Animation 存在一些局限性,也就是动画改变的只是显示,但 View 的位置没有发生变化,View 移动后并不能响应事件,所以谷歌推出了新的动画框架,帮助开发者实现更加丰富的动画效果。在 Animator 框架中使用最多的就是 AnimatorSet 和 ObjectAnimator,配合使用 ObjectAnimator 进行更精细化的控制,控制一个对象和一个属性值,而使用多个 ObjectAnimator 组合到 AnimatorSet 形成一个动画。属性动画通过调用属性 get、set 方法来真实地控制一个 View 的属性值,因此,强大的属性动画框架基本可以实现所有的动画效果。
一、ObjectAnimator
ObjectAnimator 是属性动画最重要的类,创建一个 ObjectAnimator 只需通过其静态工厂类直接返还一个 ObjectAnimator 对象。参数包括一个对象和对象的属性名字,但这个属性必须有 get 和 set 方法,其内部会通过 Java 反射机制来调用 set 方法修改对象的属性值。下面看看平移动画是如何实现的,代码如下所示:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<View
android:id="@+id/test_view"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@android:color/holo_red_light"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val testView = findViewById<View>(R.id.test_view)
val objectAnimator = ObjectAnimator.ofFloat(testView, "translationX", 200F)
objectAnimator.setDuration(3000)
objectAnimator.start()
}
}
运行程序,效果如图1所示:
通过 ObjectAnimator 的静态方法,创建一个 ObjectAnimator 对象,查看 ObjectAnimator 的静态方法 ofFloat(),源码如下所示:
public static ObjectAnimator ofFloat(Object target, String propertyName, float... values) {
ObjectAnimator anim = new ObjectAnimator(target, propertyName);
anim.setFloatValues(values);
return anim;
}
从源码可以看出第一个参数是要操作的 Object;第二个参数是要操作的属性;最后一个参数是一个可变的 float 类型数组,需要传进去该属性变化的取值过程,这里设置了一个参数,变化到200。与 View 动画一样,也可以给属性动画设置显示时长、插值器等属性。下面就是一些常用的可以直接使用的属性动画的属性值。
- translationX 和 translationY:用来沿着 X 轴或者 Y 轴进行平移。
- rotation、rotationX、rotationY:用来围绕 View 的支点进行旋转。
- PrivotX 和 PrivotY:控制 View 对象的支点位置,围绕这个支点进行旋转和缩放变换处理。默认该支点位置就是 View 对象的中心点。
- alpha:透明度,默认是1(不透明),0代表完全透明。
- x 和 y:描述 View 对象在其容器中的最终位置。
需要注意的是,在使用 ObjectAnimator 的时候,要操作的属性必须要有 get 和 set 方法,不然 ObjectAnimator 就无法生效。如果一个属性没有 get、set 方法,也可以通过自定义一个属性类或包装类来间接地给这个属性增加 get 和 set 方法。现在来看看如何通过包装类的方法给一个属性增加 get 和 set 方法,代码如下所示:
class MyView(private val view: View) {
fun getWidth(): Int {
return view.layoutParams.width
}
fun setWidth(width: Int) {
view.layoutParams.width = width
view.requestLayout()
}
}
使用时只需要操作包类就可以调用 get、set 方法了:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val testView = findViewById<View>(R.id.test_view)
val myView = MyView(testView)
ObjectAnimator.ofInt(myView, "width", 500).setDuration(3000).start()
}
}
运行程序,效果如图2所示:
二、ValueAnimator
ValueAnimator 不提供任何动画效果,它更像一个数值发生器,用来产生有一定规律的数字,从而让调用者控制动画的实现过程。通常情况下,在 ValueAnimator 的 AnimatorUpdateListener 中监听数值的变化,从而完成动画的变换,代码如下所示:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val testView = findViewById<View>(R.id.test_view)
val valueAnimator = ValueAnimator.ofFloat(0F, 100F)
valueAnimator.setTarget(testView)
valueAnimator.setDuration(3000).start()
valueAnimator.addUpdateListener { animation ->
val animatedValue = animation.animatedValue
}
}
}
三、动画的监听
完整的动画具有 start、Repeat、End、Cancel 这4个过程,代码如下所示:
val animator = ObjectAnimator.ofFloat(testView, "alpha", 1.5F)
animator.addListener(object : AnimatorListener {
override fun onAnimationStart(animation: Animator) {
}
override fun onAnimationEnd(animation: Animator) {
}
override fun onAnimationCancel(animation: Animator) {
}
override fun onAnimationRepeat(animation: Animator) {
}
})
大部分时候我们只关心 onAnimationEnd 事件,Android 也提供了 AnimatorListenterAdaper 来让我们选择必要的事件进行监听。
val animator = ObjectAnimator.ofFloat(testView, "alpha", 1.5F)
animator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
}
})
四、组合动画(AnimatorSet)
AnimatorSet 类提供了一个 play() 方法,如果我们向这个方法中传入一个 Animator 对象(ValueAnimator 或 ObjectAnimator),将会返回一个 AnimatorSet.Builder 的实例。AnimatorSet 的 play() 方法源码如下所示:
public Builder play(Animator anim) {
if (anim != null) {
return new Builder(anim);
}
return null;
}
很明显,在 play() 方法中创建了一个 AnimatorSet.Builder 类,这个 Builder 类是 AnimatorSet 的内部类。我们来看看这个 Builder 类中有什么,代码如下所示:
public class Builder {
private Node mCurrentNode;
Builder(Animator anim) {
mDependencyDirty = true;
mCurrentNode = getNodeForAnimation(anim);
}
public Builder with(Animator anim) {
Node node = getNodeForAnimation(anim);
mCurrentNode.addSibling(node);
return this;
}
public Builder before(Animator anim) {
Node node = getNodeForAnimation(anim);
mCurrentNode.addChild(node);
return this;
}
public Builder after(Animator anim) {
Node node = getNodeForAnimation(anim);
mCurrentNode.addParent(node);
return this;
}
public Builder after(long delay) {
ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
anim.setDuration(delay);
after(anim);
return this;
}
}
从源码中可以看出,Builder 类采用了建造者模式,每次调用方法时都返回 Builder 自身用于继续构建。AnimatorSet.Builder 中包括以下4个方法:
- with(Animator anim):将现有动画和传入的动画同时执行。
- before(Animator anim):将现有动画插入到传入的动画之前执行。
- after(Animator anim):将现有动画插入到传入的动画之后执行。
- after(long delay):将现有动画延迟指定毫秒后执行。
AnimatorSet 正是通过这几种方法来控制动画播放顺序的。这里再举一个例子,代码如下所示:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val testView = findViewById<View>(R.id.test_view)
val animator1 = ObjectAnimator.ofFloat(testView, "translationX", 0.0F, 200.0F, 0F)
val animator2 = ObjectAnimator.ofFloat(testView, "scaleX", 1.0F, 2.0F)
val animator3 = ObjectAnimator.ofFloat(testView, "rotationX", 0.0F, 90.0F, 0.0F)
val set = AnimatorSet()
set.setDuration(3000)
set.play(animator1).with(animator2).after(animator3)
set.start()
}
}
首先我们创建3个 ObjectAnimator,分别是 animator1、animator2 和 animator3,然后创建 AnimatorSet。在这里先执行 animator3,然后同时执行 animator1 和 animator2(也可以调用 set.playTogether(animator1,animator2) 来使这两种动画同时执行)。
运行程序,效果如图3所示:
五、组合动画(PropertyValuesHolder)
除了上面的 AnimatorSet 类,还可以使用 PropertyValuesHolder 类来实现组合动画。不过这个组合动画就没有上面的丰富了,使用 PropertyValuesHolder 类只能是多个动画一起执行。当然我们得结合 ObjectAnimator.ofPropertyValuesHolder(Object target,PropertyValuesHolder…values) 方法来使用。其第一个参数是动画的目标对象;之后的参数是 PropertyValuesHolder 类的实例,可以有多个这样的实例。具体代码如下所示:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val testView = findViewById<View>(R.id.test_view)
val valuesHolder1 = PropertyValuesHolder.ofFloat("scaleX", 1.0F, 1.5F)
val valuesHolder2 = PropertyValuesHolder.ofFloat("rotationX", 0.0F, 90.0F, 0.0F)
val valuesHolder3 = PropertyValuesHolder.ofFloat("alpha", 1.0F, 0.3F, 1.0F)
val objectAnimator = ObjectAnimator.ofPropertyValuesHolder(
testView,
valuesHolder1,
valuesHolder2,
valuesHolder3
)
objectAnimator.setDuration(3000).start()
}
}
运行程序,效果如图4所示:
六、在 xml 中使用属性动画
和 View 动画一样,属性动画也可以直接写在 xml 中。在 res 文件中新建 animator 文件夹,在里面新建一个 scale.xml,其内容如下所示:
<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:duration="3000"
android:propertyName="scaleX"
android:valueFrom="1.0"
android:valueTo="2.0"
android:valueType="floatType" />
在程序中引用 xml 定义的属性动画也很简单,代码如下所示:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val testView = findViewById<View>(R.id.test_view)
val animator = AnimatorInflater.loadAnimator(this, R.animator.scale)
animator.setTarget(testView)
animator.start()
}
}
运行程序,效果如图5所示: