使用弹簧物理学动画运动

尝试 Compose 方式
Jetpack Compose 是 Android 推荐的 UI 工具包。了解如何在 Compose 中使用动画。

基于物理的运动 由力驱动。弹簧力是引导交互性和运动的一种力。弹簧力具有以下属性:阻尼和刚度。在基于弹簧的动画中,值和速度根据应用于每个帧的弹簧力计算得出。

如果您希望应用程序的动画仅在一个方向上减速,请考虑使用基于摩擦的 抛射动画

弹簧动画的生命周期

在基于弹簧的动画中,SpringForce 类允许您自定义弹簧的刚度、阻尼比和最终位置。动画开始后,弹簧力会在每个帧上更新动画值和速度。动画会持续进行,直到弹簧力达到平衡。

例如,如果您在屏幕上拖动应用程序图标,然后抬起手指从图标上抬起手指将其释放,则图标会通过看不见但熟悉的力拖回到其原始位置。

图 1 展示了类似的弹簧效果。圆圈中间的加号 (+) 符号表示通过触摸手势施加的力。

Spring release
图 1. 弹簧释放效果

构建弹簧动画

为您的应用程序构建弹簧动画的一般步骤如下

以下部分详细讨论了构建弹簧动画的一般步骤。

添加支持库

要使用基于物理的支持库,您必须将支持库添加到您的项目中,如下所示

  1. 打开您的应用模块的 build.gradle 文件。
  2. 将支持库添加到 dependencies 部分。

    Groovy

            dependencies {
                def dynamicanimation_version = '1.0.0'
                implementation "androidx.dynamicanimation:dynamicanimation:$dynamicanimation_version"
            }
            

    Kotlin

            dependencies {
                val dynamicanimation_version = "1.0.0"
                implementation("androidx.dynamicanimation:dynamicanimation:$dynamicanimation_version")
            }
            

    要查看此库的当前版本,请参阅 版本 页面上的有关 Dynamicanimation 的信息。

创建弹簧动画

SpringAnimation 类允许您为对象创建弹簧动画。要构建弹簧动画,您需要创建一个 SpringAnimation 类的实例,并提供一个对象、要动画化的对象的属性以及一个可选的最终弹簧位置,您希望动画在该位置静止。

注意:在创建弹簧动画时,弹簧的最终位置是可选的。但是,它必须在启动动画之前定义。

Kotlin

val springAnim = findViewById<View>(R.id.imageView).let { img ->
    // Setting up a spring animation to animate the view’s translationY property with the final
    // spring position at 0.
    SpringAnimation(img, DynamicAnimation.TRANSLATION_Y, 0f)
}

Java

final View img = findViewById(R.id.imageView);
// Setting up a spring animation to animate the view’s translationY property with the final
// spring position at 0.
final SpringAnimation springAnim = new SpringAnimation(img, DynamicAnimation.TRANSLATION_Y, 0);

基于弹簧的动画可以通过更改视图对象中的实际属性来动画化屏幕上的视图。系统中提供了以下视图

  • ALPHA:表示视图上的 Alpha 透明度。默认情况下,该值为 1(不透明),值为 0 表示完全透明(不可见)。
  • TRANSLATION_XTRANSLATION_YTRANSLATION_Z:这些属性控制视图的位置,作为其布局容器设置的左坐标、上坐标和高度的增量。
  • ROTATIONROTATION_XROTATION_Y:这些属性控制围绕枢轴点的二维 (rotation 属性) 和三维旋转。
  • SCROLL_XSCROLL_Y:这些属性指示源左边缘和上边缘的滚动偏移量(以像素为单位)。它还指示页面滚动程度的位置。
  • SCALE_XSCALE_Y:这些属性控制视图围绕其枢轴点的二维缩放。
  • XYZ:这些是用于描述视图在其容器中的最终位置的基本实用程序属性。

注册监听器

DynamicAnimation 类提供了两个监听器:OnAnimationUpdateListenerOnAnimationEndListener。这些监听器会监听动画的更新,例如动画值发生更改时以及动画结束时。

OnAnimationUpdateListener

当您要动画化多个视图以创建链式动画时,您可以设置 OnAnimationUpdateListener 以在当前视图的属性发生更改时接收回调。回调会通知其他视图根据当前视图属性中发生的更改更新其弹簧位置。要注册监听器,请执行以下步骤

  1. 调用 addUpdateListener() 方法并将监听器附加到动画。

    注意:您需要在动画开始之前注册更新监听器。但是,仅当您需要动画值更改的每帧更新时,才应注册更新监听器。更新监听器会阻止动画可能在单独的线程上运行。

  2. 覆盖 onAnimationUpdate() 方法,通知调用者当前对象的更改。以下示例代码说明了 OnAnimationUpdateListener 的整体使用。

Kotlin

// Setting up a spring animation to animate the view1 and view2 translationX and translationY properties
val (anim1X, anim1Y) = findViewById<View>(R.id.view1).let { view1 ->
    SpringAnimation(view1, DynamicAnimation.TRANSLATION_X) to
            SpringAnimation(view1, DynamicAnimation.TRANSLATION_Y)
}
val (anim2X, anim2Y) = findViewById<View>(R.id.view2).let { view2 ->
    SpringAnimation(view2, DynamicAnimation.TRANSLATION_X) to
            SpringAnimation(view2, DynamicAnimation.TRANSLATION_Y)
}

// Registering the update listener
anim1X.addUpdateListener { _, value, _ ->
    // Overriding the method to notify view2 about the change in the view1’s property.
    anim2X.animateToFinalPosition(value)
}

anim1Y.addUpdateListener { _, value, _ -> anim2Y.animateToFinalPosition(value) }

Java

// Creating two views to demonstrate the registration of the update listener.
final View view1 = findViewById(R.id.view1);
final View view2 = findViewById(R.id.view2);

// Setting up a spring animation to animate the view1 and view2 translationX and translationY properties
final SpringAnimation anim1X = new SpringAnimation(view1,
        DynamicAnimation.TRANSLATION_X);
final SpringAnimation anim1Y = new SpringAnimation(view1,
    DynamicAnimation.TRANSLATION_Y);
final SpringAnimation anim2X = new SpringAnimation(view2,
        DynamicAnimation.TRANSLATION_X);
final SpringAnimation anim2Y = new SpringAnimation(view2,
        DynamicAnimation.TRANSLATION_Y);

// Registering the update listener
anim1X.addUpdateListener(new DynamicAnimation.OnAnimationUpdateListener() {

// Overriding the method to notify view2 about the change in the view1’s property.
    @Override
    public void onAnimationUpdate(DynamicAnimation dynamicAnimation, float value,
                                  float velocity) {
        anim2X.animateToFinalPosition(value);
    }
});

anim1Y.addUpdateListener(new DynamicAnimation.OnAnimationUpdateListener() {

  @Override
    public void onAnimationUpdate(DynamicAnimation dynamicAnimation, float value,
                                  float velocity) {
        anim2Y.animateToFinalPosition(value);
    }
});

OnAnimationEndListener

OnAnimationEndListener 通知动画结束。您可以设置监听器,以便在动画达到平衡或被取消时接收回调。要注册监听器,请执行以下步骤

  1. 调用 addEndListener() 方法并将监听器附加到动画。
  2. 覆盖 onAnimationEnd() 方法,以便在动画达到平衡或被取消时接收通知。

移除监听器

要停止接收动画更新回调和动画结束回调,请分别调用 removeUpdateListener()removeEndListener() 方法。

设置动画开始值

要设置动画的开始值,请调用 setStartValue() 方法并传递动画的开始值。如果您没有设置开始值,动画将使用对象的属性的当前值作为开始值。

设置动画值范围

当您要将属性值限制在特定范围内时,您可以设置动画的最小值和最大值。如果您要动画化具有内在范围的属性(例如,alpha(从 0 到 1)),它也有助于控制范围。

  • 要设置最小值,请调用 setMinValue() 方法并传递属性的最小值。
  • 要设置最大值,请调用 setMaxValue() 方法并传递属性的最大值。

这两种方法都返回正在设置值的动画。

注意:如果您已设置开始值并定义了动画值范围,请确保开始值在最小值和最大值范围内。

设置开始速度

开始速度定义了动画开始时动画属性更改的速度。默认开始速度设置为每秒零像素。您可以使用触摸手势的速度或使用固定值作为开始速度来设置速度。如果您选择提供固定值,我们建议您以每秒 dp 为单位定义该值,然后将其转换为每秒像素。以每秒 dp 为单位定义值可以使速度独立于密度和外形尺寸。有关将值转换为每秒像素的更多信息,请参阅将每秒 dp 转换为每秒像素部分。

要设置速度,请调用 setStartVelocity() 方法并传递每秒像素的速度。该方法返回设置了速度的弹簧力对象。

注意:使用 GestureDetector.OnGestureListenerVelocityTracker 类方法检索和计算触摸手势的速度。

Kotlin

findViewById<View>(R.id.imageView).also { img ->
    SpringAnimation(img, DynamicAnimation.TRANSLATION_Y).apply {
        …
        // Compute velocity in the unit pixel/second
        vt.computeCurrentVelocity(1000)
        val velocity = vt.yVelocity
        setStartVelocity(velocity)
    }
}

Java

final View img = findViewById(R.id.imageView);
final SpringAnimation anim = new SpringAnimation(img, DynamicAnimation.TRANSLATION_Y);
…
// Compute velocity in the unit pixel/second
vt.computeCurrentVelocity(1000);
float velocity = vt.getYVelocity();
anim.setStartVelocity(velocity);

将每秒 dp 转换为每秒像素

弹簧的速度必须以每秒像素为单位。如果您选择提供固定值作为速度的开始,请以每秒 dp 为单位提供该值,然后将其转换为每秒像素。对于转换,使用 applyDimension() 方法,该方法来自 TypedValue 类。请参考以下示例代码

Kotlin

val pixelPerSecond: Float =
    TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpPerSecond, resources.displayMetrics)

Java

float pixelPerSecond = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpPerSecond, getResources().getDisplayMetrics());

设置弹簧属性

SpringForce 类定义了每个弹簧属性的 getter 和 setter 方法,例如阻尼比和刚度。要设置弹簧属性,重要的是要检索弹簧力对象或创建一个自定义弹簧力,您可以在其上设置属性。有关创建自定义弹簧力的更多信息,请参阅创建自定义弹簧力部分。

提示:在使用 setter 方法时,您可以创建一个方法链,因为所有 setter 方法都返回弹簧力对象。

阻尼比

阻尼比描述了弹簧振荡的逐渐衰减。通过使用阻尼比,您可以定义振荡从一个反弹到下一个反弹衰减的速度。有四种不同的方法可以阻尼弹簧

  • 过阻尼发生在阻尼比大于 1 时。它可以让物体平稳地返回静止位置。
  • 临界阻尼发生在阻尼比等于 1 时。它可以让物体在最短时间内返回静止位置。
  • 欠阻尼发生在阻尼比小于 1 时。它可以让物体多次越过静止位置而超调,然后逐渐达到静止位置。
  • 无阻尼发生在阻尼比等于 0 时。它可以让物体永远振荡。

要将阻尼比添加到弹簧,请执行以下步骤

  1. 调用 getSpring() 方法检索要添加阻尼比的弹簧。
  2. 调用 setDampingRatio() 方法并传递要添加到弹簧的阻尼比。该方法返回设置了阻尼比的弹簧力对象。

    注意:阻尼比必须是非负数。如果您将阻尼比设置为零,弹簧将永远无法达到静止位置。换句话说,它会永远振荡。

系统中提供了以下阻尼比常量

图 2:高弹跳

图 3:中等弹跳

图 4:低弹跳

图 5:无弹跳

默认阻尼比设置为 DAMPING_RATIO_MEDIUM_BOUNCY

Kotlin

findViewById<View>(R.id.imageView).also { img ->
    SpringAnimation(img, DynamicAnimation.TRANSLATION_Y).apply {
        …
        // Setting the damping ratio to create a low bouncing effect.
        spring.dampingRatio = SpringForce.DAMPING_RATIO_LOW_BOUNCY
        …
    }
}

Java

final View img = findViewById(R.id.imageView);
final SpringAnimation anim = new SpringAnimation(img, DynamicAnimation.TRANSLATION_Y);
…
// Setting the damping ratio to create a low bouncing effect.
anim.getSpring().setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY);
…

刚度

刚度定义了弹簧常数,它衡量弹簧的强度。当弹簧没有处于静止位置时,一个刚性的弹簧会对附着在弹簧上的物体施加更大的力。要将刚度添加到弹簧,请执行以下步骤

  1. 调用 getSpring() 方法检索要添加刚度的弹簧。
  2. 调用 setStiffness() 方法并传递要添加到弹簧的刚度值。该方法返回设置了刚度的弹簧力对象。

    注意:刚度必须是正数。

系统中提供了以下刚度常量

图 6:高刚度

图 7:中等刚度

图 8:低刚度

图 9:非常低的刚度

默认刚度设置为 STIFFNESS_MEDIUM

Kotlin

findViewById<View>(R.id.imageView).also { img ->
    SpringAnimation(img, DynamicAnimation.TRANSLATION_Y).apply {
        …
        // Setting the spring with a low stiffness.
        spring.stiffness = SpringForce.STIFFNESS_LOW
        …
    }
}

Java

final View img = findViewById(R.id.imageView);
final SpringAnimation anim = new SpringAnimation(img, DynamicAnimation.TRANSLATION_Y);
…
// Setting the spring with a low stiffness.
anim.getSpring().setStiffness(SpringForce.STIFFNESS_LOW);
…

创建自定义弹簧力

您可以创建自定义弹簧力,作为使用默认弹簧力的替代方法。自定义弹簧力可以让您在多个弹簧动画之间共享相同的弹簧力实例。创建弹簧力后,您可以设置诸如阻尼比和刚度之类的属性。

  1. 创建一个 SpringForce 对象。

    SpringForce force = new SpringForce();

  2. 通过调用相应的方法分配属性。您还可以创建一个方法链。

    force.setDampingRatio(DAMPING_RATIO_LOW_BOUNCY).setStiffness(STIFFNESS_LOW);

  3. 调用 setSpring() 方法将弹簧设置为动画。

    setSpring(force);

启动动画

有两种方法可以启动弹簧动画:通过调用 start() 或通过调用 animateToFinalPosition() 方法。这两种方法都需要在主线程上调用。

animateToFinalPosition() 方法执行两个任务

  • 设置弹簧的最终位置。
  • 如果动画尚未启动,则启动动画。

由于该方法更新弹簧的最终位置并在需要时启动动画,因此您可以在任何时候调用该方法来更改动画的进程。例如,在一个链式弹簧动画中,一个视图的动画依赖于另一个视图。对于这种动画,使用 animateToFinalPosition() 方法更方便。通过在链式弹簧动画中使用此方法,您无需担心要更新的下一个动画当前是否正在运行。

图 10 说明了一个链式弹簧动画,其中一个视图的动画依赖于另一个视图。

Chained spring demo
图 10. 链式弹簧演示

要使用 animateToFinalPosition() 方法,请调用 animateToFinalPosition() 方法并传递弹簧的静止位置。您也可以通过调用 setFinalPosition() 方法来设置弹簧的静止位置。

start() 方法不会立即将属性值设置为开始值。属性值在每次动画脉冲时都会改变,这发生在绘制传递之前。因此,这些更改将在下一帧中反映出来,就好像这些值是立即设置的一样。

Kotlin

findViewById<View>(R.id.imageView).also { img ->
    SpringAnimation(img, DynamicAnimation.TRANSLATION_Y).apply {
        …
        // Starting the animation
        start()
        …
    }
}

Java

final View img = findViewById(R.id.imageView);
final SpringAnimation anim = new SpringAnimation(img, DynamicAnimation.TRANSLATION_Y);
…
// Starting the animation
anim.start();
…

取消动画

您可以取消动画或跳到动画的末尾。当用户交互要求立即终止动画时,您需要取消动画或跳到动画的末尾,这是一个理想的情况。这主要是在用户突然退出应用程序或视图变得不可见时。

终止动画有两种方法。 cancel() 方法会在动画当前值处终止。 skipToEnd() 方法会跳过动画,直接到达最终值,然后终止。

在终止动画之前,首先要检查弹簧的状态。如果状态为未阻尼,动画永远无法到达静止位置。要检查弹簧的状态,请调用 canSkipToEnd() 方法。如果弹簧已阻尼,该方法将返回 true,否则返回 false

确定弹簧状态后,可以使用 skipToEnd() 方法或 cancel() 方法来终止动画。 cancel() 方法 **必须** 在主线程上调用。

注意: 通常情况下,skipToEnd() 方法会导致视觉上的跳跃。