Android的视图动画和属性动画在功能和使用上有一些明显的区别。
视图动画主要作用于视图,实现如缩放、旋转等效果。这种动画效果相对固定,只能应用于视图对象,且只能改变视图的大小和位置,而不能真正改变视图的属性。视图动画在Android 3.0以前的版本中广泛应用,但其在应用中的灵活性相对较低。
相比之下,属性动画在Android 3.0之后引入,其功能和灵活性都大大增强。属性动画可以对一个对象的属性进行操作,不仅能应用于视图对象,还能应用于任何对象。它不仅能实现缩放、旋转等效果,还能自定义动画效果,监听动画的过程,并在动画过程中或完成后执行特定的动作。属性动画通过改变对象的属性来实现动画,可以真正改变对象的属性。
总的来说,属性动画比视图动画更强大和灵活。它不仅可以实现视图动画的所有功能,还具有更多的自定义选项和更广泛的应用范围。
我们知道视图动画并不能真正影响VIew的属性,而view的相对位置,大小等都是view的属性,所以视图动画并不能真正的改变view,所以属性动画便有了价值。同时之前的视图动画不能动态的改变背景颜色什么的,而属性动画却可以。下面我们来看属性动画的一些相关类:
- ValueAnimator :Animator 的子类,实现了动画的整个处理逻辑,也是属性动画最为核心的类
- ObjectAnimator:对象属性动画的操作类,继承自ValueAnimator,通过该类使用动画的形式操作对象的属性
- TimeInterpolator:时间插值器,它的作用是根据时间流逝的百分比来计算当前属性值改变的百分比,系统预置的有线性插值器、加速减速插值器、减速插值器等。
- TypeEvaluator:TypeEvaluator翻译为类型估值算法,它的作用是根据当前属性改变的百分比来计算改变后的属性值
- Property:属性对象、主要定义了属性的set和get方法
- PropertyValuesHolder:持有目标属性Property、setter和getter方法、以及关键帧集合的类。
- KeyframeSet:存储一个动画的关键帧集
那么应该如何理解ValueAnimator呢?ValueAnimator可以看做一个动画时间值的处理分发器,他逻辑上并不直接操作view的属性。我们通过监听去获取到分发下来的值去对view进行操作,所以说,一个valueAnimator 分发下来的值只建议操作一个属性,写到一起也不是不行。那么如何获取到值的分发就很重要了。
ValueAnimator主要支持以下设置:
可以看到入参都是多个。支持int,argb,float,PropertyValuesHolder,TypeEvaluator等类型。
我们知道,视图动画支持缩放scale、alpha 透明度、rotate旋转、translate平移。而ValueAnimator 主要是基于时间将我们设置到值进行分发,所以我们需要获取到每个片的值的改变。这个就是上面提到的addUodateListenner
我们知道,view的位置的决定是由layout(l,t,r,b) 个参数决定的。
所以说,我们平移,就可以通过改变view.layout 中的值进行处理。
val animator=ValueAnimator.ofInt(0,400)
animator.duration = 1000
animator.addUpdateListener {
val curValue :Int= it.animatedValue as Int
binding.btn1.layout(curValue,0,curValue+binding.btn1.width,binding.btn1.height)
}
animator.start()
上面的代码,我们将一个view 在X轴方向上,从0移动到400px 的位置。整个动画耗时1秒。下面代码也是平移:
val animator=ValueAnimator.ofInt(0,400)
animator.duration = 1000
animator.addUpdateListener {
val curValue :Int= it.animatedValue as Int
binding.btn1.translationX=curValue.toFloat()
}
animator.start()
那么平移还有没有其他思路,我们知道view 还有一个滚动函数。如果说是平移内容,跑马灯效果,那么就可以调用scrollTo 函数。
view 也提供了scale 函数用于缩放view。那么我们尝试将整个view在1秒内从1变化到2.
val animator=ValueAnimator.ofFloat(1f,2f)
animator.duration = 1000
animator.addUpdateListener {
val curValue :Float= it.animatedValue as Float
binding.btn1.scaleX=curValue
}
animator.start()
可以看到,我们只是缩放了X轴,并没有缩放Y轴,缩放Y轴则是调用scaleY
view 提供了rotation、rotationX、rotationY
val animator=ValueAnimator.ofFloat(0f,360f)
animator.duration = 1000
animator.addUpdateListener {
val curValue :Float= it.animatedValue as Float
//binding.btn1.rotation=curValue
binding.btn1.rotationX=curValue
//binding.btn1.rotationY=curValue
}
animator.start()
可以看到,上面的代码,我们围绕X轴进行了翻转360度。
改变透明度分为改变整个view的透明度和改变view 背景的透明度。
val animator=ValueAnimator.ofFloat(1f,0f,1f)
animator.duration = 3000
animator.addUpdateListener {
val curValue :Float= it.animatedValue as Float
binding.btn1.alpha=curValue
}
animator.start()
可以看到,上面代码是将一个view的透明度从1改变到0完全透明在到1完全不透明。
val animator=ValueAnimator.ofFloat(255f,0f,255f)
animator.duration = 3000
animator.addUpdateListener {
val curValue :Float= it.animatedValue as Float
binding.btn1.background.alpha=curValue.toInt()
}
animator.start()
上面代码是将view的背景从完全不透明改到完全透明,再到完全不透明。
我们将按钮的颜色由黑色变化到红色。
val animator=ValueAnimator.ofArgb(Color.BLACK,Color.RED)
animator.duration = 1000
animator.addUpdateListener {
val curValue :Int= it.animatedValue as Int
binding.btn1.setTextColor(curValue)
}
animator.start()
}
可以看到,我们上面的几乎的所有写法都是基于addUpdateListener,然后去改变view的属性。所以说,限制我们的全是想象力。这个动画只要view属性支持,我们就可以整出各种花样来。
我们在视图动画里面,知道Android 系统给我们提供了很多插值器。哪些插值器,在属性动画中同样可用。默认的插值器是LinearInterpolator。
可以看懂LinearInterpolator 继承于BaseInterpolator。最终可以看到都是继承于TimeInterpolator,而这个接口只有一个函数。
float getInterpolation(float input);
而LinearInterpolator 则是原封不动的将input 给返回了,所以说,自定义插值器其实是对于input 进行计算,然后返回。在这个接口的描述中我们知道 input 的取值范围是0到1。这个用于表示动画执行时间的百分比。所以这个干预的是动画的时间。
通过自定义插值器的描述,我们可以知道,插值器只是对于动画时长的处理。但是我们通过构造器,发现可以传入很多种类型,而对不同类型的值的分发,就靠evaluator了。所以不同的值类型有自己的evaluator.所有的evaluator都实现 TypeEvaluator 接口。默认的有:
这个干预的是动画分发下来的值。我们看下IntEvaluator的实现代码:
public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
int startInt = startValue;
return (int)(startInt + fraction * (endValue - startInt));
}
他是开始位置+百分比*(结束位置-开始位置)。所以,evaluate会调用多次。
比如说,有一个需求,分发A到Z,因为涉及到动画,所以考虑写到动画里面。那么结合上面的知识,我们就需要自定义一个Evaluator.结合IntEvaluator 的经验。这里又一个知识点。就是每个char都有一个ASCII码,所以我们通过ASCII 码进行计算。
class CharEvaluator : TypeEvaluator {
override fun evaluate(fraction: Float, startValue: Char, endValue: Char): Char {
val start = startValue.code
val end = endValue.code
val cur = start + fraction * (end - start)
return cur.toInt().toChar()
}
}
class CharEvaluator : TypeEvaluator {
override fun evaluate(fraction: Float, startValue: Char, endValue: Char): Char {
val start = startValue.code
val end = endValue.code
val cur = start + fraction * (end - start)
return cur.toInt().toChar()
}
}
构造器里面有一个 **static ValueAnimator ofPropertyValuesHolder(PropertyValuesHolder… values) ** 。通过阅读ValueAnimator 源码可以发现,几乎所有的构造器都会生成一个ProPertyValuesHolder 对象,这个对象其中保存了动画过程中需要操作的属性和对应的值。所以我们可以根据这个对象构造动画,大致分为一下几个大类。
当然包含了不同入参的重载函数,可以看到propertyValuesHolder是允许传入多个的,我使用ofInt 等每次都只能生成一个PropertyValuesHolder。
val animator=ValueAnimator.ofPropertyValuesHolder(PropertyValuesHolder.ofFloat("a",0f,10f),PropertyValuesHolder.ofInt("b",1,50),PropertyValuesHolder.ofInt("c",100,50))
animator.duration = 1000
animator.addUpdateListener {
LogUtils.e("${it.getAnimatedValue("a")} ${it.getAnimatedValue("b")} ${it.getAnimatedValue("c")} ")
}
animator.start()
当我们设置多个Holderd的时候,可以调用 **Object getAnimatedValue(String propertyName) ** 获取对应的value。如果只有一个值,默认去的第一个,所以 it.animatedValue 就可以获取到。当然一个值也可以传递propertyName 进行获取。
我们想要控制动画的速率的变化,可以通过自定义插值器或者自定义Evaluator实现。但是这个涉及到数学知识,为了更方便的解决控制动画速率的问题,Google提供了一个新的思路,Keyframe,这个和帧动画的概念类似,动画将由具体帧决定,而不是计算。而Keyframe 则依旧和ValueAnimator一样,操作的是数据。可以看到keyframe 依旧支持3种类型的重载:
入参:
例如:keyFrame.ofFloat(0.25f,25) 表示动画进度是百分之25,数值是25。
val animator=ValueAnimator.ofPropertyValuesHolder(PropertyValuesHolder.ofKeyframe("a",
Keyframe.ofInt(0f,0),
Keyframe.ofInt(0.25f,25),
Keyframe.ofInt(0.50f,50),
Keyframe.ofInt(0.75f,75),
Keyframe.ofInt(1f,100),
))
animator.duration = 1000
animator.addUpdateListener {
LogUtils.e("${it.getAnimatedValue("a")} ")
}
animator.start()
通过Log可以看到,这个同样包含匀速的插值器,所以这个也可以设置插值器。和之前的逻辑是一致的,values 的值必须大于等于2,必须保证有一个开始和一个结束,所以Keyframe 必须大于等于2。
通过上面的例子,我们可以看到,ValueAnimator 只是对值进行了分发,一些属性的设置还需要我们自己写,于是就产生了ObjectAnimator类。结合ValueAnimator 实现动画的经验,ObjectAnimator 其实也就是在addUpdateListener 中帮助我们调用了函数,默认帮我们拼接了set,所以这个应该是反射实现的。
ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(binding.image, "alpha", 1.0f, 0.5f, 0.8f, 1.0f);
alphaAnim.setDuration(3000);
alphaAnim.start();
// 旋转
ObjectAnimator anim = ObjectAnimator.ofFloat(binding.image, "rotation", 0f, 360f);
// 动画时长
anim.setDuration(1000);
anim.start();
ObjectAnimator anim = ObjectAnimator.ofFloat(binding.image, "scaleX", 1.0f, 1.5f);
anim.setDuration(1000);
anim.start();
ObjectAnimator transXAnim = ObjectAnimator.ofFloat(binding.image, "translationX", 100, 400);
transXAnim.setDuration(3000);
transXAnim.start();
AnimatorSet set = new AnimatorSet();
ObjectAnimator animX = ObjectAnimator.ofFloat(binding.image, "scaleX", 1.0f, 1.5f);
ObjectAnimator animY = ObjectAnimator.ofFloat(binding.image, "scaleY", 1.0f, 1.5f);
// 同时动
set.playTogether(animX, animY);
set.setDuration(3000);
set.start();
AnimatorSet set = new AnimatorSet();
ObjectAnimator animX = ObjectAnimator.ofFloat(binding.image, "scaleX", 1.0f, 1.5f);
ObjectAnimator animY = ObjectAnimator.ofFloat(binding.image, "scaleY", 1.0f, 1.5f);
// 按照顺序动
set.playSequentially(animX, animY);
set.setDuration(3000);
set.start();
这么写的好处就是view 移除出屏幕就暂停动画了。而且便于recyclerview的复用
binding.image.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
// TODO 设置动画
}
@Override
public void onViewDetachedFromWindow(View v) {
v.clearAnimation();
}
});
TypeEvaluator evaluator, Object... values)
那么,当我们需要调用view的自定义函数呢?那么就需要使用这个函数了。还是上面的哪例子,我们A到Z,设置为text。这个时候,就需要用到反射的相关知识了,setText 的入参类型是CharSequence,但是它是通过values去获取到对应的class的,但是我们传入的参数是CharSequence的实现类,所以,基于values 是永远拿不到void setText(CharSequence text)这个函数的。但是我们可以偷换概念。既然65是ASCII码的A,那么我们就用int,然后自己写一个int 入参的函数调用setText.
val animator= ObjectAnimator.ofObject(this,"Text",IntEvaluator(),65,75)
animator.duration=1000
animator.start()
fun setText(code:Int){
binding.btn1.text = code.toChar().toString()
}
当然。写一个入参类型是string 的方法也行。
因为ValueAnimator 需要自己绑定属性,那么我们做动画的大多数都是基于ObjectAnimator。
我们知道动画是否循环播放是又动画本身控制的。所以AnimatorSet 不参与动画循环播放的控制。
用于更精细化的控制组合动画的播放。
通过ObjectAnimator 可以便捷的设置属性动画,那么还有没有更便捷的呢?那就是ViewPropertyAnimator。
binding.btn1.animate().setDuration(1000).translationX(10f).rotationX(360f).start()
因为这个并没有像ObjectAnimator 一样使用反射,而是通过计算出具体的的属性值,然后调用invalidata() 函数进行重绘,性能是要比ObjectAnimator 高一些,但是反射的性能影响微乎其微,所以他的优势是链式调度与简化了代码的读写。
如何通过XML 实现ValueAnimator,ObjectAnimator,AnimatorSet。我们通常不使用ValueAnimator,而是使用ObjectAnimator。
对应的标签为animator。通过animatorInflater.loadAnimator() 获取xml中的属性动画。
和animator 标签类似,只是多了一个属性:
这个在属性动画中特指animatorSet,用于实现多个属性动画的组合。
整体的属性动画都是还是基于一个分发器和操作view的属性去实现的。通常而言,还是写到代码里面的时候居多,而且通过ViewPropertyAnimator也可以实现一些简单的组合动画什么的。
Android 性能优化篇:https://qr18.cn/FVlo89
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap