当前位置: 首页 > news >正文

手机App里的动画是如何实现的-安卓动画深入探索

Android动画

参考资料 《Android开发艺术探索》 任玉刚

Android的动画可以分为三种:View动画,帧动画和属性动画,其实帧动画也属于View动画一种,不过它和平移,旋转等常见的View动画在表现形式上略有不同。View动画通过对场景里对象不断做图像变换(平移,缩放,旋转,透明度)从而产生动画效果,它是一种渐进式动画,并且View动画支持自定义。帧动画通过顺序播放一系列图像从而产生动画效果,可以简单理解为图片切换动画,如果图片过多过大就会导致OOM。属性动画通过动态地改变对象属性从而达到动画效果,属性动画为API 11新特性,低版本无法直接使用属性动画。本文将从易到难分别解释帧动画,View动画和属性动画。

帧动画

帧动画是顺序播放一组预先定义好的图片。系统提供了AnimationDrawable来使用帧动画。首先在res/drawable目录下写一个root为animation-list的xml文件定义一个AnimationDrawable,如下

<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"android:oneshot="false"><item android:drawable="@drawable/closer" android:duration="500"/><item android:drawable="@drawable/bainiangudu" android:duration="300" />
</animation-list>

然后将上述Drawable作为View的背景并通过Drawable来播放动画即可。

val view = findViewById<View>(R.id.view)
view.setBackgroundResource(R.drawable.my_animation)
val drawable = view.background as AnimationDrawable
drawable.start()

帧动画将一组图片和播放参数(如每帧持续时间、是否循环)打包成一个完整的动画单元,它的使用很简单,不过容易引起OOM,所以使用帧动画时尽量避免使用过多大尺寸图片。

View动画

View动画支持四种动画效果,分别是平移动画,缩放动画,旋转动画和透明度动画,这四种也常称补间动画。这里要特别强调,像平移,旋转这样的动画,它改变了View的显示,但未触动其根本。系统并不会去更新View的lefttoprightbottom等决定其布局位置的真实坐标。

View动画的种类

View动画的4个种类也对应着Animation的四个子类:TranslateAnimationScaleAnimationRotateAnimationAlphaAnimation。这四种动画既可以通过代码创建,也可以通过xml来定义,对于View动画来说,建议采用XML定义动画,因为xml可读性更好。

名称标签子类效果
平移动画<translate>TranslateAnimation移动View
缩放动画<scale>ScaleAnimation放大或缩小View
旋转动画<rotate>RotateAnimation旋转View
透明度动画<alpha>AlphaAnimation改变透明度

要使用View动画,首先要创建动画的xml文件,这个文件路径为res/anim/filename.xml,View动画的描述文件是有固定语法的,如下

    <?xml version="1.0" encoding="utf-8"?><set xmlns:android="http://schemas.android.com/apk/res/android"android:interpolator="@[package:]anim/interpolator_resource"android:shareInterpolator=["true" | "false"] ><alphaandroid:fromAlpha="float"android:toAlpha="float" /><scaleandroid:fromXScale="float"android:toXScale="float"android:fromYScale="float"android:toYScale="float"android:pivotX="float"android:pivotY="float" /><translateandroid:fromXDelta="float"android:toXDelta="float"android:fromYDelta="float"android:toYDelta="float" /><rotateandroid:fromDegrees="float"android:toDegrees="float"android:pivotX="float"android:pivotY="float" /><set>...</set></set>

从上面语法可以看出,View动画既可以是单个动画,也可以由一系列动画组成。

<set>标签表示动画集合,对应AnimationSet类,它可以包含若干个动画,并且它的内部也可以嵌套其他动画集合,它的两个属性如下:

  • interpolator:表示动画集合所才用的插值器,插值器影响动画的速度,比如非匀速动画就需要通过插值器控制动画的播放过程。这个属性可以不指定,默认为@android:anim/accelerate_delerate_interpolator,即加速减速插值器。
  • shareInterpolater:表示集合中的动画是否和集合共享同一个插值器。如果集合不指定插值器,那么子动画就需要单独指定所需的插值器或者使用默认值。

4个View动画的核心属性在上面基本都列出来了,from指定初始状态,to指定结束状态。其中旋转动画中的pivotX和pivotY是确定轴点的属性,可以理解为旋转中心的意思,注意是相对于应用该动画的 View 自身的坐标系统。除上面的外View动画还有一些常用的属性,比如duration指定动画事件,fillAfter指定动画结束后View是否停留在结束位置。true表示停留在结束位置,false表示不停留。

下面举个例子,这是设置的动画xml文件。主活动对应的xml文件里放一个view用来展示动画即可。

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"><alphaandroid:fromAlpha="0.1"android:toAlpha="1.0"android:duration="2000"/>
</set>

主活动中 的使用也很简单,用AnimationUtils.loadAnimation(this, R.anim.my_view_animation)获取到对应的animation对象然后进行播放即可

class MainActivity : AppCompatActivity() {private lateinit var drawable: AnimationDrawableoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)enableEdgeToEdge()setContentView(R.layout.activity_main)ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)insets}val view = findViewById<View>(R.id.view)val animation = AnimationUtils.loadAnimation(this, R.anim.my_view_animation)view.animation = animationview.startAnimation(animation)}
}

最后的效果如下,同时实现了透明度淡入和旋转的效果。

在这里插入图片描述

除了在XML中定义动画外,还可以通过代码来实现动画,举个例子

val view = findViewById<View>(R.id.view)
val alphaAnimation = AlphaAnimation(0.5f, 1f)
alphaAnimation.duration = 500
view.startAnimation(alphaAnimation)

如果想组合多个动画,也可以用AnimationSet进行组合设置,代码比较简单,这里就不解释了。

val animationSet = AnimationSet(true)val alphaAnimation = AlphaAnimation(0.5f, 1f)
alphaAnimation.duration = 500val rotateAnimation = RotateAnimation(0f, 360f,Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f  
)
rotateAnimation.duration = 500animationSet.addAnimation(alphaAnimation)
animationSet.addAnimation(rotateAnimation)view.startAnimation(animationSet)

自定义View动画

除了View给我们提供的4种动画外,我们还可以自定义View动画。自定义View动画需要继承Animation这个抽象类,重写它的initializeapplyTransformation方法,由于applyTransFormation涉及到了矩阵变换属于数学上的概念,这里就不展开介绍了。

View动画的特殊使用场景

除了上面View动画的四种形式外,View动画还可以在一些特殊场景下使用,比如在ViewGroup种可以控制子元素的出场效果,在Activity中可以实现不同的Activity的切换效果。

LayoutAnimation

LayoutAnimation作用于ViewGroup,为ViewGroup指定一个动画,这样当它的子元素出场时都会具有这种动画效果。作用在ListView,RecyclerView上就可以实现让每个item都以一定的动画的形式出现,这其实就是使用了LayoutAnimation,LayoutAnimation也是一个View动画,使用如下。

  • 首先在res/anim目录下定义LayoutAnimation
<layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"android:animation="@anim/my_view_animation"android:delay="0"android:animationOrder="normal"/>

它的属性如下

android:delay:表示子元素开始动画的时间延迟,比如子元素入场动画的事件周期为300ms,那么0.5表示每个子元素都需要延迟150ms才能播放入场动画。

android:animationOrder:表示子元素动画的顺序,有三种选项:normal、reverse和random,其中normal表示顺序显示,即排在前面的子元素先播放,reverse表示逆向显示,排在后面的子元素先开始播放入场动画,random则是随机播放入场动画。

android:animation:为子元素指定具体的入场动画

  • 接下来为元素指定具体的入场动画,如下
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"><alphaandroid:fromAlpha="0.1"android:toAlpha="1.0"android:duration="2000"/><rotateandroid:duration="2000"android:fillAfter="true"android:fromDegrees="0"android:toDegrees="360"android:pivotX="50%"android:pivotY="50%"/>
</set>
  • 最后为ViewGroup指定android:layoutAnimation属性即可,这种方式适用于所有的ViewGroup,常用在RecyclerView或者LIstView的子项中,这里简单在布局里使用演示一下。下面是主活动对应的布局。
<?xml version="1.0" encoding="utf-8"?><androidx.constraintlayout.widget.ConstraintLayoutxmlns: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:id="@+id/main"android:layoutAnimation="@anim/my_layout_animation"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity"><Viewandroid:id="@+id/view"android:layout_width="100dp"android:layout_height="100dp"android:background="@drawable/closer"app:layout_constraintTop_toTopOf="parent"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintEnd_toEndOf="parent"/><TextViewandroid:text="这是一行文本"app:layout_constraintTop_toBottomOf="@id/view"app:layout_constraintStart_toStartOf="@id/view"app:layout_constraintEnd_toEndOf="@id/view"android:layout_width="wrap_content"android:layout_height="wrap_content"android:textSize="25sp"android:textColor="@color/black"android:id="@+id/text"/></androidx.constraintlayout.widget.ConstraintLayout>

设置了主活动对应的布局后,活动中不需要添加任何代码,直接运行项目,子项就有对应的动画效果了。

在这里插入图片描述

除了在xml中指定LayoutAnimation外,还可以通过LayoutAnimationController在代码中指定LayoutAnimatoin。具体代码如下,最终的效果和上面的一样。

val animation = AnimationUtils.loadAnimation(this, R.anim.my_view_animation)
val controller = LayoutAnimationController (animation);
controller.setDelay(0f);
controller.setOrder(LayoutAnimationController.ORDER_NORMAL);
val mainView = findViewById<ViewGroup>(R.id.main)
mainView.setLayoutAnimation(controller);

Activity的切换效果

Activity有默认的切换效果,但是这个效果我们可以自定义,主要用到overridePendingTransition(int enterAnim, int exitAnim)这个方法,这个方法必须在startActivity(Intent)或者finish()之后被调用才能生效,它的参数含义如下:

  • enterAnim:Activity被打开时,所需的动画资源id
  • exitAnim:Activity被暂停时,所需的动画资源id

当启动一个Activity时,可以按照如下方式为其添加自定义的切换效果:

Intent intent = new Intent(this, TestActivity.class);
startActivity(intent);
overridePendingTransition(R.anim.enter_anim, R.anim.exit_anim);

当Activity退出时,也可以为其指定自己的切换效果

@Override
public void finish() {super.finish();overridePendingTransition(R.anim.enter_anim,R.anim.exit_anim);
}

在这里插入图片描述

为了方便简单用一下之前的动画文件演示一下,即上面这个效果。

val view = findViewById<View>(R.id.view)
view.setOnClickListener {val intent = Intent(this@MainActivity, MainActivity2::class.java)startActivity(intent)overridePendingTransition(R.anim.my_view_animation, R.anim.my_view_animation)}

属性动画

属性动画是API11新加入的特性,与View动画不同,它对作用对象做了扩展,属性动画可以对任何对象做动画,甚至可以没有对象。除了对作用对象进行扩展以外,属性动画的效果也得到了加强,不再像View动画那样只能支持四种简单的变换,属性动画中有ValueAnimatorObjectAnimatorAnimatorSet等概念,通过它们可以实现绚丽的动画。

使用属性动画

属性动画可以对任意对象的属性进行动画而不仅仅是View,动画默认时间间隔300ms,默认帧率10ms/帧。

其可以在一个时间间隔内完成对象从一个属性值到另一个属性值的改变。但是属性动画从API11才有,这严重制约了属性动画的使用,可以采用nineoldandroids来兼容以前的版本。Nineoldandroids对属性动画做了兼容,在API 11 以前的版本还是通过代理View动画来实现的。因此,在低版本上它的本质还是View动画。

比较常用的几个动画类是ValueAnimatorObjectAnimatorAnimatorSet,只要对象有这个属性,它都能实现动画效果。下面先看一下属性动画的使用。

  • 改变一个对象的translationY属性,让其沿着Y轴向上平移一段距离:它的高度,该动画在默认时间内完成,动画的完成时间是可以自定义的,想要更灵活的效果还可以自定义插值器和估值算法,不过系统内置的已经能够满足常用的动画
ObjectAnimator.ofFloat(myObject, "translationY", -myObjectHeight()).start();
  • 改变一个对象的背景色属性,典型的情形是改变View的背景色,下面的动画可以让背景色在3秒内实现从0xFFFF80800xFF8080FF的渐变,动画会无限循环而且会有反转的效果。
ValueAnimator colorAnim = objectAnimator.ofInt(this, "backgroundColor", 0xFFFF8080, 0XFF8080FF);
colorAnim.setDuration(3000);
colorAnim.setEvaluator(new ArgbEvaluator());
colorAnim.setRepeatCount(ValueAnimator.INFINITE);
colorAnim.setRepeatMode(ValueAnimator.REVERSE);
colorAnim.start();
  • 动画集合,5秒内对View的旋转,平移,缩放,透明度都进行了改变。
AnimatorSet set = new AnimatorSet();
set.playTogether(objectAnimator.ofFloat(myView, "rotationX", 0, 360),objectAnimator.ofFloat(myView, "rotationY", 0, 180),objectAnimator.ofFloat(myView, "rotation", 0, -90),objectAnimator.ofFloat(myView, "translationX", 0, 90),objectAnimator.ofFloat(myView, "translationY", 0, 90),objectAnimator.ofFloat(myView, "scaleX", 1, 1.5f),objectAnimator.ofFloat(myView, "alpha", 1, 0.25f, 1)
);
set.setDuration(5000).start();

属性动画除了通过代码实现以外,还可以通过XML来定义。属性动画需要定义在res/animator/目录下,它的语法如下。

    <setandroid:ordering=["together" | "sequentially"]><objectAnimatorandroid:propertyName="string"android:duration="int"android:valueFrom="float | int | color"android:valueTo="float | int | color"android:startOffset="int"android:repeatCount="int"android:repeatMode=["repeat" | "reverse"]android:valueType=["intType" | "floatType"]/><animatorandroid:duration="int"android:valueFrom="float | int | color"android:valueTo="float | int | color"android:startOffset="int"android:repeatCount="int"android:repeatMode=["repeat" | "reverse"]android:valueType=["intType" | "floatType"]/><set>...</set></set>

属性动画的各种参数都比较好理解,在XML中可以定义ValueAnimatorObjectAnimator以及AnimatorSet,其中<set>标签对应AnimatorSet<animator>标签对应ValueAnimator,而<objectAnimator>则对应ObjectAnimator<set>标签的android:ordering属性有两个可选值:"together""sequentially",其中“together”表示所有动画同时播放,sequentially则表示动画集合中的子动画按照前后顺序一次播放,该属性默认值是together

<animator>标签下面不再介绍了,因为它和<objectAnimator>相比仅仅是少了一个propertyName属性,下面主要再说明一下<objectAnimator>标签各个属性

  • android:propertyName-表示属性动画作用对象的属性的名称
  • android:duration-表示动画的时长
  • android:valueFrom-表示属性的起始值
  • android:valueTo-表示属性的结束值
  • android:startOffset-表示动画的延迟时间,当动画开始后,需要延迟多少毫秒才会真正播放此动画
  • android:repeatCount-表示动画的重复次数,默认值为0,-1表示无限循环。
  • android:repeatMode-表示动画的重复模式,有repeatreverse两种类型,前者从头播放,后者一直反复倒着播放
  • android:valueType-表示android:propertyName所指定的属性的类型,有"intType"和"floatType"两个可选项,分别表示属性的类型为整型和浮点型,它告诉动画系统如何解释和处理valueFromvalueTo中的数值,像平移坐标类的都需要设成浮点型。另外,如果android:propertyName所指定的属性表示的是颜色,那么不需要指定valueType,系统会自动对颜色类型的属性做处理。

下面举个例子应用一下属性动画

<set xmlns:android="http://schemas.android.com/apk/res/android"android:ordering="together"><objectAnimatorandroid:propertyName="x"android:duration="1000"android:valueTo="100"android:valueType="floatType"/><objectAnimatorandroid:propertyName="y"android:duration="1000"android:valueTo="300"android:valueType="floatType"/>
</set>

这是res/animator目录下定义的动画

val view = findViewById<View>(R.id.view)
val set: AnimatorSet = AnimatorInflater.loadAnimator(this, R.animator.animator_set) as AnimatorSet
set.setTarget(view)
set.start()

这是活动中对应的代码,用AnimatorInflater加载对应的动画后给target对象设置,最后启动即可。

实际开发中建议使用代码来实现属性动画,一方面因为通过代码实现比较简单,更重要的是不同手机的屏幕长宽都是未知的,这种情况下必须动态地创建属性动画。

插值器和估值器

TimeInterpolator中文翻译为时间插值器,它的作用是根据时间流逝的百分比计算当前属性值改变的百分比,系统预置的有线性插值器(匀速动画)LinearInterpolator、加速减速插值器(两头快中间慢)AccelerateDecelerateInterpolator和减速插值器(越来越慢)DecelerateInterpolator等。

TypeEvaluator的中文翻译为类型估值算法,也就是估值器,它的作用是根据当前属性改变的百分比来计算改变后的属性值,系统预置的有IntEvaluator 针对整形属性,FloatEvaluator 针对浮点型属性和 ArgbEvaluator 针对color属性。

首先,动画系统会根据当前已过去的时间占总动画时长的比例,计算出一个时间进度百分比(input,范围0~1)。这个值被传递给插值器。插值器的核心作用是根据其特定的算法(如匀速、加速、减速或弹跳),将这个线性的时间进度百分比,映射为属性值应该改变的百分比。例如,在匀速动画中,时间过了一半(input=0.5),插值器返回的output值也是0.5;但在一个先加速后减速的动画中,时间过半时,属性值的改变可能已经完成了70%(output=0.7)。插值器返回的output值(即属性变化的百分比)仍然是一个相对比例。接下来,估值器开始工作。它接收三个关键参数:当前属性变化的百分比(fraction,即插值器的output值)属性的起始值(startValue)属性的结束值(endValue)。估值器利用一个数学公式,根据这些参数计算出当前帧具体的属性值。系统内置的IntEvaluatorFloatEvaluator所使用的标准计算公式是:当前属性值=startValue+fraction×(endValue−startValue)

根据不同的插值器,我们就可以做出千奇百怪的动画。来看这段代码,首先注释掉BounceInterpolator,让动画使用默认的线性插值器(即动画匀速进行。)

val textView = findViewById<TextView>(R.id.text)textView.setOnClickListener {val rotateAnimator = ObjectAnimator.ofFloat(textView, "rotation", 0f, 180f)rotateAnimator.duration = 2000//rotateAnimator.interpolator = BounceInterpolator()rotateAnimator.start()}

效果如下

在这里插入图片描述

将注释删掉启用BounceInterpolator插值器后

在这里插入图片描述

弹跳插值器之所以能产生弹跳效果,是因为它内部实现了一套复杂的、非线性的分段函数,这套函数使得动画的进度(输出值)在接近结束(input 接近 1)时,并非平滑地趋向于 1,模拟现实中的反弹效果。

除了系统内置的插值器和估值算法外,我们还可以自定义,实现方式也很简单,因为插值器和估值算法都是一个接口,内部也只有一个方法,我们只要派生一个类实现接口就可以了。自定义插值器就实现Interpolator或者TimeInterpolator,自定义估值算法需要实现TypeEvaluator。如果要对其他类型(非int,float,Color)做动画,那么必须自定义类型估值算法,比如一个对于一个简单的抛物线动画,我们需要操作横纵坐标x和y,就需要自定义一个估值器。

首先自定义一个坐标类

data class Point (val x: Float, val y: Float)

接着实现估值器

class ParabolicEvaluator(// 垂直方向加速度private val acceleration: Float
) : TypeEvaluator<Point?> {override fun evaluate(fraction: Float,startValue: Point?,endValue: Point?): Point? {val currentX = startValue!!.x + fraction * (endValue!!.x - startValue.x)// 计算垂直位移(匀加速运动)// 使用抛物线公式:y = (1/2)*a*t²val time = fraction // 时间因子val currentY = startValue.y + 0.5f * acceleration * time * timereturn Point(currentX, currentY)}
}

最后在代码中设置该估值器即可。由于Pointer是我们的自定义类,ValueAnimatorofObject()方法天然支持这种复杂数据类型的动画化,并且需要同时改变View的X和Y坐标,因此选择valueAnimator的AnimatorUpdateListener进行监听每一帧并手动改变属性。最后效果如下

val evaluator = ParabolicEvaluator(300f)val animator = ValueAnimator.ofObject(evaluator,Point(100f, 100f),  // 起始点Point(500f, 800f) // 结束点)
animator.setDuration(2000)animator.addUpdateListener(object : AnimatorUpdateListener {override fun onAnimationUpdate(animation: ValueAnimator) {val currentPoint = animation.getAnimatedValue() as Point// 更新View的位置textView.setX(currentPoint.x)textView.setY(currentPoint.y)}
})
textView.setOnClickListener { animator.start() }

在这里插入图片描述

属性动画的监听器

属性动画提供了监听器用于监听动画的播放过程,主要有如下两个接口:AnimatorUpdateListenerAnimatorListener

AnimatorListener的定义如下

public static interface AnimatorListener {void onAnimationStart(Animator animation);void onAnimationEnd(Animator animation);void onAnimationCancel(Animator animation);void onAnimationRepeat(Animator animation);
}

他可以监听动画的开始、结束、取消以及重复播放。同时Android还提供了AnimatorListenerAdapter类,它是AnimatorListener的适配器类,这样我们就可以有选择地实现这些方法

下面再看一下AnimatorUpdateListener的定义,该接口我们在上面定义抛物线定义用到了,还会在下面对View宽度做动画时用到。

public static interface AnimatorUpdateListener {void onAnimationUpdate(ValueAnimator animation);
}

AnimatorUpdateListener会监听整个动画过程,动画是由许多帧组成的,每播放一帧,onAnimationUpdate()就会被调用一次。

对任意属性做动画

val textView = findViewById<TextView>(R.id.text)val scaleAnimation = ScaleAnimation(1f, 2f, 1f, 1f,Animation.RELATIVE_TO_SELF,  // X轴基准类型:相对于自身0.5f,  // X轴基准点:自身宽度的50%(中心点)Animation.RELATIVE_TO_SELF,  // Y轴基准类型:相对于自身0.5f)scaleAnimation.duration = 2000scaleAnimation.fillAfter = truetextView.setOnClickListener {textView.animation = scaleAnimationtextView.startAnimation(scaleAnimation)}

用View动画对TextView设置缩放动画,一方面TextView实际宽度没有变化,另一方面连里面的文字也被拉长了

在这里插入图片描述

用属性动画如下

class MainActivity : AppCompatActivity() {private lateinit var drawable: AnimationDrawableoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)enableEdgeToEdge()setContentView(R.layout.activity_main)val textView = findViewById<TextView>(R.id.text)textView.setOnClickListener {val startWidth = textView.width.toFloat()val wrapView = WrapView(textView)ObjectAnimator.ofFloat(wrapView, "width", startWidth, startWidth * 2).setDuration(2000).start()}}class WrapView (val targetView: View) {fun setWidth (width: Float) {targetView.layoutParams.width = width.toInt()targetView.requestLayout()}fun getWidth(): Float {return targetView.layoutParams.width.toFloat()}}}
  1. 采用ValueAnimator,监听动画过程,在动画过程中修改对象的属性值,自己实现属性的改变。
class MainActivity : AppCompatActivity() {private lateinit var drawable: AnimationDrawableoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)enableEdgeToEdge()setContentView(R.layout.activity_main)val textView = findViewById<TextView>(R.id.text)val startWidth = textView.widthtextView.setOnClickListener {performAnimator(textView, startWidth, startWidth * 2)}}private fun performAnimator(target: View, start: Int, end: Int): Unit {val valueAnimator = ValueAnimator.ofInt(1, 100)valueAnimator.addUpdateListener {anim -> {val mEvaluator = IntEvaluator()val currentValue = anim.getAnimatedValue()val fraction = anim.animatedFractiontarget.layoutParams.width = mEvaluator.evaluate(fraction, start, end)target.requestLayout()}}valueAnimator.setDuration(3000).start()}
}

这里创建了一个ValueAnimator实例,ofFloat(0f, 1f)创建了一个用于生成从 0.0 到 1.0 平滑过渡的浮点数值的动画控制器。ValueAnimator会自动使用内置的估值器来计算从起始值到结束值之间每一帧的当前值。然后通过 float fraction = animator.getAnimatedFraction();获取动画进度比例。接着用target.layoutParams.width = mEvaluator.evaluate(fraction, start, end)通过估值器按比例计算出宽度再进行设置即可。

最后的效果如下,不过TextView 的文字绘制位置是相对于 自身左上角 的,动画过程中不断调用 requestLayout() 会重新布局,因此属性×2后就跑到了文字就相对跑到了左边去。

在这里插入图片描述

属性动画的工作原理

    public void start() {// See if any of the current active/pending animators need to be canceledAnimationHandler handler = sAnimationHandler.get();if (handler != null) {int numAnims = handler.mAnimations.size();for (int i = numAnims -1; i => 0; i--) {if (handler.mAnimations.get(i) instanceof ObjectAnimator) {ObjectAnimator anim = (ObjectAnimator) handler.mAnimations.get(i);if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {anim.cancel();}}}numAnims = handler.mPendingAnimations.size();for (int i = numAnims -1; i => 0; i--) {if (handler.mPendingAnimations.get(i) instanceof ObjectAnimator) {ObjectAnimator anim = (ObjectAnimator) handler.mPending-Animations.get(i);if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {anim.cancel();}}}numAnims = handler.mDelayedAnims.size();for (int i = numAnims -1; i => 0; i--) {if (handler.mDelayedAnims.get(i) instanceof ObjectAnimator) {ObjectAnimator anim = (ObjectAnimator) handler.mDelayedAnims.get(i);if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {anim.cancel();}}}}if (DBG) {Log.d(LOG_TAG,"Anim target,duration: " + getTarget() + "," + getDuration());for (int i = 0; i < mValues.length; ++i) {PropertyValuesHolder pvh = mValues[i];Log.d(LOG_TAG,"   Values[" + i + "]: " +pvh.getPropertyName() + "," + pvh.mKeyframes.getValue(0) + "," +pvh.mKeyframes.getValue(1));}}super.start();}                               

简单看一下这段代码,通过sAnimationHandler.get()获取当前线程的动画处理器,这是管理所有动画的核心组件,这里内部是使用ThreadLocal确保每个线程有独立的动画处理器实例。接着遍历三个动画列表:检查mAnimations(运行中动画)、mPendingAnimations(等待中动画)和mDelayedAnims(延迟动画)三个列表,通过anim.mAutoCancel(是否具有自动取消功能)和hasSameTargetAndProperties(anim)是否为true,即三个动画列表中有相同动画就将动画列表中相同动画取消掉,防止多个动画同时作用于同一属性而产生的冲突和视觉混乱。接着调用父类的super.start(),因为ObjectAnimator实际继承自ValueAnimator,接着看一下ValueAnimatorStart方法。

    private void start(boolean playBackwards) {if (Looper.myLooper() == null) {throw new AndroidRuntimeException("Animators may only be run on Looper threads");}mPlayingBackwards = playBackwards;mCurrentIteration = 0;mPlayingState = STOPPED;mStarted = true;mStartedDelay = false;mPaused = false;updateScaledDuration(); // in case the scale factor has changed since           creation timeAnimationHandler animationHandler = getOrCreateAnimationHandler();animationHandler.mPendingAnimations.add(this);if (mStartDelay == 0) {// This sets the initial value of the animation,prior to actually starting it runningsetCurrentPlayTime(0);mPlayingState = STOPPED;mRunning = true;notifyStartListeners();}animationHandler.start();}

if (Looper.myLooper() == null) { throw new AndroidRuntimeException("Animators may only be run on Looper threads"); }首先检查当前线程是否具有Looper,确保属性动画只能在有消息循环的线程(如主线程)中运行,这是Android动画系统的基本要求。后面是代码初始化动画的各种状态标志:mPlayingBackwards控制动画播放方向;mCurrentIteration重置迭代次数;mPlayingState设置为停止状态;mStarted标记动画已启动;

后面的代码涉及很多和底层交互的部分,我们直接跳到后面看animateValue的代码

    void animateValue(float fraction) {fraction = mInterpolator.getInterpolation(fraction);mCurrentFraction = fraction;int numValues = mValues.length;for (int i = 0; i < numValues; ++i) {mValues[i].calculateValue(fraction);}if (mUpdateListeners != null) {int numListeners = mUpdateListeners.size();for (int i = 0; i < numListeners; ++i) {mUpdateListeners.get(i).onAnimationUpdate(this);}}}

上述代码中的calculateValue方法就是计算每帧动画所对应的属性的值,下面着重看一下到底是在哪里调用属性的get和set方法的,毕竟这个才是我们最关 心的。 在初始化的时候,如果属性的初始值没有提供,则get方法将会被调用,请看Property-ValuesHoldersetupValue方法,可以发现get方法是通过反射来调用 的,如下所示。

    private void setupValue(Object target,Keyframe kf) {if (mProperty != null) {Object value = convertBack(mProperty.get(target));kf.setValue(value);}try {if (mGetter == null) {Class targetClass = target.getClass();setupGetter(targetClass);if (mGetter == null) {// Already logged the error -just return to avoid NPEreturn;}}Object value = convertBack(mGetter.invoke(target));kf.setValue(value);} catch (InvocationTargetException e) {Log.e("PropertyValuesHolder",e.toString());} catch (IllegalAccessException e) {Log.e("PropertyValuesHolder",e.toString());}}                        

当动画的下一帧到来的时候,PropertyValuesHolder中的setAnimatedValue方法会将新的属性值设置给对象,调用其set方法。从下面的源码可以看出,set方 法也是通过反射来调用的:

    void setAnimatedValue(Object target) {if (mProperty != null) {mProperty.set(target,getAnimatedValue());}if (mSetter != null) {try {mTmpValueArray[0] = getAnimatedValue();mSetter.invoke(target,mTmpValueArray);} catch (InvocationTargetException e) {Log.e("PropertyValuesHolder",e.toString());} catch (IllegalAccessException e) {Log.e("PropertyValuesHolder",e.toString());}}}
http://www.dtcms.com/a/586287.html

相关文章:

  • Lua中的可变参数
  • 夜晚的梦
  • 销售平台网站建设方案手机网站开发建设方案
  • Git 入门教程
  • 建行企业网站如何学编程入门教程
  • 做国外直播网站有哪些宜春做网站 黑酷seo
  • 重生归来,我要成功 Python 高手--day33 决策树
  • AI大模型全景图:十大核心能力与十大应用领域详解,附学习资源(建议收藏)
  • 学习Linux——软件管理
  • 广东品牌网站建设报价表网站建设中的财务预算
  • 什么网站的易用性重庆seo网站推广费用
  • 速卖通测评自养号技术:搭建安全稳定账号体系,流量销量双突破
  • 22-webpack案例:36kr
  • PCB批量处理命令
  • 网站建设 制作教程 pdf在山东和网页有关的公司
  • Go 语言接口
  • wordpress网站如何迁移电商网站建设课程
  • Keil编译出现:Missing Compiler Version 5
  • 十大免费网站推广平台有哪些创意设计素材
  • 为什么网站浏览不是做的那样农村自建房设计图软件
  • 如何设计一份精美到ppt
  • 网站如何做ssl认证计算机网站建设是什么
  • MySQL——数据库基础
  • 配置flutter鸿蒙的环境和创建并运行第一个flutter鸿蒙项目【精心制作】
  • UE核心架构概念
  • 叙述一个网站的建设过程免费网站建设绑定域名
  • 模型理解与可解释性图表案例解读之SHAP 瀑布图(Waterfall Plot)
  • 网站建设在哪个会计科目核算游戏币网站建设成本
  • 地方招聘网站如何做推广温州市城市建设档案馆网站
  • Robotiq 2F-85/2F-140夹爪:为具身智能科研搭建物理交互核心硬件支撑