Android动画——属性动画:ObjectAnimator

版权声明:全文原创,转载请注明作者:_过路 及来源: https://blog.csdn.net/lsl_1996/article/details/82895099

【注】本文全部内容均为原创,引用链接仅做学习指导,内容无任何摘抄及复制,原作可随时联系要求删除

——前言
笔者是一名行业菜鸟,对编程有浓厚的兴趣和热情,乐于分享
相比于大量专业名词和成篇的代码,笔者更喜欢用分隔的代码块和大量注释的形式
欢迎所有读者共同探讨,以及在发现错误时指正和批评,不胜感激!




- 背景

早前在设计一个项目的时候,有一个对图片的缩放和显示范围进行动画展示的需求,用于引导用户进行设备的初始化或者按键设置等,但是不能影响其他控件,当时在网上苦寻了很久的方案无果,查看了很多关于ObjectAnimator的思路,但自己总觉得有点没抓住重点,看了还是不会的感觉,经过自己慢慢摸索和实验,用ObjectAnimator + Matrix非常完美地解决了问题,最近在项目中又遇到了这个问题,在此记录一些自己的理解,希望能给同样在摸索中的读者一些值得借鉴的思路。关于Matrix的介绍在上一篇文章中做了一些基本介绍和简单的解释,如果想要对Matrix有个初始的了解可以查看:Android属性——Matrix矩阵。本篇就结合ObjectAnimator和Matrix使用做一个入门的介绍和简单的使用示例。



- 构思

在一开始构思这个效果的时候,最简单的也是最容易想到的方式就是预设固定的图片,在需要时设置显示,不需要提示时隐藏:

//需要提示时
imageView.setVisibility(View.VISIBLE);
...
//不需要提示时
imageView.setVisibility(View.INVISIBLE);

但是这种方式太过简单和死板界面也相当不优雅,并且也只能完成一些非常非常简单的视图展示效果,在需要动态展示一些过程的时候就很难实现预期效果了。于是想到了ValueAnimator——数值属性动画这个神奇的东西,ValueAnimator是一个非常强大的类。但是为什么最后还是没有用ValueAnimator呢?接下来本篇就会对ValueAnimator做一个简单的介绍,并解释其与ObjectAnimator的联系和异同。



- ValueAnimator 与 ObjectAnimator

提到这两个东东,就必须首先说明:ValueAnimatorObjectAnimator它爹(ObjectAnimator:???),ObjectAnimator是继承自ValueAnimator的,因此ObjectAnimator具有ValueAnimator的全部特性,并重写了ValueAnimator中的部分方法,定性地总结一句,ObjectAnimator具有更高的灵活性和便捷性
(这两个类名字可还真长啊,累死我了)


• ValueAnimator 的基本介绍

虎父无犬子,ObjectAnimator既然能被广泛使用,其父类ValueAnimator作为一个强大的属性动画类当然要放在前面先说说了。ValueAnimator提供了一系列改变属性的方式,在提供属性动画时,可以选择不同的数值增长方式控制属性值,线性增长、曲线增长等等,例如在缩放显示图像的过程中,如果选择线性增长,图片将匀速缩放,如果选择曲线增长,图片可以以加速或减速的方式缩放,并且还支持在动画执行过程中获取其值的分布和执行的过程监控等,但是! ValueAnimator本身并不会改变控件的属性,也不会直接与动画本身进行联系和控制,常用的创建一个ValueAnimator对象的方法是:ValueAnimator valueAnimator = ValueAnimator.ofXXX(xxx...);,其中XXX表示不同类型的数据(int、float、args等),xxx...表示变化范围,是一个可变长参数,例如给定ofInt(1, 0);,表示从1变为0,给定ofInt(1, 0, 1);表示从1变为0再变为1。

ValueAnimator实现动画的原理在于使用了:Interpolator(插值器)TypeEvaluator(数值计算器),通俗的说,Interpolator可以根据给出的值来设定数值的变化方式,例如上文中提到的线性和曲线方式。其他Interpolator可以参考下面引用博文内的配图(特别简单明了,大赞!)。

在缩放执行的过程中,动画效果主要就是靠这两个工具来实现的。当我们给出需要缩放的目标大小是从原大小(缩放率为1)放大到2倍(缩放率为2),缩放时间为1秒,并选择线性增长的Interpolator,那么TypeEvaluator就会生成一个时间长度为1秒,且缩放值从1到2的一系列数值,我们无法列举出每一个产生的数值,但是可以知道,当动画刚刚开始第0秒时,TypeEvaluator计算的缩放率是1;当动画结束的第1秒时,缩放率是2;而在中间第0.5秒时,缩放率是1.5……也就是说,TypeEvaluator会根据选定的Interpolator替我们计算好了某个时刻应该变化的具体值是多少,然后通过快速、连续地设定这个值来产生动画的效果。下面是改变一个ImageView的透明度的Demo,布局很简单,一个ImageView一个Button:

初始状态

点击Button改变透明度:

//初始化控件
ImageView ivImage = findViewById(R.id.ivImage);


//创建ValueAnimator对象,并给定透明度变化为从完全显示到完全消失
ValueAnimator valueAnimator = ValueAnimator.ofInt(1, 0);
//动画时间为1秒
valueAnimator.setDuration(1000);
//设置插值方式为线性插值,即透明度匀速变化
valueAnimator.setInterpolator(new LinearInterpolator() );
//开始动画
valueAnimator.start();


//监听属性数值的改变并手动做事件处理
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        //获取到改变的值
        int updateValue = animation.getAnimatedValue();
        //改变TextView的透明度
        ivImage.setAlpha(updateValue);
    }
});

看一下效果:

点击改变透明度

当然这里还要提到一个工具: AnimatorUpdateListener ,上文已经提到,ValueAnimator会自动替我们计算好需要改变的数值,但是对于Android系统而言,它怎么知道数值到达几了呢?又该如何做对应的事件反馈呢?这就用到了AnimatorUpdateListener了,顾名思义这是一个Listener,监听的就是ValueAnimator中TypeEvaluator产生的值,在ValueAnimator中,所有产生的值会根据给定的范围划分成比例的形式,例如由原大小缩放到2倍的动画中,缩放率为1.5时的比例就是0.5,AnimatorUpdateListener可以监听这个数TypeValue的变化,我们可以自行给TypeValue变化的值做一些事件处理,例如放大到一半的时候顺便改变一下透明度,等等,然而从上面的步骤来看,其实它本身完全没有接触任何一个控件,而是产生一系列的数值,再由我们人为地给控件指定对应的值来实现动画效果。以上就是关于ValueAnimator的一些简单介绍了,其实逻辑非常简单,通俗地理解,就是把我们人为while(true)的步骤交给Android提供的自带工具来处理。

总结一下ValueAnimator实现动画的步骤:

  1. 指定需要动画变换的值的范围
  2. 指定动画变换的时间
  3. 通过ValueAnimator对象指定动画的插值器变换方式Interpolator
  4. 通过ValueAnimator对象的TypeEvaluator产生值
  5. 在监听器AnimatorUpdateListener中监听数值的变化并指定事件处理

其他关于ValueAnimator的详细介绍,还可以参考下面的博文:


• ObjectAnimator 的基本介绍

上面介绍完了ValueAnimator,接下来就要说说其子类ObjectAnimator了。既然是子类,当然ValueAnimator具有的一切特性ObjectAnimator也具有,并且通过重写父类的部分方法,从而提供了更高的易用性。举个例子,在ValueAnimator中,由于本身并不接触控件,因此对动画的实现始终需要我们手动去做一些处理,但是ObjectAnimator就大大提高了普遍情况下的简易性——它可以直接控制控件的常规属性,例如透明度、 缩放、旋转等等,对于不同的控件,都可以直接指定控件拥有的属性,用同样的方式创建一个ObjectAnimator对象:
ObjectAnimator objectAnimator = ObjectAnimator.ofXXX(Object target, String propertyName, xxx...);

  1. target就是需要应用变化的控件
  2. propertyName是需要改变的控件具体要改变的属性名。
  3. **xxx…**是需要改变的范围,是一个可变长参数,与上文中ValueAnimator的一样,就不重述了。

这里主要讲一下propertyName的一个注意事项。在源码中,propertyName最后会被setPropertyName(String propertyName);方法调用,对于这个方法源码的解释有这么一段:

setPropertyName源码

意思就是,根据传入的String属性名,通过把首字母强制大写,并在前面加上"set",然后反射寻找对应的设置方法。例如要改变ImageView的透明度,我们可以直接用tvText.setAlpha(alpha);来实现,但在ObjectAnimator的参数中,属性名要写"alpha""Alpha",如果我们写成"setAlpha",最后查找的方法就变成了setSetAlpha();,这显然是不对的。

用ObjectAnimator同样实现一下设置ImageView的透明度1秒内逐渐消失的动画:

ObjectAnimator objectAnimator = ObjectAnimator.ofInt(ivImage, "alpha", 1, 0).setDuration(1000);
objectAnimator.start();

可以很明显地看到,这个方法指定了控件——ivImage,以及变化数值作用的属性的setter方法——setAlpha();,只需要一步,就能实现与上文ValueAnimator一样的效果,是不是方便的多了?

关于属性动画ValueAnimator 与 ObjectAnimator及其他更全面的介绍可以参考下面这篇大神的博文:



- ObjectAnimator 配合 Matrix

了解完ObjectAnimator的基本内容后,思考一个问题:怎么用ObjectAnimator实现复杂的变换?
在上文讨论的几个属性:透明度Alpha、缩放Scale、旋转Rotate等,几乎所有的View控件都具有直接设置的setter方法,可以很方便的直接套用ObjectAnimator来实现,但是如果是比较复杂的变化呢?比如,想要做一个由方形变为平行四边形的 “错切”过程动画又或者是想要以某个特定的点作为圆心进行缩放,这些在基本控件的内部并没有这么详细的setter方法,那要如何实现呢?

想要实现上述需求的方法,那就是利用Matrix啦!在前一篇博客中介绍了关于Matrix的一些基本内容,可以知道Matrix是控制控件变换的一个很强大的工具,并且控制的是控件内部的画面而不是控件本身(详情可查阅:Android属性——Matrix矩阵),那么如果能用ObjectAnimator不直接控制控件,而是控制一个Matrix,再由Matrix去控制控件,就能达到需求了。这么听起来,其实跟ValueAnimator非常像都是通过控制数值间接控制控件,因为毕竟ValueAnimator才是爹呀!(ObjectAnimator:?????)

其实前面讲了过程,小伙伴肯定也已经想到了一个非常简单的实现方式:重写View!在View中写一个接收Matrix的方法,通过这个方法把Matrix应用到图像中去实现变化效果。在Demo中,只是用了简单的Scale缩放,其实ImageView本身也提供了setScaleX();setScaleY();方法,可以通过PropertyValuesHolder来一次改变两个属性(下文会讲到),但是我就想一次性同比例缩放,并且这个Demo仅仅是提供一种解决思路,例如在使用Matrix进行Skew错切变换时,重写View实现就是一个好方法了,读者可以举一反三多多尝试其他需要间接改变的属性。
还是这个Demo,上代码!

首先因为我们需要自己的View中可以接收到Matrix,所以先写一个接口,定义接收Matrix的方法,然后在自定义的View中实现这个接口并重写接口方法:

//定义 接收Matrix方法的接口
public interface IView {
    void setScaleToMatrix(float scaleValue);
}

也可以不用接口直接在自定义View中添加方法,但养成良好的编程习惯总没有坏处。接下来自定义View继承自ImageView(根据需求选择继承)并实现接口方法:

//这是我自定义的ScaleImageView,继承自ImageView
public class ScaleImageView extends ImageView implements IView {

    //重用的Matrix
    Matrix matrix = new Matrix();

    //直接默认重写所有四个构造方法即可
    public ScaleImageView(Context context) {
        super(context);
    }
    public ScaleImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public ScaleImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    public ScaleImageView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }
    //实现并重写接口方法
    //我将该方法命名为setScaleMatrix
    //则ObjectAnimator中属性名String为“scaleMatrix”或“ScaleMatrix”
    @Override
    public void setScaleToMatrix(float scaleValue) {
        //布局中为了初始时视觉美观设置ScaleType为fit_center,需改成matrix否则Matrix变换不起作用
        this.setScaleType(ScaleType.MATRIX);
        //X、Y轴同比例缩放
        matrix.setScale(scaleValue, scaleValue);
        //调用ImageView自带的方法应用Matrix
        setImageMatrix(matrix);
    }
}
//MainActivity中就简单了,在需要执行动画的地方创建ObjectAnimator再指定变化范围即可
//初始化控件,略了
ScaleImageView sivImage;
......
//设置按键监听,略了
Button btButton;
......
//设置对应的属性和setter方法名,缩放范围:1~2,动画时间1秒
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(sivImage, "scaleToMatrix", 1f, 2f).setDuration(1000);
objectAnimator.start();

再看一下效果:

点击放大

可以看到,动画还是很好的,相比我们手动写成循环的方式,ObjectAnimator流畅多了,这里给一个小提示,因为Matrix如果采用set的方式时都会先重置为单位矩阵,因此不需要每次传值缩放时都重新new Matrix();,直接在自定义的View中声明并初始化一个Matrix,可以减少对象的创建,节约内存。


更多关于ObjectAnimator的详细资料可以继续阅读下面的文章


本文下半段将结合简单的应用和代码进行说明



- ObjectAnimator 多动画并行

现在我们的动画效果出来了,但是假如我又有一个需求,希望在放大的动画结束后再来个缩小的动画,怎么办呢?最直观的方法就是:在放大的动画下面在加一个缩小的动画不就行了吗?实践是检验真理的唯一标准,试一试就知道!在MainActivity调用动画的下面再加上缩小的动画:

//放大动画
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(sivImage, "scaleToMatrix", 1f, 2f).setDuration(1000);
objectAnimator.start();
//缩小动画
objectAnimator = ObjectAnimator.ofFloat(sivImage, "scaleMatrix", 2f, 1f).setDuration(1000);
objectAnimator.start();

结果发现最后的效果是:

放大接缩小动画

视觉上来看,仿佛放大的动画并没有执行,直接跳过到了缩小的动画,但其实放大动画是执行了的,之所以会突然放大一下,并不是因为放大动画执行完的原因,而是我们设置的缩小动画的起始大小就是2f——两倍,所以动画会先从两倍大小开始缩小,造成这样的原因是因为:ObjectAnimator在执行一个动画时,是非线程阻塞的 (这也是比我们自己用while()写更好的原因之一) ,在执行一个动画的时候,可以理解为objectAnimator.start();开始执行后,具体这个动画怎么变化,变多久,都交给后台处理 (其实这个说法是不严谨的,因为要更新UI只有主线程才能做到,后面整理清楚了会专门再发一个从源码角度的解释) ,然后主线程继续向下执行代码,而下一句代码就是缩小动画,于是放大动画刚刚开始,还没来得及展示,就已经开始执行缩小动画了,于是就变成了图中的效果。我们把第二个缩小的动画用改变透明度来替换一下,就直观多了!

//放大动画
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(sivImage, "scaleToMatrix", 1f, 2f).setDuration(1000);
objectAnimator.start();
//改变透明度
objectAnimator = ObjectAnimator.ofFloat(sivImage, "alpha", 1f, 0f).setDuration(1000);
objectAnimator.start();
放大接改变透明度

这个效果就很明显了,放大也是执行了的,改变透明度也是执行了的,因为每一次动画开始执行后就会继续向下执行,因此视觉上看起来很就是一起执行了。这也提供给了我们一个同时执行多个动画效果的思路:硬生生的把多个动画效果顺序提交执行。但是这也是非常非常不友好的实现方式,在每个动画效果比较简单、设备性能较好等情况下也许没有很严重的问题,但是如果动画复杂、处理量大,就会导致动画严重卡顿。于是这里就引出了 PropertyValuesHolder 这个工具,它和数据库中的Transaction事务管理非常像——要么不做、要么全做。


• PropertyValuesHolder属性值保存器

PropertyValuesHolder一次性记录多个要执行的动画,然后把自己传给ObjectAnimator,来实现多个动画同时并行 (事实上从底层上来看,CPU的处理在微观上依然单线程的,并且PropertyValuesHolder也有导致卡顿的风险) ,下面是PropertyValuesHolder的使用样例:

//通过PropertyValuesHolder存入需要改变的属性和值
PropertyValuesHolder pvhScale = PropertyValuesHolder.ofFloat("scaleToMatrix", 4.2f, 8.4f);
PropertyValuesHolder pvhAlpha = PropertyValuesHolder.ofFloat("alpha", 1f, 0f);

//通过ObjectAnimator应用到控件中
objectAnimator = ObjectAnimator.ofPropertyValuesHolder(sivImage, pvhScale, pvhAlpha);
objectAnimator.setDuration(1000);
objectAnimator.start();

从PropertyValuesHolder(太长了,简称PVH)的字面上就可以理解,PVH是一个属性取值的保持器,也就是说,一个PVH对象就存入了一个你需要改变值的属性以及这个属性需要变化的取值,但是PVH本身是不接触控件的,从这方面来看,PVH像极了ValueAnimator,不同点在于PVH依然需要针对具体的属性。动画效果与上面的“放大接改变透明度”一样,就不再重复贴图了。


• AnimatorSet动画集合

AnimatorSet是一个动画集合,与之类似的还有一个叫AnimationSet的东西,从功能性来说,AnimatorSet更为强大,AnimationSet实现的效果与PropertyValuesHolder相似,都是把动画组合在一起执行,而AnimatorSet强大的地方在于,不仅能让动画组合执行,还能通过指定顺序让多个动画依次执行,因此本文中仅对AnimatorSet进行简单的讲解,篇幅有限,此处就只列出基本的应用方法,细节原理和复杂应用考虑在日后补充。

先说说其中两个方法:

  1. AnimatorSet.playTogether(property1)
  2. playSequentially(property2)

这两个方法从字面上就很好理解了,playTogether()是同时进行,playSequentially()是逐一进行。这里property1和property2不是具体的参数,因为这两种方法都分别各有两种类型的参数。

playTogether()方法的参数可以是: Collection<Animator>Animator… (注意后者是可变长参数),先看后者很明显可以知道,毕竟这个方法是要实现“同时进行”,所以肯定支持多个动画一起作为可变长参数传入,第一个Collection<>就是集合,泛型是Animator类,也就是说Collection集合内的每一个元素都要是Animator的对象,这和可变长的Animator…是一样的。之所以是集合还是因为这个方法要同时执行所有动画,所以所有的动画在逻辑上是“一层”的,没有顺序、前后之分,在Java中就用集合来表示。

playSequentially()的方法参数可以是:List<Animator>和Animator…(注意后者依然是可变长参数),与上面的大同小异,传进去的都是一个或多个Animator的对象,但是这里是List列表而不是集合,就是因为这个方法是逐一进行,是有执行的前后顺序之分的,在Java中就用List来表示。

从参数来看,无论是哪一种,目的都是传Animator对象过去,也就是我们要先定义一个个Animator的对象,然后制定出每一个Animator需要做的动画效果,再把这些(或一个)Animator交给AnimatorSet替我们完成。在Demo中,我们可以这样运用:

//放大动画
ObjectAnimator zoomInAnim = ObjectAnimator.ofFloat(sivImage, "scaleToMatrix", 1f, 2f);
//缩小动画
ObjectAnimator zoomOutAnim = ObjectAnimator.ofFloat(sivImage, "scaleToMatrix", 2f, 1f);

//ObjectAnimator继承自ValueAnimator,而后者又继承自Animator
AnimatorSet animatorSet = new AnimatorSet();
//以下四种都是对的

//同时进行方式一
animatorSet.playTogether(zoomInAnim, zoomOutAnim);
//同时进行方式二
ArrayList<Animator> arrayList = new ArrayList<>();
arrayList.add(zoomInAnim);
arrayList.add(zoomOutAnim);
//关于Collection的详情可以参考网上,这里不细展开
Collection<Animator> collection = arrayList;
animatorSet.playTogether(collection);
animatorSet.setDuration(1000);
animatorSet.start();

//逐一进行方式一
animatorSet.playSequentially(zoomInAnim, zoomOutAnim);
//逐一进行方式二
List<Animator> list = new ArrayList<>();
list.add(zoomInAnim);
list.add(zoomOutAnim);
animatorSet.playSequentially(list);
animatorSet.setDuration(1000);
animatorSet.start();

除了上面两个方法外,还有以下这几个方法:

  1. play(Animator)
  2. with(Animator)
  3. before(Animator)
  4. after(delayTime)
  5. after(Animator)

依然从字面上看就很好理解,play()就是当前需要进行的动画,with(Animator)就是同时执行的动画,before(Animator)和after(Animator)就是需要在参数中的Animator前/后执行,而after(delayTime)方法是用来设置动画延迟执行的时间。具体的使用方式直接通过代码解释:

//Animator集合
AnimatorSet animatorSet = new AnimatorSet();
//定义四种动画:透明度减半,放大,缩小,旋转90°
ObjectAnimator animAlpha, animZoomIn, animZoomOut, animRotation;

animAlpha = ObjectAnimator.ofFloat(sivImage, "alpha", 1f, 0.5f);
animZoomIn = ObjectAnimator.ofFloat(sivImage, "scaleToMatrix", 1f, 2f);
animZoomOut = ObjectAnimator.ofFloat(sivImage, "scaleToMatrix", 2f, 1f);
animRotation = ObjectAnimator.ofFloat(sivImage, "rotation", 0f, 90f);

//设置动画的播放顺序
animatorSet.play(animAlpha).with(animZoomIn);
animatorSet.play(animZoomIn).before(animRotation);
animatorSet.play(animZoomOut).after(animRotation);
animatorSet.play(animRotation);

animatorSet.setDuration(1000);
animatorSet.start();

看一下我们预期的播放顺序,四个play操作从上到下的预期结果是:

  1. 执行透明度减半,同时执行放大
  2. 执行放大,并且放大在旋转前执行
  3. 执行缩小,之后执行旋转
  4. 执行旋转

可以看到,我们在play(Anim_1)动画后定义了with(anim_2)/before(anim_2)/after(anim_2)后,还需要在下面正式调用play(anim_2)才能完成播放,这是因为with、before、after只是制定了播放的顺序,并没有表示开始播放。并且,这三个方法是可以链式调用的,也即:play().before().after().with()...但是我们应当避免这种写法,以避免出现play(anim1).before(anim2).after(anim2)这种矛盾的情况。看一下实际的动画结果:

连续动画

确实和预期是相符的!AnimatorSet也确实是一个对动画进行控制非常非常强大的工具。

AnimatorSet的详解可以参考以下文章

AnimatorSet以及AnimationSet的区别可以参考以下文章


• AnimatorListener 动画监听

在做动画效果的时候,往往我们不会仅仅只是为了展示,而会接一些逻辑上的事情,比如Splash页面的动画在执行完后要进入主页、更改信息的动画执行完后要退回上一个界面、Tips引导动画执行完后要打开对应的操作窗口等等,但是上文我们也提到过,不论是ValueAnimator也好,ObjectAnimator也好,或者Animation视图动画也好,其动画的运行过程可以理解为是在后台执行的,而主线程依然会继续向下执行代码,因此如果我们只是简单地把需要操作的事务代码放在动画执行的后面,会导致事务和动画一起执行,那么我们到底要如何才能做到 “动画执行完之后操作事务” 呢?方法就是:利用AnimatorListener监听器。

同理,既然AnimatorListener是一个监听器,那它就需要被绑定到想要监听的动画上,动画监听有四个需要重写的方法:

  1. onAnimationStart(Animator animation){...} :动画开始时要做的
  2. onAnimationCancel(Animator animation){...} :动画被取消时要做的
  3. onAnimationRepeat(Animator animation){...} :动画重复执行时要做的
  4. onAnimationEnd(Animator animation){...} :动画结束时要做的

命名都很直观,第1个方法就是动画一开始要做的事,很简单就不多说了。这里再提一下第2、3个方法。
onAnimationCancel()是动画被取消时要做的,和end不同的是,cancel中的事务是我们人为地调用animator.cancel();方法时才会触发,而end是动画自己执行完后就会执行的。

至于onAnimationRepeat();方法,我们在设置一个动画不论是属性动画也好还是视图动画也好,都可以设置动画重复的次数:

objectAnimator.setRepeatCount(5);

onAnimationRepeat();方法就会在动画每次重复执行时调用,并且第一次执行也算在内,也就是说我们设置的repeatCount是几,这个方法就会调用几次。

这么一看马上就知道了,要在动画结束时执行某些事务,直接重写onAnimationEnd()方法即可。如果我们想要Demo中的图片无限重复放大再缩小再放大再缩小……最直观的的解决方案当然是利用死循环来做,但是前面也说了,while会导致主线程阻塞,对于比较简单的动画效果(例如本文的Demo,或者旋转等等规律步骤比较少的)可以进行如下设置:

objectAnimater.setRepeatCount(ValueAnimator.INFINITE);  // ValueAnimator.INFINITE是int型,值为1

这是Android官方给我们的方法,但是如果我们有一个非常非常复杂的动画(虽然基本上是没有这种需求的,Android的动画大都能通过分解转换成简单动画)比如我非要把:放大,旋转、对称、缩小、错切、平移、旋转……作为单次动画流程然后再无限循环这个流程的话,显然我们很难把这么多操作写在一个方法里完成,这时就可以用一个脑洞大开的方式曲线救国啦——AnimatorListener的迭代

· AnimatorListener的迭代

仔细想一下AnimatorListener,里面有onAnimationEnd()方法,再仔细想一下,只要我们设置的不是无限动画,那就都会在结束时调用监听。利用这个思路,我们可以定义一个动画执行步骤的变量,用来引导动画进行的顺序。在Demo中,仅用缩放作为示例,复杂的动画变换可以参考思路进行拓展。

先写出成员变量和动画监听部分,在按钮的点击事件中有一些处理放在下面讲。

/*
先让类实现OnclickListener和AnimatorListener接口,并在类中重写四个方法
具有大量操作时,尽量避免使用下述匿名内部类的形式:
objectAnimator.addListener(new AnimatorListener() {
    ......
});
*/

//定义一个动画步骤成员变量,用来指导动画执行的顺序
private int animStep = 0;

private ObjectAnimator objectAnimator;
private ScaleImageView sivImage;
private Button btButton;

//仅写出onAnimationEnd()部分
@Override
public void onAnimationEnd(Animatior animation) {
    //判断一下非空的情况,避免在重复点击时出现空指针异常
    if(animation != null) {
        switch(animStep) {
            case 0 : {
                //在点击按钮时进行放大,因此在放大动画结束后应该先缩小
                objectAnimator = ObjectAnimator.ofFloat(sivImage, "scaleToMatrix", 2f, 1f).setDuration(1000);
                //自增,指导第二个步骤
                animStep++;
                break;
            }
            case 1 : {
                //第二步为缩小,在Demo中只有两步,因此最后一步应把animStep重置
                objectAnimator = ObjectAnimator.ofFloat(sivImage, "scaleToMatrix", 1f, 2f).setDuration(1000);
                //有多个步骤时,前面的步骤执行完animStep都自增,最后一步执行完后重置
                animStep = 0;
                break;
            }
            defalut : break;
        }
        
        objectAnimator.start();
        //再把自己重新注册进监听器内继续监听onAnimationEnd()方法
        objectAnimator.addListener(this);
    }
}

接下来就是点击按钮开始动画的部分了。在具体的应用中,并不一定非要用Button来触发动画,这里只是专门做一个提醒来指出在使用手动触发动画时一个容易出错的地方。在Demo中,按钮主要用来启动动画的第一次执行,那么我们常常只会想到要这么写:

@Override
public void onClick(View v) {
    objectAnimator = ObjectAnimator.ofFloat(sivImage, "scaleToMatrix", 1f, 2f).setDuration(1000);
    objectAnimator.start();
    objectAnimator.addListener(this);
}

执行一下

初始点击事件

乍一看确实没什么问题,确实达到预期的效果了,但是如果我们这时多次点击按钮就会“翻车”

动画并行错误

原因也很简单,因为我们每次点击时都会触发一次重新执行动画,并且也会加入动画监听,也就相当于每次点击都会增加一个动画效果的运行线,每个动画效果又自行监听运行过程,所以就会造成这样的混乱场面,如果是上文中的一系列复杂动画,翻车现场只会更加惨烈~解决的办法也很简单,就是每次点击都让动画步骤初始化从第一步开始,然后取消原先的所有动画,并且取消原先绑定的监听器,然后再重新绑定和设定动画的效果。改进的点击事件代码如下:

@Override
public void onClick(View v) {
    //依然要进行一个非空判断,否则第一次运行时会报空指针异常
    //非空从逻辑上也表明了是“已经在执行动画的过程中再次点击”
    if(objectAnimator != null) {
        //取消当前的动画
        objectAnimator.cancel();
        //移除所有监听器,也可以单独仅移除AnimatorListener
        //仅移除某个监听器时,需要实例化具体的监听器并绑定动画
        objectAnimator.removeAllListeners();
    }
    //初始化动画步骤
    animStep = 0;
    //重新设置动画和监听
    objectAnimator = ObjectAnimator.ofFloat(ivImage, "scaleToMatrix", 1.05f, 2.1f).setDuration(1000);
    objectAnimator.start();
    objectAnimator.addListener(this);
}

改进完后再看,也就不会出现翻车现场啦!

改进的点击事件

这下这个“脑洞大开然鹅好像并没有什么卵用也不知道为什么要写这么多的需求 ”总算是解决了,代码而言是真的很简单也很少,但是思路和解决方案希望可以给同是初学的读者们一些灵感,用以运用在大型、复杂的项目中去。
本篇结合前一篇写的关于Matrix的文章,应该可以解决大部分情况下简单的动画场景了,但是这两篇博客的讲解可以说是非常皮毛的,甚至对于我自己的实际应用都远远不够,但还是那句话,本文主要目的是提供一种思维方式,再灵活运用进实际的项目中去。对于深入的ObjectAnimator、ValueAnimator、Matrix内容,还是应该细读官方的文档和源码为好。



- 结语

至此,我在CSDN的第一个入门博客系列——Matrix结合ObjectAnimator也算正式结束啦,如果后面有想到比较好的内容再来修改补充。也非常感谢每一位能仔细看到这里的读者,期待我们都能共同进步!

目前,正好项目中遇到了一些NDK方面的问题,前段时间手贱把AndroidStudio从2.2升级到了3.2,一大堆东西要改动,光是迁移项目就花了好几天时间 ,3.2的AndroidStudio变化非常大,光是Gradle的一些新语法就有很多地方要改,NDK的编译方式也开始重点推广 CMake ,相比以往的ndk-build,CMake在初期构建时稍微多那么几个步骤,但是利远大于弊,索性把原来工程中的各种原生库都改为CMake编译方式,过程中遇到了好几个坑,再加上新版本Gradle的一些改动,都正好就在下一篇博客里简单记录一下!

猜你喜欢

转载自blog.csdn.net/lsl_1996/article/details/82895099