自定义控件三部曲之动画篇——联合动画的代码实现

Posted 启舰

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自定义控件三部曲之动画篇——联合动画的代码实现相关的知识,希望对你有一定的参考价值。

前言:为了梦想,行色匆匆,是否会错过眼前的风景?有时也会懊悔,为何当时没能好好享受时光,但如果当时真的跟他人一样,是否现在也会跟他人一样羡慕现在的自己?

相关博客:
《Android自定义控件三部曲文章索引》

上几篇给大家分别讲了ValueAnimator和ObjectAnimator,相比而言ObjectAnimator更为方便而且由于set函数是在控件类内部实现,所以封装性更好。而且在现实使用中一般而言都是使用ObjectAnimator的机率比较大。
但ValueAnimator和ObjectAnimator都只能单单实现一个动画,那如果我们想要使用一个组合动画,比如边放大,边移动,边改变alpha值,要怎么办。对于这种组合型的动画,谷歌给我们提供了一个类AnimatorSet;这篇我们就着重来看看组合动画的实现方法吧。
###一、AnimatorSet——playSequentially,playTogether>
首先,AnimatorSet针对ValueAnimator和ObjectAnimator都是适用的,但一般而言,我们不会用到ValueAnimator的组合动画,所以我们这篇仅讲解ObjectAnimator下的组合动画实现。
在AnimatorSet中直接给为我们提供了两个方法playSequentially和playTogether,playSequentially表示所有动画依次播放,playTogether表示所有动画一起开始。
####1、playSequentially
我们先来看看playSequentially的声明:

public void playSequentially(Animator... items);
public void playSequentially(List<Animator> items);

这里有两种声明,第一个是我们最常用的,它的参数是可变长参数,也就是说我们可以传进去任意多个Animator对象。这些对象的动画会逐个播放。第二个构造函数,是传进去一个List< Animator>的列表。原理一样,也是逐个去取List中的动画对象,然后逐个播放。但使用起来稍微麻烦一些。
下面我们就举例来看一下playSequentially的使用方法,先来看下效果:

从效果图中可以看到,首先改变了textview1的颜色,结束后移动textview1,在移动结束后,开始移动黄色的textview;所以这就是playSequentially的效果,即逐个播放动画,一个动画结束后,播放下一个动画
下面我们来看实现代码:
#####(1)、main.xml布局
从效果图中也可以看出布局非常简单,就三个控件。代码如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:orientation="vertical"
                android:layout_width="fill_parent"
                android:layout_height="fill_parent">

    <Button
            android:id="@+id/btn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:padding="10dp"
            android:text="start anim"
            />

    <TextView
            android:id="@+id/tv_1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_marginRight="30dp"
            android:layout_centerVertical="true"
            android:background="#ff00ff"
            android:padding="10dp"
            android:text="textview1"
            />
    <TextView
            android:id="@+id/tv_2"
            android:layout_width="100dp"
            android:layout_height="wrap_content"
            android:layout_centerHorizontal="true"
            android:layout_centerVertical="true"
            android:gravity="center"
            android:padding="10dp"
            android:background="#ffff00"
            android:text="Hello qijian"/>

</RelativeLayout>

这里也没有什么需要注意的地方,下面我们就直接来看MyActivity的代码:
#####(2)、MyActivity.java

public class MyActivity extends Activity 
    private Button mButton;
    private TextView mTv1, mTv2;

    @Override
    public void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        mButton = (Button) findViewById(R.id.btn);
        mTv1 = (TextView) findViewById(R.id.tv_1);
        mTv2 = (TextView) findViewById(R.id.tv_2);

        mButton.setOnClickListener(new View.OnClickListener() 
            @Override
            public void onClick(View v) 
                doPlaySequentiallyAnimator();
            
        );
    
    …………

这段代码也没什么难度,首先是初始化textview1,textview2和btn的对象,然后当点击按钮时执行doPlaySequentiallyAnimator();函数。下面我们来看看doPlaySequentiallyAnimator()的具体实现:

private void doPlaySequentiallyAnimator()
    ObjectAnimator tv1BgAnimator = ObjectAnimator.ofInt(mTv1, "BackgroundColor",  0xffff00ff, 0xffffff00, 0xffff00ff);
    ObjectAnimator tv1TranslateY = ObjectAnimator.ofFloat(mTv1, "translationY", 0, 300, 0);
    ObjectAnimator tv2TranslateY = ObjectAnimator.ofFloat(mTv2, "translationY", 0, 400, 0);

    AnimatorSet animatorSet = new AnimatorSet();
    animatorSet.playSequentially(tv1BgAnimator,tv1TranslateY,tv2TranslateY);
    animatorSet.setDuration(1000);
    animatorSet.start();

这里首先构造了三个动画,针对textview1的是前两个tv1BgAnimator和tv1TranslateY:分别是改变当前动画背景和改变控件Y坐标位置;针对textview2则只是通过translationY来改变控件Y坐标位置。有关动画的创建方式,我这里就不再讲了,不理解的同学请参考上篇《Animation动画详解(七)——ObjectAnimator基本使用》
然后是利用AnimatorSet的playSequentially函数将这三个动画组装起来,逐个播放。代码比较简单,就不再细讲。这篇我们就会在这个demo的基础上来讲解本篇所有的知识点。
源码在文章底部给出
####2、playTogether
playTogether表示将所有动画一起播放
我们先来看看playTogether的声明:

public void playTogether(Animator... items);
public void playTogether(Collection<Animator> items);

同样这里也是有两个构造函数,他们两个的意义是一样的,只是传入的参数不一样,第一个依然是传可变长参数列表,第二个则是需要传一个组装好的Collection<Animator>对象。
下面我们在上面例子的基础上,看看playTogether函数的用法;
先来看看效果图:

从效果图中可以看到,所有动画是一起开始播放的,下面来看看代码:
当点击控钮时,执行以下代码:

ObjectAnimator tv1BgAnimator = ObjectAnimator.ofInt(mTv1, "BackgroundColor",  0xffff00ff, 0xffffff00, 0xffff00ff);
ObjectAnimator tv1TranslateY = ObjectAnimator.ofFloat(mTv1, "translationY", 0, 400, 0);
ObjectAnimator tv2TranslateY = ObjectAnimator.ofFloat(mTv2, "translationY", 0, 400, 0);

AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(tv1BgAnimator,tv1TranslateY,tv2TranslateY);
animatorSet.setDuration(1000);
animatorSet.start();

同样是上面的那三个动画,只是将playSequentially改为了playTogether;
源码在文章底部给出
####3、playSequentially,playTogether真正意义
想必大家都看到赛马,在赛马开始前,每个马都会被放在起点的小门后面,到点了,门打开,马开始一起往前跑。而假如我们把每匹马看做是一个动画,那我们的playTogether就相当于赛马场里每个赛道上门的意义(当比赛开始时,每个赛道上的门会打开,马就可以开始比赛了);也就是说,playTogether只是一个时间点上的一起开始,对于开始后,各个动画怎么操作就是他们自己的事了,至于各个动画结不结束也是他们自已的事了。所以最恰当的描述就是门只负责打开,打开之后马咋跑,门也管不着,最后,马回不回来跟门也没啥关系。门的责任只是到点就打开而已。放在动画上,就是在激活动画之后,动画开始后的操作只是动画自己来负责。至于动画结不结束,也只有动画自己知道。
而playSequentially的意义就是当一匹马回来以后,再放另一匹。那如果上匹马永远没回来,那下一匹马也永远不会被放出来。
放到动画上,就是把激活一个动画之后,动画之后的操作就是动画自己来负责了,这个动画结束之后,再激活下一个动画。如果上一个动画没有结束,那下一个动画就永远也不会被激活。
我们首先用playTogether来看个例子:

ObjectAnimator tv1BgAnimator = ObjectAnimator.ofInt(mTv1, "BackgroundColor",  0xffff00ff, 0xffffff00, 0xffff00ff);

ObjectAnimator tv1TranslateY = ObjectAnimator.ofFloat(mTv1, "translationY", 0, 400, 0);
tv1TranslateY.setStartDelay(2000);
tv1TranslateY.setRepeatCount(ValueAnimator.INFINITE);

ObjectAnimator tv2TranslateY = ObjectAnimator.ofFloat(mTv2, "translationY", 0, 400, 0);
tv2TranslateY.setStartDelay(2000);

AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(tv1BgAnimator,tv1TranslateY,tv2TranslateY);
animatorSet.setDuration(2000);
animatorSet.start();

在这个例子中,我们将tv1TranslateY开始延迟2000毫秒开始,并设为无限循环。tv2TranslateY设为开始延迟2000毫秒。而tv1BgAnimator则是没有任何设置,所以是默认直接开始。我们来看效果图:

在效果图中可以看到,在点击按钮以后,先进行的是tv1的颜色变化,在颜色变化完以后,tv2的延时也刚好结束,此时两个textview开始位移变换。最后textview1的位移变换是无限循环的。
所以从这个例子中也可以看到,playTogether只是负责在同一时间点把门拉开,拉开门以后,马跑不跑,那是它自己的事了,回不回来,门也管不着。
playSequentially也是一样,只是一个回来结束以后,才打开另一个的门。如果上一个一直没回来,那下一个也是永远不会开始的。

通过这个例子,我想告诉大家:playTogether和playSequentially在开始动画时,只是把每个控件的动画激活,至于每个控件自身的动画是否具有延时、是否无限循环,只与控件自身的动画设定有关,与playTogether、playSequentially无关。playTogether和playSequentially只负责到点激活动画。

我们再来看一个例子:

ObjectAnimator tv1BgAnimator = ObjectAnimator.ofInt(mTv1, "BackgroundColor",  0xffff00ff, 0xffffff00, 0xffff00ff);
tv1BgAnimator.setStartDelay(2000);

ObjectAnimator tv1TranslateY = ObjectAnimator.ofFloat(mTv1, "translationY", 0, 300, 0);
tv1TranslateY.setRepeatCount(ValueAnimator.INFINITE);

ObjectAnimator tv2TranslateY = ObjectAnimator.ofFloat(mTv2, "translationY", 0, 400, 0);

AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playSequentially(tv1BgAnimator,tv1TranslateY,tv2TranslateY);
animatorSet.setDuration(2000);
animatorSet.start();

同样是那三个动画,首先tv1BgAnimator设置了延时开始,tv1TranslateY设置为无限循环;使用playSequentially来逐个播放这三个动画,首先是tv1BgAnimator:在开始之后,这个动画会延时2000毫秒再开始。结束之后,激活tv1TranslateY,这个动画会无限循环。无限循环也就是说它永远也不会结束。那么第三个动画tv2TranslateY也永远不会开始。下面来看看效果图:

在效果图中也可以看出,textview1先是等了一段时间然后开始背景色变化,然后开始无限循环的上下运动。另一个textview永远也不会开始动画了。
源码在文章底部给出

通过上面两个例子,总结的时候到了:

  • 第一:playTogether和playSequentially在激活动画后,控件的动画情况与它们无关,他们只负责定时激活控件动画。
  • 第二:playSequentially只有上一个控件做完动画以后,才会激活下一个控件的动画,如果上一控件的动画是无限循环,那下一个控件就别再指望能做动画了。
    ####4、如何实现无限循环动画
    很多同学会一直纠结如何实现无限循环的组合动画,因为AnimatorSet中没有设置循环次数的函数!通过上面的讲解,我们也能知道是否无限循环主要是看动画本身,与门(playTogether)无关!
    下面我们就实现三个动画同时开始并无限循环的动画:
ObjectAnimator tv1BgAnimator = ObjectAnimator.ofInt(mTv1, "BackgroundColor",  0xffff00ff, 0xffffff00, 0xffff00ff);
tv1BgAnimator.setRepeatCount(ValueAnimator.INFINITE);
ObjectAnimator tv1TranslateY = ObjectAnimator.ofFloat(mTv1, "translationY", 0, 400, 0);
tv1TranslateY.setRepeatCount(ValueAnimator.INFINITE);
ObjectAnimator tv2TranslateY = ObjectAnimator.ofFloat(mTv2, "translationY", 0, 400, 0);
tv2TranslateY.setRepeatCount(ValueAnimator.INFINITE);

AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(tv1BgAnimator,tv1TranslateY,tv2TranslateY);
animatorSet.setDuration(2000);
animatorSet.start();

上面的代码很容易理解,我们为每个动画设置了无限循环,所以在playTogether指定开始动画之后,每个动画都是无限循环的。
效果图如下:

总之:playTogether和playSequentially只是负责指定什么时候开始动画,不干涉动画自己的运行过程。换言之:playTogether和playSequentially只是赛马场上的每个赛道的门,门打开以后,赛道上的那匹马怎么跑跟它没什么关系。
源码在文章底部给出
###二、自由设置动画顺序——AnimatorSet.Builder
####1、概述
上面我们讲了playTogether和playSequentially,分别能实现一起开始动画和逐个开始动画。但并不是非常自由的组合动画,比如我们有三个动画A,B,C我们想先播放C然后同时播放A和B。利用playTogether和playSequentially是没办法实现的,所以为了更方便的组合动画,谷歌的开发人员另外给我们提供一个类AnimatorSet.Builder;
我们这里使用AnimatorSet.Builder实现下面这个效果:
即两个控件一同开始动画

我们直接来看实现的代码:

ObjectAnimator tv1BgAnimator = ObjectAnimator.ofInt(mTv1, "BackgroundColor",  0xffff00ff, 0xffffff00, 0xffff00ff);
ObjectAnimator tv1TranslateY = ObjectAnimator.ofFloat(mTv1, "translationY", 0, 400, 0);

AnimatorSet animatorSet = new AnimatorSet();
AnimatorSet.Builder builder = animatorSet.play(tv1BgAnimator);
builder.with(tv1TranslateY);
animatorSet.start();

关键部分在最后几句:

AnimatorSet animatorSet = new AnimatorSet();
AnimatorSet.Builder builder = animatorSet.play(tv1BgAnimator);
builder.with(tv1TranslateY);

首先是构造一个AnimatorSet对象。然后调用animatorSet.play(tv1BgAnimator)方法生成一个AnimatorSet.Builder对象,直接调用builder.with()就能实现两个控件同时做动画了,多么神奇,下面我们来看看这个AnimatorSet.Builder的定义!
源码在文章底部给出
####2、AnimatorSet.Builder函数
从上面的代码中,我们可以看到AnimatorSet.Builder是通过animatorSet.play(tv1BgAnimator)生成的,这是生成AnimatorSet.Builder对象的唯一途径!

//调用AnimatorSet中的play方法是获取AnimatorSet.Builder对象的唯一途径
//表示要播放哪个动画
public Builder play(Animator anim)

在上面的例子中,我们已经接触AnimatorSet.Builder的with(Animator anim)函数,其实除了with函数以外,AnimatorSet.Builder还有一些函数,声明如下:

//和前面动画一起执行
public Builder with(Animator anim)
//执行前面的动画后才执行该动画
public Builder before(Animator anim)
//执行先执行这个动画再执行前面动画
public Builder after(Animator anim)
//延迟n毫秒之后执行动画
public Builder after(long delay)

play(Animator anim)表示当前在播放哪个动画,另外的with(Animator anim)、before(Animator anim)、after(Animator anim)都是以play中的当前所播放的动画为基准的。

比如,当play(playAnim)与before(beforeAnim)共用,则表示在播放beforeAnim之前,先播放playAnim动画;同样,当play(playAnim)与after(afterAnim)共用时,则表示在在播放afterAnim动画之后,再播放playAnim动画。

上面每个函数的意义很好理解,这里要格外注意一点,他们每个函数的返回值都是Builder对象,也就是说我们有两种方式使用他们:
#####方式一:使用builder对象逐个添加动画

AnimatorSet.Builder builder = animatorSet.play(tv1TranslateY);
builder.with(tv2TranslateY);
builder.after(tv1BgAnimator);

#####方式二:串行方式
由于每个函数的返回值都是Builder对象,所以我们是依然可以直接调用Builder的所有函数的,所以就可以用串行的方式把他们一行串起来,所以上面的代码我们也可以写成下面的简化方式:

animatorSet.play(tv1TranslateY).with(tv2TranslateY).after(tv1BgAnimator);

下面我们就举个例子来看一下他们的用法,这里实现的效果是:在tv1颜色变化后,两个控件一同开始位移动画:

ObjectAnimator tv1BgAnimator = ObjectAnimator.ofInt(mTv1, "BackgroundColor",  0xffff00ff, 0xffffff00, 0xffff00ff);
ObjectAnimator tv1TranslateY = ObjectAnimator.ofFloat(mTv1, "translationY", 0, 400, 0);
ObjectAnimator tv2TranslateY = ObjectAnimator.ofFloat(mTv2, "translationY", 0, 400, 0);

AnimatorSet animatorSet = new AnimatorSet();
animatorSet.play(tv1TranslateY).with(tv2TranslateY).after(tv1BgAnimator);
animatorSet.setDuration(2000);
animatorSet.start();

上面的代码比较简单,就不再讲了,看下效果图:

源码在文章底部给出
###三、AnimatorSet监听器
在AnimatorSet中也可以添加监听器,对应的监听器为:

public static interface AnimatorListener 
    /**
     * 当AnimatorSet开始时调用
     */
    void onAnimationStart(Animator animation);

    /**
     * 当AnimatorSet结束时调用
     */
    void onAnimationEnd(Animator animation);

    /**
     * 当AnimatorSet被取消时调用
     */
    void onAnimationCancel(Animator animation);

    /**
     * 当AnimatorSet重复时调用,由于AnimatorSet没有设置repeat的函数,所以这个方法永远不会被调用
     */
    void onAnimationRepeat(Animator animation);

添加方法为:

public void addListener(AnimatorListener listener);

好像这个listenner和ValueAnimator的一模一样啊。不错,确实是一模一样,因为ValueAnimator和AnimatorSet都派生自Animator类,而AnimatorListener是Animator类中的函数。
监听器的用法并不难,难点在于,我们AnimatorSet中的监听器,监听的AnimatorSet本身的动作,还是它内部的每个动画的动作?在AnimatorSet代码注释中我们已经提到,它监听的是AnimatorSet的过程,所以只有当AnimatorSet的状态发生变化时,才会被调用。
我们来看个例子:
额外添加一个Cancel按钮,在点击start按钮时,开始动画,在点击取消按钮时取消动画

private AnimatorSet mAnimatorSet;
public void onCreate(Bundle savedInstanceState) 
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    mButton = (Button) findViewById(R.id.btn);
    mBtnCancel = (Button) findViewById(R.id.btn_cancel);
    mTv1 = (TextView) findViewById(R.id.tv_1);
    mTv2 = (TextView) findViewById(R.id.tv_2);

    mButton.setOnClickListener(new View.OnClickListener() 
        @Override
        public void onClick(View v) 
            mAnimatorSet = doListenerAnimation();
        
    );

    mBtnCancel.setOnClickListener(new View.OnClickListener() 
        @Override
        public void onClick(View v) 
            if (null != mAnimatorSet) 
                mAnimatorSet.cancel();
            
        
    );

这段代码很简单,在点击开始时,执行 doListenerAnimation()函数, doListenerAnimation()会把构造的AnimatorSet对象返回,在点击取消时,取消AnimatorSet;
然后看一下 doListenerAnimation()的代码:

private AnimatorSet doListenerAnimation() 
    ObjectAnimator tv1BgAnimator = ObjectAnimator.ofInt(mTv1, "BackgroundColor", 0xffff00ff, 0xffffff00, 0xffff00ff);
    ObjectAnimator tv1TranslateY = ObjectAnimator.ofFloat(mTv1, "translationY", 0, 400, 0);
    ObjectAnimator tv2TranslateY = ObjectAnimator.ofFloat(mTv2, "translationY", 0, 400, 0);
    tv2TranslateY.setRepeatCount(ValueAnimator.INFINITE);

    AnimatorSet animatorSet = new AnimatorSet();
    animatorSet.play(tv1TranslateY).with(tv2TranslateY).after(tv1BgAnimator);
    //添加listener
    animatorSet.addListener(new Animator.AnimatorListener() 
        @Override
        public void onAnimationStart(Animator animation) 
            Log.d(ta

以上是关于自定义控件三部曲之动画篇——联合动画的代码实现的主要内容,如果未能解决你的问题,请参考以下文章

[转]Android自定义控件三部曲系列完全解析(动画, 绘图, 自定义View)

自定义控件三部曲之动画篇——alphascaletranslaterotateset的xml属性及用法

自定义控件三部曲之绘图篇——Path之贝赛尔曲线和手势轨迹水波纹效果

Android自定义控件:动画类----PropertyValuesHolder与Keyframe

android动画相关

自定义控件三部曲之绘图篇(二十)——RadialGradient与水波纹按钮效果