Android属性动画笔记基础汇总

Android 视图动画与属性动画的区别

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呢?ValueAnimator可以看做一个动画时间值的处理分发器,他逻辑上并不直接操作view的属性。我们通过监听去获取到分发下来的值去对view进行操作,所以说,一个valueAnimator 分发下来的值只建议操作一个属性,写到一起也不是不行。那么如何获取到值的分发就很重要了。

ValueAnimator主要支持以下设置:

  • setDuration 设置动画执行时间。
  • setRepeatCount 重复次数 当等于ValueAnimator.INFINITE 表示循环。
  • setRepeatMode 设置循环播放模式,正序播放(RESTART)和逆序播放(REVERSE)。
  • setStartDelay 延时开始时间,第一次生效后,重复动画过程中将不再生效。
  • getAnimatedValue 获取当前运动点的值。
  • start 开始动画
  • cancel 取消动画
  • addUodateListenner 添加动画值的变化的监听。
  • addListener 添加动画监听。 开始start,结束end ,取消cancel,重复repeat
  • removeUpdateListener 移除更新值的监听,removeAllUpdateListeners 移除所有更新值的监听。
  • removelistener 移除动画监听,removeListeners 移除所有的动画监听
  • setStartDelay 延时多久开始动画,单位毫秒。
  • clone 完全克隆一个动画对象,包括他的所有设置及其监听器代码。

构造器

  • static ValueAnimator ofInt(int… values)
  • static ValueAnimator ofArgb(int… values)
  • static ValueAnimator ofFloat(float… values)
  • static ValueAnimator ofPropertyValuesHolder(PropertyValuesHolder… values)
  • static ValueAnimator ofObject(TypeEvaluator evaluator, Object… values)

可以看到入参都是多个。支持int,argb,float,PropertyValuesHolder,TypeEvaluator等类型。

如何实现视图动画的效果

我们知道,视图动画支持缩放scale、alpha 透明度、rotate旋转、translate平移。而ValueAnimator 主要是基于时间将我们设置到值进行分发,所以我们需要获取到每个片的值的改变。这个就是上面提到的addUodateListenner

translate 平移效果

我们知道,view的位置的决定是由layout(l,t,r,b) 个参数决定的。

  • l 视图的左边界相对于其父视图的位置。
  • t 顶部
  • r 右边
  • 底部

所以说,我们平移,就可以通过改变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 函数。

scale 缩放

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

roate 旋转

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度。

alpha 透明度

改变透明度分为改变整个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属性支持,我们就可以整出各种花样来。

自定义插值器与evaluator

我们在视图动画里面,知道Android 系统给我们提供了很多插值器。哪些插值器,在属性动画中同样可用。默认的插值器是LinearInterpolator。

自定义插值器

可以看懂LinearInterpolator 继承于BaseInterpolator。最终可以看到都是继承于TimeInterpolator,而这个接口只有一个函数。

float getInterpolation(float input);

而LinearInterpolator 则是原封不动的将input 给返回了,所以说,自定义插值器其实是对于input 进行计算,然后返回。在这个接口的描述中我们知道 input 的取值范围是0到1。这个用于表示动画执行时间的百分比。所以这个干预的是动画的时间。

evaluator

通过自定义插值器的描述,我们可以知道,插值器只是对于动画时长的处理。但是我们通过构造器,发现可以传入很多种类型,而对不同类型的值的分发,就靠evaluator了。所以不同的值类型有自己的evaluator.所有的evaluator都实现 TypeEvaluator 接口。默认的有:

  • IntEvaluator
  • FloatEvaluator
  • ArgbEvaluator

这个干预的是动画分发下来的值。我们看下IntEvaluator的实现代码:

public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
    int startInt = startValue;
    return (int)(startInt + fraction * (endValue - startInt));
}

他是开始位置+百分比*(结束位置-开始位置)。所以,evaluate会调用多次。

使用ofObject 分发想要的内容

比如说,有一个需求,分发A到Z,因为涉及到动画,所以考虑写到动画里面。那么结合上面的知识,我们就需要自定义一个Evaluator.结合IntEvaluator 的经验。这里又一个知识点。就是每个char都有一个ASCII码,所以我们通过ASCII 码进行计算。

class CharEvaluator : TypeEvaluator<Char> {
    
    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()
    }
}

        val animator=ValueAnimator.ofObject(CharEvaluator(),'A','Z')
        animator.duration = 1000
        animator.addUpdateListener {
            val curValue :Char= it.animatedValue as Char
            binding.btn1.text = curValue.toString()
        }
        animator.start()

propertyValuesHolder与关键帧KeyFrame

构造器里面有一个 **static ValueAnimator ofPropertyValuesHolder(PropertyValuesHolder… values) ** 。通过阅读ValueAnimator 源码可以发现,几乎所有的构造器都会生成一个ProPertyValuesHolder 对象,这个对象其中保存了动画过程中需要操作的属性和对应的值。所以我们可以根据这个对象构造动画,大致分为一下几个大类。

  • ofFloat
  • ofint
  • ofKeyFrame,关键帧
  • ofMultFloat
  • ofMultInt
  • ofObject

当然包含了不同入参的重载函数,可以看到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 进行获取。

keyFrame 关键帧

我们想要控制动画的速率的变化,可以通过自定义插值器或者自定义Evaluator实现。但是这个涉及到数学知识,为了更方便的解决控制动画速率的问题,Google提供了一个新的思路,Keyframe,这个和帧动画的概念类似,动画将由具体帧决定,而不是计算。而Keyframe 则依旧和ValueAnimator一样,操作的是数据。可以看到keyframe 依旧支持3种类型的重载:

  • ofFloat
  • ofInt
  • ofObject

入参:

  • fraction 表示当前显示的进度,即插值器中getInterpolation 函数的返回值。
  • value 表示动画当前所在的数值位置,即animateValue .

例如: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。

ObjectAnimator

通过上面的例子,我们可以看到,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();

组合同时缩放X与Y

            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();

组合顺序缩放X与Y

            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 生命周期绑定动画

这么写的好处就是view 移除出屏幕就暂停动画了。而且便于recyclerview的复用

 binding.image.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
            @Override
            public void onViewAttachedToWindow(View v) {
                // TODO 设置动画 
            }

            @Override
            public void onViewDetachedFromWindow(View v) {
                v.clearAnimation();
            }
        });

自定义属性与ofObject(Object target, String propertyName,
        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 的方法也行。

组合动画 AnimatorSet

因为ValueAnimator 需要自己绑定属性,那么我们做动画的大多数都是基于ObjectAnimator。

  • playAwquentially 动画依次执行。
  • playTogether 动画一起执行。
  • setDuration 单个动画的时长,优先级高于子动画的时长。
  • setInterPolator 插值器,优先级高于子动画。
  • setTarget 目标动画,这个优先级也高于子动画。
  • setStartDelay 设置延时开始动画时长,仅争对AnimatorSet的激活时长,对单个动画的延时设置没有影响。

我们知道动画是否循环播放是又动画本身控制的。所以AnimatorSet 不参与动画循环播放的控制。

animatorSet.builder

用于更精细化的控制组合动画的播放。

  • play 播放那个动画
  • with 和前面动画一起播放。
  • before 先执行这个动画,再执行前面的动画。
  • after 前面的动画执行完成后,才执行这个动画
  • after 延迟n毫秒后执行动画。
animatorSet 监听器AnimatorListener
  • onAnimationStart 开始
  • end 结束
  • cancel 取消
  • repeat 重复

ViewPropertyAnimator

通过ObjectAnimator 可以便捷的设置属性动画,那么还有没有更便捷的呢?那就是ViewPropertyAnimator。

binding.btn1.animate().setDuration(1000).translationX(10f).rotationX(360f).start()

因为这个并没有像ObjectAnimator 一样使用反射,而是通过计算出具体的的属性值,然后调用invalidata() 函数进行重绘,性能是要比ObjectAnimator 高一些,但是反射的性能影响微乎其微,所以他的优势是链式调度与简化了代码的读写。

在xml中的实现

如何通过XML 实现ValueAnimator,ObjectAnimator,AnimatorSet。我们通常不使用ValueAnimator,而是使用ObjectAnimator。

ValueAnimator

对应的标签为animator。通过animatorInflater.loadAnimator() 获取xml中的属性动画。

  • duration 动画时长。
  • valueFrom 初始动画的值,为float,int和color种类型。
  • valueTo 同样3个类型的值。
  • startOffset 动画激活延时,单位毫秒。
  • repeatConunt 重复次数,infinite 表示无限循环
  • repeatMode 重复模式。repeat 正序,reverse 倒序。
  • valueType 表示数值类型,和to、from 对应,如果是color 就不需要设置这个值。
  • interpolator 设置插值器。

objectAnimator 标签

和animator 标签类似,只是多了一个属性:

  • propertyName 对应要操作的属性的名称。

set标签

这个在属性动画中特指animatorSet,用于实现多个属性动画的组合。

总结

整体的属性动画都是还是基于一个分发器和操作view的属性去实现的。通常而言,还是写到代码里面的时候居多,而且通过ViewPropertyAnimator也可以实现一些简单的组合动画什么的。

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
img
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

欢迎大家一键三连支持,若需要文中资料,直接扫描文末CSDN官方认证微信卡片免费领取↓↓↓(文末还有ChatGPT机器人小福利哦,大家千万不要错过)

PS:群里还设有ChatGPT机器人,可以解答大家在工作上或者是技术上的问题

图片

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值