使用动画移动视图

1,180 阅读23分钟

本篇学习笔记主要根据官方文档中使用动画移动视图使用Fling动画移动视图以及运用弹簧物理学原理为图形运动添加动画这三部分,想要看原文档的可以直接点击链接进入。

通过本篇笔记将会学习到以下内容:

  1. 使用ObjectAnimator移动视图
  2. 使用PathInterpolator实现曲线动画
  3. 深入理解Interpolator,理解曲线动画和曲线轨迹的区别
  4. 使用Path配合ObjectAnimatorAnimatorSet实现曲线轨迹。
  5. 投掷动画的学习和使用
  6. 弹性动画的学习和使用

曲线动画是官方文档这样称呼的,曲线轨迹是我自己瞎叫的,主要是为了区别曲线动画和想要绘制出沿着曲线运动的动画轨迹的区别。

使用动画移动视图

屏幕上的对象有时候需要重新定位,比如一些悬浮的视图可能会跟随手指移动,在手指抬起后可能需要停靠在左边或者右边等,在这些情况下,应该使用动画去重新定位视图的位置,而不应该立即更新视图的位置,因为立即更新视图的位置会使视图从一个位置闪跳到另一个位置。

Android提供了一些可以在屏幕上重新定位对象的方法,比如ObjectAnimator,使用属性动画我们可以提供希望对象停留的结束位置,以及动画的持续时间,同时可以通过设置插值器来控制动画的加速或者减速。

使用ObjectAnimator更改视图的位置

使用ObjectAnimator配合对象内部的属性,就可以很容易的实现在指定时间段内修改对象的属性。在下面的实例中,通过ObjectAnimator配合视图内部的translationXtranslationY这两个属性,则可以很容易的实现在指定时间段内修改视图的位置。

  • 通过translationX使视图横向移动:
    //横向移动View
    private void moveInX() {
        ObjectAnimator animator =
                ObjectAnimator.ofFloat(mBinding.tvMovedView, "translationX", 200);
        animator.setDuration(300);
        animator.start();
    }
  • 通过translationY使视图纵向移动:
    //纵向移动View
    private void moveInY() {
        ObjectAnimator animator =
                ObjectAnimator.ofFloat(mBinding.tvMovedView, "translationY", 200);
        animator.setDuration(300);
        animator.start();
    }
  • 使用AnimatorSet同时移动视图:
    //同时移动View
    private void moveTogether() {
        AnimatorSet set = new AnimatorSet();

        //横向移动
        ObjectAnimator xAnimator = ObjectAnimator.ofFloat(mBinding.tvMovedView, "translationX", mBinding.tvMovedView.getTranslationX() + 200);
        xAnimator.setDuration(300);

        //纵向移动
        ObjectAnimator yAnimator = ObjectAnimator.ofFloat(mBinding.tvMovedView, "translationY", mBinding.tvMovedView.getTranslationY() + 200);
        yAnimator.setDuration(300);

        set.play(xAnimator).with(yAnimator);
        set.start();
    }

运行效果如下:

使用动画移动视图

添加曲线动画

从上面的例子可以看出,ObjectAnimator能够很方便地实现位移动画,但是默认情况下它会使用起点和终点之间地直线重新定位视图。这种实现方式虽然简单,但是并不美观,而使用曲线动画则有助于提升应用地真实感,让动画更有趣。

PathInterpolator类是在Android5.0中引入的新的插值器,它基于贝塞尔曲线或Path对象。此插值器是在一个1×1的正方形内指定一个曲线动作,定位点位于(0,0)(1,1),而控制点则使用构造函数参数指定。如需创建PathInterpolator对象,可以创建Path对象并将其传递给PathInterpolator

下面的代码通过向PathInterpolator构造函数传递一个控制点来构造一个二阶贝塞尔曲线,并以此为基础构建动画的变化规律:

    //使用pathInterpolator移动视图
    private void moveViewWithPathInterpolator(){
        float endX = mBinding.tvMovedView.getTranslationX() > 200 ? mBinding.tvMovedView.getTranslationX() - 200 : mBinding.tvMovedView.getTranslationX() + 200;
        //设置动画
        ObjectAnimator animatorX = ObjectAnimator.ofFloat(mBinding.tvMovedView,"translationX",mBinding.tvMovedView.getTranslationX(),endX,mBinding.tvMovedView.getTranslationX());
        //判断版本
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
            PathInterpolator pathInterpolator = new PathInterpolator(1f,0f);
            animatorX.setInterpolator(pathInterpolator);
        }
        animatorX.setDuration(1000);
        animatorX.start();
    }

最终执行效果如下:

曲线动画示例1

这样,我们就通过上面的代码实现了一个曲线动画。这里需要说明的是:曲线动画并不是说看见的动画轨迹是曲线,而是表示动画的变化规律是曲线。就像上面的动画,缓慢加速到最右边之后又迅速回到原始位置。在这个过程中,这段变化过程是曲线的,如果没有设置Interpolator属性或者设置的Interpolator属性是匀速运动的,那动画就是先滑动到最右边再滑动到最左边,中间不会有任何速率的变化。

同时应该注意到,上面是通过给PathInterpolator的构造函数传递两个参数来构建的Interpolator,这两个参数其实是二阶贝塞尔曲线的控制点。构造函数同时支持三阶贝塞尔曲线的两个控制点以及一个Path对象。

上面说到,这里的曲线变化其实是速率的变化,如果只在一个方向执行动画,我们始终只能看到一个直线运动的轨迹,那如果我们期望看到轨迹是曲线,就像下面这样呢?

曲线轨迹动画

在上面的动画中,我们可以看到View的运动轨迹是一个弧形,也就是一条曲线。其实分析这个动画就会发现,就像上学时候分析物体的受力情况一样,这个动画其实是在两个方向上都包含了动画,在X轴从0开始运动到最右边,然后又运动到最左边,在Y轴则是从0开始运动到指定位置。两个方向同时执行,根据运动的轨迹,在Y轴就是一条直线,可以认为只需要设置一个匀速运动的动画即可,但是在X轴则是一条曲线,我们可以提前规划出曲线,然后写出这个曲线的方程,并将这个方程设置到Interpolator中(上面的曲线动画的方程是一个简单的二次函数:y = -2x^2 + 2x),然后将这个Interpolator设置给需要曲线运动的那个动画,让两个动画同时执行就可以了。所以最终实现这个曲线动画的代码如下:

    //同时移动View
    private void moveTogether() {
        AnimatorSet set = new AnimatorSet();

        //横向移动
        ObjectAnimator xAnimator = ObjectAnimator.ofFloat(mBinding.tvMovedView, "translationX", 200f);
        xAnimator.setInterpolator(input -> input * input * -2f + 2 * input);
        xAnimator.setDuration(1000);

        //纵向移动
        ObjectAnimator yAnimator = ObjectAnimator.ofFloat(mBinding.tvMovedView, "translationY", 200f);
        yAnimator.setDuration(1000);


        set.play(xAnimator).with(yAnimator);
        set.start();
    }

可以看到,就是使用了之前就已经看到过的同时移动View动画那部分代码,只是对横向运动的动画设置了一个新的Interpolator,而这个Interpolator正是我们上面提到过的那个方程。

但是其实上面的方程是有问题的,当然也不算是一个错误,只是视觉上我们会发现横向运动并没有到达我们设置的200f这个距离,这是因为我们设置的200f是结束值而不是最大值,根据方程来看,我们将在x = 1/2的时候取到最大值为1/2,乘上200则计算在这个动画中,横向运动的最大值为100。如果我们仍然期望横向运动的最大值为200,则一种方式是可以通过修改方程,另一种方式则可以通过ObjectAnimator xAnimator = ObjectAnimator.ofFloat(mBinding.tvMovedView, "translationX", 0f,200f,0f);来指定。

定义自己的路径

在上面通过两个例子主要演示了PathInterpolator的简单使用,另外就是比较了曲线动画和我们视觉上看到的曲线轨迹的区别。通俗来讲就是曲线动画表示的是在一段时间内动画的速率是变化的,而视觉上的曲线轨迹则首先需要至少两个方向上的动画,同时需要满足某一个方向上的动画的变化速率是曲线的才能实现曲线动画。

通过上面的例子我们已经能够实现视觉上的曲线动画了,那么为什么还要PathInterpolator呢?我个人认为主要在于对Interpolator的理解上,其实不管对于哪种Interpolator,我们都可以认为是在坐标轴上的(0,0)(1,1)构成的矩形范围内画线,起点是(0,0),终点则为(1,1),这样来看,只要我们能够用一个函数来表示我们画出的这条线,我们就可以实现这样的动画。而有时候我们很难直接用一个函数去表示画出的一条线,所以有了Interpolator,我们不必用函数去表示这条线,而是直接用这条线作为Interpolator设置给动画。

最重要的是:我们可以看到TimeInterpolator需要我们实现float getInterpolation(float input)这个方法,在这个方法中,这个input就是我们函数中的x值,返回值就是我们函数中的y值。由此,只要我们能写出方程,我们就能得到我们想要的动画。在有了PathInterpolator后,只要我们能画出这条线,我们就能得到动画。

上面我们通过组合两个动画实现了视觉上的曲线运动轨迹,不过在ObjectAnimator中也为我们提供了新的函数来实现这样的效果:

    //使用ObjectAnimator实现动画路径
    private void useObjectAnimatorWithPath() {
        //判断版本
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            Path path = new Path();
            path.arcTo(0, mBinding.tvMovedView.getTop(),  500f, mBinding.tvMovedView.getTop() + 500f, -90, 180, true);
            ObjectAnimator animator = ObjectAnimator.ofFloat(mBinding.tvMovedView, View.X, View.Y, path);
            animator.setDuration(2000);
            animator.start();
        }
    }

使用ObjectAnimator实现动画路径

其他的PathInterpolator

前面只是简单的演示了PathInterpolator的使用,我们提到过还可以通过传递一个Path对象来设置一个PathInterpolator,这样我们对于动画则有了很大的改善空间,因为相比于写一个函数,很明显直接用路径更容易设置。下面的代码演示了创建一个简单的小球坠落的动画:

    private void createPathInterpolatorWithPath() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            Path path = new Path();
            //移动到(0,0)点,必须从这里开始
            path.moveTo(0f, 0f);
            //绘制第一个坠落的路径,是一个1/4的椭圆,注意椭圆所在的矩形范围
            path.arcTo(new RectF(-0.5f, 0f, 0.5f, 2f), -90, 90);
            //绘制第一个弹起的路径,是一个1/2的椭圆
            path.arcTo(new RectF(0.5f, 0.5f, 0.8f, 1.5f), -180, 180);
            //不再弹起,直接连线到结束
            path.lineTo(1f, 1f);
            //设置PathInterpolator
            PathInterpolator pathInterpolator = new PathInterpolator(path);

            //横轴做普通动画
            ObjectAnimator animatorX = ObjectAnimator.ofFloat(mBinding.circleView, "translationX", 0f, 500f);
            animatorX.setDuration(2000);

            //纵轴根据指定的path做动画
            ObjectAnimator animatorY = ObjectAnimator.ofFloat(mBinding.circleView, "translationY", 0f, 500f);
            //将pathInterpolator设置给纵轴方向上的运动
            animatorY.setInterpolator(pathInterpolator);
            animatorY.setDuration(2000);

            //动画集合
            AnimatorSet set = new AnimatorSet();
            set.play(animatorX).with(animatorY);
            set.start();
        }
    }

最终实现的效果如下:
使用PathInterpolator实现一个简单的小球下落的动画

可以看到,使用PathInterpolator,我们就可以很轻易地定义出我们想要的动画路径,如果我们仅仅通过函数去设置,那么这个函数可能比较难写,用路径则比较容易。当然,对于这个动画,我们仍然可以通过函数去写,一个比较简单的方式就是将这一个动画分解为多个动画,比如Y轴方向上,首先是一个从高处下落的曲线轨迹,然后是一个弹起落下的轨迹,这两个轨迹都可以使用二次函数表示,这样我们将Y轴方向上的动画分解为3个动画去显示,仍然能够达到同样的。

这里需要注意的是,绘制弧线的方式,首先确定弧线所在的椭圆,然后确定椭圆所在的矩形,最后确定矩形的坐标,要确保x轴的连线始终是连续的,同时确保起始点和结束点分别是(0,0),(1,1)

使用投掷动画移动视图

投掷动画利用与对象速度成正比的摩擦力,我们可以使用该动画为某个对象的属性添加动画效果,还可以使用该动画逐渐结束动画。该动画具有一个初始动量,主要从手势速度获得,然后逐渐变慢。当动画速度足够低,在设备屏幕上没有任何可见变化时,动画便会结束。

需要注意的是:通过官方文档的例子的学习,这个动画还是用于类似于ScrollView这样的需要滑动的地方比较有用,我们可以通过这个动画,记录用户每次滑动屏幕的速度,然后根据这个速度去设置用户手指抬起之后的动作,向用户提供比较友好的反馈。从官方文档来看,这个动画还是适用于那些需要有条件的缓慢结束的动画(可以把这个条件转换为速度或者摩擦力)。虽然也能够用在逐渐结束一个动画上,但是我个人测试效果不明显,而且这个需求完全可以通过Interpolator去实现。

在文档《运用弹簧物理学原理为图形运动添加动画》那一节也提到,如果希望动画啊只在一个方向上放慢速度,则可以使用基于摩擦力的投掷动画。

下面的例子实现了在一个加速动画结束后通过投掷动画将这个动画缓慢停止:

    private void createFlingAnimation(){
        //首先通过一个动画创建View移动的动画
        ObjectAnimator animator = ObjectAnimator.ofFloat(mBinding.circleView,"translationX",100f);
        animator.setInterpolator(new AccelerateInterpolator());
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator anim) {
                super.onAnimationEnd(anim);
                //结束时创建投掷动画缓慢减速
                //创建投掷动画实例
                FlingAnimation animation = new FlingAnimation(mBinding.circleView, DynamicAnimation.TRANSLATION_X);
                //设置速度
                animation.setStartVelocity(200.0f);
                //设置动画的最小值
                animation.setMinValue(50f);
                //设置动画的最大值
                animation.setMaxValue(200f);
                //设置摩擦力
                animation.setFriction(2.0f);
                animation.addUpdateListener((animation1, value, velocity) -> Logs.e("动画执行中:"+ value));
                //开始执行
                animation.start();
            }
        });
        animator.start();
    }

需要注意的是,要使用投掷动画,需要先导入依赖包,如果使用androidx,使用下面的方式导入:

//基于物理特性的动画API
    implementation androidx.dynamicanimation:dynamicanimation-ktx:1.0.0-alpha03

如果使用支持库,则使用下面的方式导入:

implementation 'com.android.support:support-dynamic-animation:28.0.0'

上面的代码运行后的动画效果如下:

投掷动画示例

在上面的代码中,首先创建了一个使用ObjectAnimator执行的动画,这个动画会将View向右移动100个像素的距离,在这个动画结束之后,我们会创建一个投掷动画来缓慢结束之前的动画。

需要注意的是,我们设置的最小值需要在当前值的范围中,换句话说,最小值应该小于或者等于当前值(起始值),这是因为我们没有设置起始值,便会将当前值作为起始值。比如在上面的代码中,设置的投掷动画的最小的translationX的值应该小于等于100,如果设置为大于100则会出现异常。异常信息为"java.lang.IllegalArgumentException: Starting value need to be in between min value and max value"

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

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

弹簧动画仍然需要添加物理特性的支持库,和上面的支持库是一样的。

创建弹簧动画

借助SpringAnimation类,可以为对象制作弹簧动画。要制作弹簧动画,需要创建SpringAnimation类的一个实例,并提供一个对象,想要设置动画的这个对象的属性,以及希望动画停留的最终弹簧位置(可选)。如下:

    //创建弹性动画
    private void createSimpleSpringAnimation(){
        SpringAnimation springAnimation = new SpringAnimation(mBinding.circleView,DynamicAnimation.TRANSLATION_X);
        springAnimation.start();
    }

使用上面的代码就已经创建了一个弹性动画实例了,但是直接运行会报错:Incomplete SpringAnimation: Either final position or a spring force needs to be set.,我们需要指定finalPosition或者指定一个SpringForce,其实最终我们还是需要指定finalPosition,直接创建SpringForce不设置其中的finalPosition仍然会导致运行失败。这是因为在SpringAnimation有一个默认的最大值mMaxValue = Float.MAX_VALUE,在SpringForcefinalPosition的默认值为Double.MAX_VALUE,而在执行start()方法时会判断mMaxValue是否小于或者等于finalPosition,如果不是小于等于则会报错。

    //创建弹性动画
    private void createSimpleSpringAnimation(){
        //直接通过构造函数指定finalPosition
        SpringAnimation springAnimation = new SpringAnimation(mBinding.circleView,DynamicAnimation.TRANSLATION_X,500f);
        //通过SpringForce指定finalPosition
//        SpringForce force = new SpringForce();
//        force.setFinalPosition(500f);
//        springAnimation.setSpring(force);
        springAnimation.start();
    }

指定finalPosition之后就可以执行动画了,执行效果如下:

一个简单的弹性动画

可以看到有一个回弹的效果,这个效果其实使用Interpolator也是可以实现的。如下:

        ObjectAnimator animator = ObjectAnimator.ofFloat(mBinding.circleView,"translationX",0f,500f);
        animator.setInterpolator(new OvershootInterpolator());
        animator.start();

通过指定ObjectAnimatorInterpolatorOvershootInterpolator就实现了一个和默认弹性动画差不多的动画。

系统为弹性动画和投掷动画提供了以下常用参数来设置动画:

  1. ALPHA表示视图的透明度,该值默认为1,表示不透明,为0则表示完全透明,通过DynamicAnimation.ALPHA指定
  2. TRANSLATION_X,TRANSLATION_Y,TRANSLATION_Z这些属性用于控制视图的所在位置,值为视图的布局容器所设置的左侧坐标,顶部坐标和高度的增量
  3. ROTATION,ROTATION_X,ROTATION_Y这些属性用于控制视图围绕轴心点进行的2D(rotation)属性和3D旋转。
  4. SCROLL_X,SCROLL_Y这些属性表示视图距离左边和顶部边缘的滚动偏移量,它还以页面滚动的距离来表示位置
  5. SCALE_X,SCALE_Y这些属性用于控制视图围绕其轴心点进行的2D缩放。
  6. X,Y,Z这些是基本的实用属性,用于描述视图在容器中的最终位置。

注册监听器

DynamicAnimation类提供了两个监听器OnAnimationUpdateListenerOnAnimationEndListener,这些监听器会监听动画中的更新,例如当动画值发生改变或结束时则会回调这些监听器。

当需要监听动画值发生改变的时候,我们可以通过注册这个监听器来获得动画执行过程中的当前值。例如当一个动画参数依赖于另一个动画执行过程中的数据时,这个监听器就非常有用,如下所示:

    //创建弹性动画并注册监听器
    private void createSpringAnimationWithListener(){
        //对第一个View设置横向和纵向移动的动画
        SpringAnimation animation1X = new SpringAnimation(mBinding.circleView,DynamicAnimation.TRANSLATION_X,500);
        SpringAnimation animation1Y = new SpringAnimation(mBinding.circleView,DynamicAnimation.TRANSLATION_Y,300);

        //对第二个View设置横向和纵向移动的动画
        SpringAnimation animation2X = new SpringAnimation(mBinding.circleView2,DynamicAnimation.TRANSLATION_X);
        SpringAnimation animation2Y = new SpringAnimation(mBinding.circleView2,DynamicAnimation.TRANSLATION_Y);

        //添加动画监听器,当动画1中的值改变后修改动画2中的值
        animation1X.addUpdateListener((animation, value, velocity) -> animation2X.animateToFinalPosition(value));
        animation1Y.addUpdateListener((animation,value,velocity) -> animation2Y.animateToFinalPosition(value));
        animation1X.addEndListener((animation,canceled,value,velocity) -> Logs.e("动画1执行结束"));
        animation2X.addEndListener((animation,canceled,value,velocity) -> Logs.e("动画2执行结束"));


        animation1X.start();
        animation1Y.start();
    }

在上面的代码中,我们对两个View设置了四个动画,其中第二个View的动画跟随第一个View的动画,通过对第一个View的两个动画分别注册更新监听器,我们能够拿到每次动画更新后的值,然后将这这个值设置给第二个View的动画上,通过animateToFinalPosition()方法更新第二个View动画的值,就达到了第二个View跟随第一个View移动的效果。可以看到,使用这种方式创建的动画更加生动并符合客观规律。

执行效果如下:

添加监听器

移除监听器

当不需要对动画的状态进行监听的时候,可以通过removeUpdateListener()removeEndListener()方法移除相应的监听器。

设置动画的起始值

可以通过调用setStartValue()方法设置动画的起始值,如果没有设置动画的起始值,则动画将以对象属性的当前值作为起始值。

设置动画值的范围

如果要将属性值限制在特定的范围内,则可以设置最小动画值和最大动画值,如果为具有内在范围的属性(如Alpha透明度的范围应该限制在01之间)添加动画效果,这样做还有助于控制范围。

  • 通过setMinValue()设置最小值
  • 通过setMaxValue()设置最大值

需要注意的是:如果同时设置了动画的范围和动画的起始值,则应该确保动画的起始值在最小值和最大值之间。

设置弹簧属性

通过SpringForce类可以为每个弹性动画设置弹簧属性,比如阻尼比和刚度。

  • 阻尼比

阻尼比用于描述弹簧震动逐渐衰减的状况。通过使用阻尼比,可以定义震动从一次弹跳到下一次弹跳所衰减的速度有多快,下面列出了可使弹簧弹力衰减的四种不同的方式:

  1. 当阻尼比大于1时,会出现过阻尼现象,会使对象快速地返回到静止位置
  2. 当阻尼比等于1时,会出现临界阻尼现象,会使对象在最短时间内返回静止位置
  3. 当阻尼比小于1时,会出现欠阻尼现象,这会使对象多次越过静止位置,然后逐渐到达静止位置
  4. 当阻尼比等于0时,便会出现无阻尼现象,这会使对象永远振动下去。

通过下面的方法为弹簧设置阻尼比:

    //为弹性动画设置阻尼比
    private void createSpringAnimationWithSpringForce(){
        SpringAnimation springAnimation = new SpringAnimation(mBinding.circleView2,DynamicAnimation.TRANSLATION_X);
        //创建SpringForce对象
        SpringForce force = new SpringForce(500f);
        //设置阻尼比
        force.setDampingRatio(0.2f);
        //为动画设置SpringForce
        springAnimation.setSpring(force);

        //设置动画的起始位置
        springAnimation.setStartValue(0f);
        //开始播放动画
        springAnimation.start();
    }

通过上面的代码我们就为一个弹性动画设置了阻尼比,通过改变阻尼比的参数,就可以看到不同的回弹效果,当阻尼比为0时,动画便会一直运动下去。当阻尼比为1时,动画会尽快到达指定的位置,当阻尼比在0到1之间时,便会看到几次回弹效果,当阻尼比大于1时,动画会缓慢到达指定位置。

系统中为我们提供了以下阻尼比常量:

  • DAMPING_RATIO_HIGH_BOUNCY
  • DAMPING_RATIO_MEDIUM_BOUNCY
  • DAMPING_RATIO_LOW_BOUNCY
  • DAMPING_RATIO_NO_BOUNCY

其中默认的阻尼比为DAMPING_RATIO_MEDIUM_BOUNCY.

  • 刚度

刚度定义了用于衡量弹簧强度的弹簧常量。不在静止位置的坚硬弹簧可以对所连接的对象施加更大的力,通过以下步骤可以为弹簧增加刚度:

  1. 通过getSpring()方法来检索要增加刚度的弹簧
  2. 通过setStiffness()方法并传递
    //为弹性动画设置阻尼比
    private void createSpringAnimationWithSpringForce(){
        SpringAnimation springAnimation = new SpringAnimation(mBinding.circleView2,DynamicAnimation.TRANSLATION_X);
        //创建SpringForce对象
        SpringForce force = new SpringForce(500f);
        //设置阻尼比
        force.setDampingRatio(0.2f);
        //设置刚度
        force.setStiffness(10f);
        //为动画设置SpringForce
        springAnimation.setSpring(force);

        //设置动画的起始位置
        springAnimation.setStartValue(0f);
        //开始播放动画
        springAnimation.start();
    }

设置刚度的意义在于设置回弹的速度,设置阻尼比在于设置回弹的次数,刚度越大,回弹的速度越快,阻尼比越大,回弹的次数越少。

系统已经为我们提供了下面四种刚度类型,分别是:

  • STIFFNESS_HIGH 高刚度 (10000)
  • STIFFNESS_MEDIUM 中刚度 (1500)
  • STIFFNESS_LOW 低刚度 (200)
  • STIFFNESS_VERY_LOW 非常低的刚度 (50)

想要查看这四种不同刚度和不同阻尼比的演示效果,可以点击此处跳转到文档部分。

下面是添加了阻尼比和刚度之后的运行效果:

添加阻尼比和刚度

需要注意的是:图片不能和上面的代码对应,这是因为我是在写完所有代码后再在模拟器运行然后截图,使用上面的代码仍然可以正常运行。

创建自定义的弹簧弹力

在上面的代码中已经演示了如何创建弹簧弹力的过程,分别是:

  1. 创建SpringForce对象
  2. 调用SpringForce对象的setDampingRatio()setStiffness()来设置弹簧的阻尼比和刚度
  3. SpringForce对象设置给SpringAnimation

启动动画

可以通过start()或者animateToFinalPosition()来启动动画,这两个方法都需要在主线程中调用,其中animateToFinalPosition会执行两项任务:

  • 设置弹簧的最终位置
  • 启动动画(如果尚未启动)

animateToFinalPosition会更新弹簧的最终位置并根据需要启动动画,因此我们可以随时通过调用此方法来更改动画过程。例如,在链接动画中,一个视图依赖于另一个视图,对于此类动画,使用animateToFinalPosition方法将会更加便捷。如下面的例子中演示了一个链接动画。

        //创建动画用于修改位置
        SpringAnimation animation1 = new SpringAnimation(mBinding.circleView2,DynamicAnimation.TRANSLATION_X);
        SpringForce force1 = new SpringForce();
        force1.setDampingRatio(0.5f);
        force1.setStiffness(500);
        animation1.setSpring(force1);

        SpringAnimation animation2 = new SpringAnimation(mBinding.circleView2, DynamicAnimation.TRANSLATION_Y);
        SpringForce force2 = new SpringForce();
        force2.setDampingRatio(0.5f);
        force2.setStiffness(500);
        animation2.setSpring(force2);

        SpringAnimation animation3 = new SpringAnimation(mBinding.circleView3,DynamicAnimation.TRANSLATION_X);
        SpringForce force3 = new SpringForce();
        force3.setDampingRatio(0.5f);
        force3.setStiffness(500);
        animation3.setSpring(force3);

        SpringAnimation animation4 = new SpringAnimation(mBinding.circleView3,DynamicAnimation.TRANSLATION_Y);
        SpringForce force4 = new SpringForce();
        force4.setDampingRatio(0.5f);
        force4.setStiffness(500);
        animation4.setSpring(force4);

        mBinding.circleView.setMoveXListener(aFloat -> {
            animation1.animateToFinalPosition(aFloat);
            return null;
        });

        mBinding.circleView.setMoveYListener(translationY ->{
            animation2.animateToFinalPosition(translationY);
            return null;
        });

        mBinding.circleView2.setMoveXListener(translationX -> {
            animation3.animateToFinalPosition(translationX);
            return null;
        });

        mBinding.circleView2.setMoveYListener(translationY -> {
            animation4.animateToFinalPosition(translationY);
            return null;
        });

上面的动画依赖的三个View是一个简单的自定义的View,代码如下:

class CircleView: View {

    private val mPaint by lazy {
        Paint(Paint.ANTI_ALIAS_FLAG).apply {
            color = Color.RED
        }
    }

    //定义一个接口,当前的View移动时将位置传递出去
    var moveXListener: ((translationX: Float) -> Unit)? = null
    var moveYListener: ((translationY: Float) -> Unit)? = null

    private val mRect by lazy {
        RectF()
    }
    constructor(context: Context): this(context, null){

    }

    constructor(context: Context, attributes: AttributeSet?):this(context, attributes, 0){

    }

    constructor(context: Context, attributes: AttributeSet?, defStyleAttr: Int): super(
        context,
        attributes,
        defStyleAttr
    ){

    }


    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        //super.onMeasure(50, 50)
        setMeasuredDimension(
            100,
            100
        )
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        canvas?.drawCircle(measuredWidth / 2f, measuredHeight / 2f, measuredWidth / 2f, mPaint)
    }



    //用户手指按下的位置
    private var mTouchX: Float = 0f
    private var mTouchY: Float = 0f

    override fun onTouchEvent(event: MotionEvent?): Boolean {

        when(event?.action){
            MotionEvent.ACTION_DOWN -> {
                mTouchX = event.x
                mTouchY = event.y
                return true
            }

            MotionEvent.ACTION_MOVE -> {
                this.translationX += event.x - mTouchX
                this.translationY += event.y - mTouchY
                return true
            }

            MotionEvent.ACTION_UP -> {


            }
        }

        return super.onTouchEvent(event)
    }

    override fun setTranslationX(translationX: Float) {
        super.setTranslationX(translationX)
        moveXListener?.invoke(translationX)
    }

    override fun setTranslationY(translationY: Float) {
        super.setTranslationY(translationY)
        moveYListener?.invoke(translationY)
    }
}

每次View的位置改变之后都会通过接口将位置信息传递出去,然后再Activity中获取到第一个View的位置信息后通过动画修改第二个CircleView的位置信息,同理,第二个CircleView的位置信息修改之后也会通过接口修改第三个CircleView的位置信息。

上面的代码运行效果如下:

通过animateToFinalPosition创建一个连接动画

取消动画

如果用户退出应用或者页面,或者视图变得不可见时,我们可能需要取消动画或者将动画直接设置到结尾处。通过cancel()方法可以在动画的当前值处终止动画,skipToEnd()方法会跳转到动画结束值处然后终止动画。

在终止动画之前,首先需要判断动画是否能够终止,如果动画处于无阻尼状态,则动画永远不会终止,通过调用canSkipEnd()可以判断动画能否终止,如果动画处于无阻尼状态则会返回false,否则将会返回true。注意:cancel()方法不受影响,skipToEnd()受到阻尼状态的影响。

    private void createSpringAnimationWithSpringForce(){
        SpringAnimation springAnimation = new SpringAnimation(mBinding.circleView2,DynamicAnimation.TRANSLATION_X);
        //创建SpringForce对象
        SpringForce force = new SpringForce(500f);
        //设置阻尼比
        force.setDampingRatio(0f);
        //设置刚度
        force.setStiffness(20);
        //为动画设置SpringForce
        springAnimation.setSpring(force);

        //设置动画的起始位置
        springAnimation.setStartValue(0f);
        //开始播放动画
        springAnimation.start();

        //经过1秒后终止动画
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                springAnimation.cancel();
                
            }
        },1000);
    }

在上面的代码中,将动画的阻尼比设置为0,然后执行动画,1秒后通过cancel()方法取消动画,可以看到,动画停止在了当前位置。效果如下:

通过cancel()终止无阻尼动画

修改代码,使用skipToEnd()方法终止动画,则会出现异常: java.lang.UnsupportedOperationException: Spring animations can only come to an end when there is damping

使用下面的方法动画不会结束,但是也不会出错:

        //经过1秒后终止动画
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                if(springAnimation.canSkipToEnd()){
                      springAnimation.skipToEnd();
                }
                //springAnimation.cancel();

            }
        },1000);