Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

属性动画 #19

Open
cnwutianhao opened this issue Feb 29, 2024 · 0 comments
Open

属性动画 #19

cnwutianhao opened this issue Feb 29, 2024 · 0 comments
Labels

Comments

@cnwutianhao
Copy link
Owner

在属性动画出现之前,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所示:

图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所示:

图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所示:

图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所示:

图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所示:

图5

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant