《Android开发进阶:从小工到专家》——第2章,第2.4节让应用更精彩——动画-阿里云开发者社区

开发者社区> 开发与运维> 正文
登录阅读全文

《Android开发进阶:从小工到专家》——第2章,第2.4节让应用更精彩——动画

简介:

本节书摘来自异步社区《Android开发进阶:从小工到专家》一书中的第1章,第2.4节让应用更精彩——动画,作者 何红辉,更多章节内容可以访问云栖社区“异步社区”公众号查看

2.4 让应用更精彩——动画
为了使用户的交互更为流畅、自然,动画已经成为一款应用中不可缺少的部分。在Android中,动画的分类较多,有最早的帧动画、补间动画,从Android 3.0之后添加了属性动画,而在Android 5.0中又增加了VectorDrawable,使得Android的动画多种多样,能够满足用户的各种需求。

动画实际上就是在指定的时间段内持续地修改某个属性的值,使得该值在指定取值范围之内平滑的过渡。如图2-22所示是一个执行时长为40毫秒、将x从0平滑过渡为40的动画。


eb84cd3fdae1cec018c1b1e14305c52a0ed90669

从图2-22可以看出,动画就是在某个时间点根据一定的计算方式计算出属性的取值,并且设置给目标对象。在动画的执行周期内持续执行这个过程,形成动画的效果。

2.4.1 帧动画
帧动画也就是我们说的Frame动画。Frame动画是一系列图片按照一定的顺序展示的过程,和放电影的机制很相似,它的原理是在一定的时间段内切换多张有细微差异的图片从而达到动画的效果。

Frame动画可以被定义在xml文件中,也可以完全编码实现。如果被定义在xml文件中,可以放置在/res下的anim或drawable目录中,文件名可以作为资源id在代码中引用;如果完全由编码实现,需要使用到AnimationDrawable对象。需要注意的是,当我们在xml文件中定义帧动画时,元素必须要作为根元素,它可以包含一或多个元素。android:onshot如果定义为true的话,此动画只会执行一次,如果为false则一直循环。元素代表一帧动画,android:drawable指定此帧动画所对应的图片资源,android:druation代表此帧持续的时间,单位为毫秒。

下面用一个简单的示例演示一下帧动画的使用。

下面为res/drawable中的5张类似的图片,图片的名字从ic_heart_0~ic_heart_4,每张图片都有些差异,将这几张图片作为帧动画时就能够看到类似gif图片的进度条效果,如图2-23所示。


634c83ece6672c5fae2c9cf9e867abfad76704bb

我们定义一个名为heart_anim.xml的帧动画存放在res/drawable目录下,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="true">

       <item
       android:duration="500"
       android:drawable="@drawable/ic_heart_0"/>

       <item
       android:duration="500"
       android:drawable="@drawable/ic_heart_1"/>

       <item
       android:duration="500"
       android:drawable="@drawable/ic_heart_2"/>

       <item
       android:duration="500"
       android:drawable="@drawable/ic_heart_3"/>

       <item
       android:duration="500"
       android:drawable="@drawable/ic_heart_4"/>

</animation-list>

定义好之后,我们还需要将动画设置给某个View,例如,将该动画设置为某个ImageView的背景,代码如下:

<ImageView
    android:id="@+id/imageview_anim "
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@drawable/heart_anim "
    />

但是,此时动画并不会在ImageView显示时启动,我们还需要通过Java代码启动该动画。代码如下:

ImageView mImageView = (ImageView) findViewById(R.id.imageview_anima);
((AnimationDrawable) mImageView.getBackground()).start();

这样,帧动画就会启动了,ImageView的背景就会在指定的时间间隔之内切换。

当然,也可以通过Java代码来构建帧动画,示例如下:

AnimationDrawable anim = new AnimationDrawable();
for (int i = 0; i <= 4; i++) {
     // 获取图片的资源id
      int id = getResources().getIdentifier("ic_heart_" + i, "drawable",   
      getPackageName());
     Drawable drawable = getResources().getDrawable(id);
     // 将Drawable添加到帧动画中
     anim.addFrame(drawable, 300);
}
anim.setOneShot(false);
// 将动画设置为ImageView的背景
mImageView.setBackgroundDrawable(anim);
anim.start();

通过xml还是Java代码来设置帧动画完全取决于个人意愿,当然,推荐的方式自然是xml。因为它将动画的代码从复杂的Java代码逻辑中隔离,使得动画的定义更易于维护。

2.4.2 补间动画
tween动画是操作某个控件让其展现出旋转、渐变、移动、缩放的一种转换过程,这称成为补间动画。同样的,我们可以以xml形式定义动画,也可以编码实现。

如果以xml形式定义一个动画,我们按照动画的定义语法完成xml,并放置于/res/anim目录下,文件名可以作为资源id被引用;如果由编码实现,需要使用到Animation对象。

下面是一个补间动画集合与补间动画的格式,也就是说该集合里面包含了多个自动化,在执行该动画集合时,它们将一起执行:

    <?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"] >
    <alpha
    android:fromAlpha="float"
    android:toAlpha="float" />
    <scale
    android:fromXScale="float"
    android:toXScale="float"
    android:fromYScale="float"
    android:toYScale="float"
    android:pivotX="float"
    android:pivotY="float" />
    <translate
    android:fromX="float"
    android:toX="float"
    android:fromY="float"
    android:toY="float" />
    <rotate
    android:fromDegrees="float"
    android:toDegrees="float"
    android:pivotX="float"
    android:pivotY="float" />
</set>

xml文件中必须有一个根元素,可以是、、、中的任意一个,也可以是来管理一个由前面几个元素组成的动画集合。

是一个动画容器,管理多个动画的群组,与之相对应的Java对象是AnimationSet。它有两个属性,android:interpolator代表一个插值器资源,可以引用系统自带插值器资源,如表2-4所示。当然你也可以用自定义插值器资源,默认值是匀速插值器。android:shareInterpolator代表里面的多个动画是否要共享插值器,默认值为true,即共享插值器,如果设置为false,那么的插值器就不再起作用,我们要在每个动画中加入插值器。


66442afe5c350a4736646b765b2c26525e2282a8

是透明度的渐变动画,可以实现淡入、淡出的效果,与之对应的Java对象是AlphaAnimation。android:fromAlpha属性代表起始alpha值、浮点值,范围在0.0和1.0之间,分别代表透明和完全不透明,android:toAlpha属性代表结尾alpha值、浮点值,范围也在0.0和1.0之间。
是缩放动画,可以实现动态调整控件尺寸的效果,与之对应的Java对象是ScaleAnimation。android:fromXScale属性代表起始的X方向上相对自身的缩放比例,浮点值,比如1.0代表自身无变化,0.5代表起始时缩小一倍,2.0代表放大一倍;android:toXScale属性代表结尾的X方向上相对自身的缩放比例,浮点值;android:fromYScale属性代表起始的Y方向上相对自身的缩放比例,浮点值;android:toYScale属性代表结尾的Y方向上相对自身的缩放比例,浮点值;android:pivotX属性代表缩放的中轴点X坐标,浮点值;android:pivotY属性代表缩放的中轴点Y坐标,浮点值,对于这两个属性,如果我们想表示中轴点为图像的中心,可以把两个属性值定义成0.5或者50%。

是位移动画,代表一个水平、垂直的位移。与之对应的Java对象是TranslateAnimation。android:fromXDelta属性代表起始X方向的位置,android:toXDelta代表结尾X方向上的位置,android:fromYScale属性代表起始Y方向上的位置,android:toYDelta属性代表结尾Y方向上的位置,以上4个属性都支持3种表示方式:浮点数、num%、num%p。如果以浮点数字表示,代表相对自身原始位置的像素值;如果以num%表示,代表相对于自己的百分比,比如toXDelta定义为100%就表示在X方向上移动自己的1倍距离;如果以num%p表示,代表相对于父类组件的百分比。

是旋转动画,与之对应的Java对象是RotateAnimation。android:fromDegrees属性代表起始角度,浮点值,单位:度;android:toDegrees属性代表结尾角度,浮点值,单位:度;android:pivotX属性代表旋转中心的X坐标值,android:pivotY属性代表旋转中心的Y坐标值,这两个属性也有3种表示方式,数字方式代表相对于自身左边缘的像素值,num%方式代表相对于自身左边缘或顶边缘的百分比,num%p方式代表相对于父容器的左边缘或顶边缘的百分比。

补间动画只能运用在View对象之上,并且功能相对来说较为局限。例如旋转动画只能够在x、y轴进行,而不能在z轴方向进行旋转。因此,补间动画通常用于执行一些比较简单的动画。由于比较简单,我们在此不过多赘述。

2.4.3 属性动画
在Android 3.0之后,Android推出了新的动画包,也就是属性动画。属性动画机制不再是针对View来设计的,也不限定于只能实现移动、缩放、旋转和淡入、淡出这几种简单的动画操作,同时也不再只是一种视觉上的动画效果。它实际上是一种在一定时间段内不断修改某个对象的某个属性值的机制。所以我们仍然可以通过属性动画将一个View进行移动或者缩放,但同时也可以对View的其他属性进行动画操作。我们只需要告诉系统动画要操作的属性、动画时长、需要执行哪种类型的动画,以及动画的初始值和结束值,剩下的工作就可以全部交给系统去完成了。

2.4.3.1 属性动画的核心类——ValueAnimator
ValueAnimator是整个属性动画机制当中最核心的一个类,它的作用就是在一定的时间段内不断地修改对象的某个属性值。前文我们已经说过,属性动画的基本原理就是通过不断地修改对象的属性值来实现。ValueAnimator的内部使用一种时间循环的机制来计算值与值之间的动画过渡,我们只需要将属性的取值范围、运行时长提供给ValueAnimator,那么它就会自动帮我们计算属性值在各个动画运行时段的取值,这些值会按照一定的计算方式来实现平滑过渡。除此之外,ValueAnimator还负责管理动画的播放次数、播放模式,以及对动画设置监听器等,这使得它成为属性动画中最核心的类型。

ValueAnimator不仅功能强大,它的API也设计得非常简单。通常我们都是通过ofFloat、ofInt等静态工厂函数构建ValueAnimator。例如下面是我们将数值从0.0过渡到1.0的动画:

private void startValueAnimation() {
    ValueAnimator animator = ValueAnimator.ofFloat(0.0f, 1.0f);
    animator.setDuration(1000);
    animator.addUpdateListener(mAnimationListener);
    animator.start();
}
ValueAnimator.AnimatorUpdateListener mAnimationListener = new 
                ValueAnimator.AnimatorUpdateListener() {

    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        float newValue = (Float) animation.getAnimatedValue();
        Log.e("", "### 新的属性值 : " + newValue);
    }
};

当然,我们也可以在res/anim目录下的xml文件中定义该动画,实现如下:

<?xml version="1.0" encoding="utf-8"?>
<animator xmlns:android="http://schemas.android.com/apk/res/android"
    android:valueFrom="0.0"
    android:valueTo="1.0"
    android:valueType="floatType" />
然后在Java代码中加载该动画:

ValueAnimator animator = (ValueAnimator) AnimatorInflater.loadAnimator(
getApplicationContext(),R.anim.value_animator);

启动动画之后,每次更新属性值时就会调用onAnimationUpdate函数,在这里可以获取新的属性值。当然,在这里我们并没有将这个值运用到具体的对象上。但它是非常灵活的实现,它只操作属性值本身,这个值不属于某个具体的对象,但它却能运用于任意对象之上。例如,通过ValueAnimator动画将某个View的透明度从0.0过渡到1.0,那么可以对onAnimationUpdate做如下修改:

    @Override
public void onAnimationUpdate(ValueAnimator animation) {
    float newValue = (Float) animation.getAnimatedValue();
    Log.e("", "### 新的属性值 : " + newValue);
    // 将数值设置给具体的对象
    myView.setAlpha(newValue);
}

这样一来,我们就将ValueAnimator与具体的对象结合在一起,通过这种形式就能更自由地控制动画,完成各种各样的动画效果。

2.4.3.2 对任意属性进行动画操作——Object Animator
ValueAnimator功能强大、自由度高,但是,这也意味着开发人员需要做更多的工作来实现动画需求,这在效率致上的软件开发领域来说并不是一个很好的选择。我们开发中运用更多的应该是ObjectAnimator,因为ValueAnimator只是对值进行了一个平滑的动画过渡,但实际开发中需要做的通常是对某个对象的某个属性值进行修改,也就是对某个对象执行动画,当然用得最多的就是View的动画,而ObjectAnimator就是可以直接对任意对象的任意属性进行动画操作的类。

ObjectAnimator继承自ValueAnimator,因此,的动画实现机制也与ValueAnimator一致,所以,前文才说ValueAnimator是属性动画中最核心的类。ObjectAnimator最常用的形式也是通过ofFloat、ofInt等静态工厂形式构建Animator对象,例如下述代码就是在2秒之内将myView的alpha属性从1.0过渡到0.3,再从0.3过渡到0.7:

ObjectAnimator animator = ObjectAnimator.ofFloat(myView, "alpha", 1.0f, 0.3f, 0.7f);  
animator.setDuration(2000);  
animator.start();

ofXxx这样的静态工厂函数通常至少含有4参数,例如,这里的ofFloat函数,参数1就是要操作的对象,参数2是要操作该对象的哪个属性,我们这里要操作的是alpha属性。剩下的参数就是可变参数,也就是说它可以是0到多个。很多情况我们都是传递2个值,即起始值和目标值。如果是多个数值,那么在动画过程中将会逐个过渡到各个值。

ObjectAnimator极为强大,它能够操作任意对象中的任意属性,因此,它突破了补间动画只运用于View的限制,使得任意对象类型都可以使用属性动画。它的原理是在初始时设置目标对象、目标属性以及要经历的属性值,然后通过内部的计算方式计算出在各个时间段该属性的取值,在动画运行期间通过目标对象属性的setter函数更新该属性值,如果该属性没有setter函数,那么将会通过反射的形式更新目标属性值。在运行周期内不断地计算、更新新的属性值,从而达到对象的属性动画效果。

2.4.3.3 实现丰富多彩的动画效果——AnimatorSet
独立的动画能够实现的视觉效果毕竟是相当有限的,例如,要实现一个View在平移过程中同时在y轴方向进行旋转,这种情况就需要使用AnimatorSet将多个动画组合在一起执行。AnimatorSet类提供了一个play()方法,如果我们向这个方法中传入一个Animator对象将会返回一个AnimatorSet.Builder的实例,AnimatorSet.Builder中包括以下5个核心方法,如表2-5所示。


f6de1be63f929839a64316aa6ef7c52345771e56

有了这5个方法,我们就可以将各种各样的动画组合在一起执行,使得动画效果更加丰富多彩。示例如下:

// 动画集,假设anim1~anim3已经初始化
AnimatorSet animSet = new AnimatorSet();  
// 在anim3执行之后同时执行anim1和anim2
animSet.play(anim1).with(anim2).after(anim3);  
animSet.setDuration(2000);  
animSet.start();

假设anim1~anim3是我们已经初始化好的动画,然后我们创建一个AnimatorSet对象,并将这3个动画通过play、with、after进行组合,最终使得anim3首先执行,在anim3执行完成之后同时执行anim1和anim2。当然,我们也可以通过playTogether函数将3个动画一起执行,代码如下:

animatorSet.playTogether(anim1,anim2, anim3);    // 

将3个动画一起执行
通过AnimatorSet,我们可以将多个动画进行自由组合、排序,使得不同类型的动画最终可以一起实现复杂的效果,满足各种各样的交互应用。

2.4.3.4 动画执行时间——TypeEvaluator与TimeInterpolator
前文多次说到,动画的原理就是在一定时间内不断地修改某个值。那么在某个时间点这个属性的值如何确定呢?

答案就是通过TypeEvaluator计算得到。TypeEvaluator的中文翻译为类型估值器,它的作用是根据当前动画已执行时间占总时间的百分比来计算新的属性值。TypeEvaluator只有一个evaluate函数,该函数的职责就是计算出新的属性值。函数声明如下:

public abstract T evaluate (float fraction, T startValue, T endValue)

该函数的参数1为已执行时间占总时间的百分比,取值为0.0到1.0。参数2为属性的起始值,参数3为属性的最终值。通常,属性的计算公式为:

 T newValue = startValue + (T)( fraction * ( endValue – startValue ) ) ;

也就是已执行时间的百分比乘以两个取值范围的差值再加上起始值。例如某个动画的总时间为1秒,动画的功能是将View的x坐标从0移到100的位置,当已执行时间为300毫秒时,已执行时间的百分比则为30%,对应时float值为0.3,那么此时计算得到的属性值x则为30。它的计算公式为:

    int newValue = 0  +  (int) ( 0.3 * ( 100 – 0 ) ) ;
因此一个完整的Type代码如下所示:

public class TranslateXEvaluator implements TypeEvaluator<Integer> {

    @Override
    public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
          // 计算新的属性值
         int newValue = startValue + (int) (fraction * (endValue - startValue));
         Log.d("", "### fraction = " + fraction + ", start = " + startValue + ", end = "   
         + endValue + ", new Value = " + newValue);
        return newValue;
    }
}
使用代码如下:

private void useCustomEvaluator() {
    ObjectAnimator animator = ObjectAnimator.ofObject(mView, "x",
            new TranslateXEvaluator(), 0, 200);
    animator.setDuration(500);
    animator.start();
}

动画运行之后可以看到输出了如下所示的Log:

fraction = 0.0, start = 0, end = 200, new Value = 0
fraction = 0.50942427, start = 0, end = 200, new Value = 101
fraction = 0.8039651, start = 0, end = 200, new Value = 160
fraction = 0.8791809, start = 0, end = 200, new Value = 175
fraction = 0.9117664, start = 0, end = 200, new Value = 182
fraction = 0.9625386, start = 0, end = 200, new Value = 192
fraction = 0.9801469, start = 0, end = 200, new Value = 196
fraction = 0.99228215, start = 0, end = 200, new Value = 198
fraction = 0.9985795, start = 0, end = 200, new Value = 199
fraction = 1.0, start = 0, end = 200, new Value = 200

fraction从0逐渐增加到1.0,在这个过程中属性值也从0慢慢线性增加到200。线性变化也就是说属性的变化范围基本上比较平均,在同一个时间间隔之内属性的变化范围基本没有大的变化。但是,问题是有的时候为了使动画更动感,我们需要动画产生一些非线性的效果,例如动画开始前比较慢,随着时间的推移动画越来越快,直到结束。要实现这种功能就需要TimeInterpolator。

TimeInterpolator中文译为时间插值器,它的作用是修改动画已执行时间与总时间的百分比,也就是修改fraction参数值。系统预置的有匀速的线性插值LinearInterpolator、加速插值器AccelerateInterpolator、减速插值器DecelerateInterpolator和加速减速插值器AccelerateDecelerate Interpolator等。它的作用是在获得已执行时间百分比之后,通过调用TimeInterpolator的getInterpolation函数来对该百分比做出修改,并且返回。

例如上述的加速插值器,它的实现原理是使fraction参数在动画前面部分变化范围小,越往后变化范围越大。还是以1秒内从x轴的坐标0变化到200,如果使用加速动画,在同一个时间段内得到的效果大致如图2-24所示。


5b85de4bcb299e856252018c7d8bc0cc10982cb0

如图2-27所示,在300秒两个关键节点的x、fraction之间的差值不断递增,0到300毫秒的x差值为40、fraction为0.2,300毫秒到600毫秒的x差值为60、fraction为0.3,依次类推,fraction变化频率不断增大,使得x的变化也逐渐增大,也就造成了动画加速的效果。

那么如何控制fraction来实现这种效果呢?

那就是TimeInterpolator的任务,在动画执行时,会调用TimeInterpolator的getInterpolation函数使得开发人员有机会参与到fraction的设定,这样开发人员就可以通过不同的TimeInterpolator实现各种各样与动画频率相关的效果。所谓插值器,也就是在动画执行中“插入一脚”,影响动画的执行。getInterpolation函数的声明如下:

public float getInterpolation (float input)

参数就是fraction本身,返回值则是修改后的fraction值。例如线性插值器是匀速执行的,因此,它没有修改fraction值,LinearInterpolator代码如下:

public class LinearInterpolator implements Interpolator, NativeInterpolatorFactory {
      // 代码省略

    public float getInterpolation(float input) {
        return input;
    }
}

下面就来实现一个加速插值器。它的原理就是对getInterpolation的fraction参数进行乘方,因为fraction是float型,且取值在0.0~1.0,对于小数而言,因此值越小乘方之后的值就更小,当fraction慢慢变大时,乘方后的fraction值变化范围就越来越大,也就是说通过逐渐增大统一时段内的fraction值变化范围,即可改变动画的执行效果。实现代码如下:

public class CustomInterpolator implements TimeInterpolator {

    @Override
    public float getInterpolation(float input) {
        return input * input;
    }
}

代码很简单,在getInterpolation中将input参数相乘之后返回即可。对于上述的x变化动画,当执行时间t为100毫秒时,此时fraction为0.1,经过乘方之后得到的值为0.01,那么在x轴坐标上移动的距离为1,此时在100毫秒内x的变化范围是1;当t为200、fraction为0.2时,它与t为100毫秒的x差值为3;当t为300、fraction为0.3时,它与t为300毫秒的x差值为5。可见x的变化范围按照1、3、5的规律在慢慢变大。我们看看具体的执行效果,代码如下:

private void useCustomEvaluator() {
        ObjectAnimator animator = ObjectAnimator.ofObject(mColorImageView, "x",
                new TranslateXEvaluator(), 0, 200);
        // 使用自定义的插值器
        animator.setInterpolator(new CustomInterpolator());
        animator.setDuration(500);
        animator.start();
}

执行结果如下:

fraction = 0.0, start = 0, end = 200, new Value = 0
fraction = 0.010404, start = 0, end = 200, new Value = 2
// 省略
fraction = 0.25603598, start = 0, end = 200, new Value = 51
fraction = 0.29160002, start = 0, end = 200, new Value = 58
// 省略
fraction = 0.891136, start = 0, end = 200, new Value = 178
fraction = 0.95648396, start = 0, end = 200, new Value = 191
fraction = 1.0, start = 0, end = 200, new Value = 200

从上述程序中可以看到,正如我们分析的,fraction的变化范围逐渐增大,x轴的变化范围也随之增大。在同一时间段内,动画前期x的变化较小,使得动画看起来较慢,越往后x的差值越大,动画看起来就像是加速效果。通过插值器,我们就可以很方便地在执行速率上控制动画的执行了。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享: