运用弹簧物理学原理为图形运动添加动画

基于物理特性的动画是依靠力来驱动的。弹簧弹力就是这样一种引导相互作用和运动的力。弹簧弹力具有阻尼和刚度这两个属性。在基于弹簧特性的动画中,值和速度是根据施加到每一帧的弹簧弹力计算的。

如果您希望应用的动画只在一个方向上放慢速度,请考虑改用基于摩擦力的投掷动画

弹簧动画的生命周期

在基于弹簧特性的动画中,SpringForce 类允许您自定义弹簧的刚度、阻尼比以及最终位置。动画一开始,弹簧弹力便会更新每一帧的动画值和速度。动画会一直持续,直到弹簧弹力达到平衡状态。

例如,如果您在屏幕上拖动某个应用图标,然后通过将手指从图标上松开来释放它,则该图标便会被一种看不见但熟悉的力量拉回其原始位置。

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

弹簧释放效果
图 1. 弹簧释放效果

制作弹簧动画

为应用制作弹簧动画的一般步骤如下:

下面的各个部分详细介绍了制作弹簧动画的一般步骤。

添加支持库

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

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

          dependencies {
              def 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:这些属性用于控制视图围绕轴心点进行的 2D(rotation属性)和 3D 旋转。
  • SCROLL_XSCROLL_Y:这些属性分别表示视图距离源左侧和顶部边缘的滚动偏移量(以像素为单位)。它还以页面滚动的距离来表示位置。
  • SCALE_XSCALE_Y:这些属性用于控制视图围绕其轴心点进行的 2D 缩放。
  • 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() 方法并传递属性的最大值。

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

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

设置起始速度

起始速度用于定义在动画开始时动画属性更改的速度。默认起始速度设置为 0 像素/秒。您可以将速度设置为触摸手势的速度,也可以将起始速度设置为固定值。如果您选择提供固定值,我们建议您以“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/秒”为单位提供,然后将其转换为以“像素/秒”为单位。要进行转换,请使用 TypedValue 类中的 applyDimension() 方法。请参阅以下示例代码:

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 时,会出现欠阻尼现象。这会使对象多次经过并越过静止位置,然后逐渐到达静止位置。
  • 当阻尼比等于零时,便会出现无阻尼现象。这会使对象永远振动下去。

要为弹簧增加阻尼比,请执行以下步骤:

  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 展示了一个链接的弹簧动画,其中一个视图的动画依赖于另一个视图。

链接的弹簧动画演示
图 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() 方法会导致视觉上的跳跃。