Android 動(dòng)畫解析

Android 的動(dòng)畫分類:

  1. View視圖動(dòng)畫(補(bǔ)間動(dòng)畫 / 逐幀動(dòng)畫)
  2. 屬性動(dòng)畫


    Android 動(dòng)畫

0x01 View視圖動(dòng)畫

1.1 逐幀動(dòng)畫

frame-by-frame animation

幀動(dòng)畫是順序播放一組預(yù)先定義好的圖片,類似電影播放。

不同于補(bǔ)間動(dòng)畫,系統(tǒng)提供了另外一個(gè)類 AnimationDrawable 來使用幀動(dòng)畫。

實(shí)現(xiàn)方式 2 種:XML定義代碼動(dòng)態(tài)創(chuàng)建

1.1.1 實(shí)現(xiàn)方式1: XML定義

創(chuàng)建動(dòng)畫的 XML 文件路徑:res/drawable/frame_animation.xml

根標(biāo)簽:animation-list

屬性名稱 說明
oneshot 是否僅播放一次
duration 每一幀的顯示時(shí)長(zhǎng)

XML 示例代碼如下:

// res/drawable/frame_animation.xml
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="false">
    <item
        android:drawable="@drawable/img_0"
        android:duration="50" />
    <item
        android:drawable="@drawable/img_1"
        android:duration="50" />
    <item
        android:drawable="@drawable/img_2"
        android:duration="50" />
</animation-list >

動(dòng)畫加載 Java 示例代碼如下:

ImageView imageView  = new ImageView(this);
imageView.setImageResource(R.drawable.frame_animation);
AnimationDrawable animationDrawable = (AnimationDrawable)imageView.getDrawable();
// 播放開始
animationDrawable.start();
// 播放結(jié)束
animationDrawable.stop();
1.1.2 實(shí)現(xiàn)方式2: 代碼動(dòng)態(tài)創(chuàng)建

Java 示例代碼如下:

AnimationDrawable frameAnimation = new AnimationDrawable();
// 參數(shù)1:圖片;參數(shù)2:顯示時(shí)長(zhǎng);
frameAnimation.addFrame(getResources().getDrawable(R.drawable.img_0), 50);
frameAnimation.addFrame(getResources().getDrawable(R.drawable.img_1), 50);
frameAnimation.addFrame(getResources().getDrawable(R.drawable.img_2), 50);
// 是否重復(fù)
frameAnimation.setOneShot(false); 
// 播放開始
frameAnimation.start();
// 播放結(jié)束
frameAnimation.stop();
1.1.3 優(yōu)缺點(diǎn)

缺點(diǎn):使用大量圖片,易導(dǎo)致OOM
建議:直接使用 gif,大小可以明顯改善

1.2 補(bǔ)間動(dòng)畫

View的作用對(duì)象是 View。

它支持4種動(dòng)畫效果,分別是 平移動(dòng)畫縮放動(dòng)畫、旋轉(zhuǎn)動(dòng)畫透明度動(dòng)畫。

幀動(dòng)畫 的表現(xiàn)形式和上面4種變換效果不太一樣,單獨(dú)介紹。

View 動(dòng)畫的 4 種變換效果對(duì)應(yīng)著 Animation 的 4 個(gè)子類:TranslateAnimationScaleAniamtion、RotateAnimationAlphaAnimation。

名稱 XML標(biāo)簽 Animation子類 效果
平移動(dòng)畫 <translate> TranslateAnimation 移動(dòng) View
縮放動(dòng)畫 <scale> ScaleAniamtion 放大/縮小 View
旋轉(zhuǎn)動(dòng)畫 <rotate> RotateAnimation 旋轉(zhuǎn) View
透明度動(dòng)畫 <alpha> AlphaAnimation 改變 View 的透明度
組合動(dòng)畫 <set> AnimationSet 多個(gè)動(dòng)畫的結(jié)合

實(shí)現(xiàn)方式 2 種:XML定義代碼動(dòng)態(tài)創(chuàng)建

對(duì)于 View 動(dòng)畫來說,建議采用 XML 定義動(dòng)畫,可讀性更好。

View 動(dòng)畫既可以是單個(gè)動(dòng)畫,也可以由一系列動(dòng)畫組成。

<set>標(biāo)簽表示動(dòng)畫集合,對(duì)應(yīng) AnimationSet 類,它可以包含若干個(gè)動(dòng)畫,并且它的內(nèi)部也是可以嵌套其他動(dòng)畫集合的。

1.2.1 實(shí)現(xiàn)方式1: XML定義

創(chuàng)建動(dòng)畫的 XML 文件路徑:res/anim/view_animation.xml。

XML 示例代碼如下:

// res/anim/view_animation.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="3000" // 動(dòng)畫時(shí)長(zhǎng)
    android:fillAfter="true" // 動(dòng)畫結(jié)束后,是否留在結(jié)束位置,優(yōu)先級(jí)高于fillBefore屬性,默認(rèn)是false
    android:fillBefore="false" //動(dòng)畫結(jié)束后,是否留在開始位置,默認(rèn)是true。
    android:fillEnabled="true" //是否應(yīng)用fillBefore的值,對(duì)fillAfter無影響,默認(rèn)是true
    android:repeatCount="0" //infinite無限重復(fù)
    android:repeatMode="restart" // restart正序重播/reverse反轉(zhuǎn)重播
    android:interpolator="@android:anim/overshoot_interpolator"
    android:shareInterpolator="true"  // 組合動(dòng)畫屬性,組合動(dòng)畫是否和集合(<set></set>)共享一個(gè)插值器,去過不指定,子動(dòng)畫需要單獨(dú)設(shè)定
    android:startOffset="100" //組合動(dòng)畫默認(rèn)是全部動(dòng)畫同時(shí)開始,如果不同動(dòng)畫不同開始需要使用該屬性延遲開始時(shí)間
    />
    
    <translate
     android:fromXDelta="0" // 水平x方向的起始值
     android:fromYDelta="0" // 豎直y方向的起始值
     android:toXDelta="500" // 水平x方向的結(jié)束值
     android:toYDelta="500" // 豎直y方向的結(jié)束值
    />

    <scale
     android:fromXScale="0.0" // 起始縮放x的倍數(shù)
     android:fromYScale="0.0" // 起始縮放y的倍數(shù)
     android:toXScale="1.0" // 結(jié)束縮放x的倍數(shù)
     android:toYScale="2.0" // 結(jié)束縮放y的倍數(shù)
     android:pivotX="50%" // 縮放中心點(diǎn)的x坐標(biāo)
     android:pivotY="50%" // 縮放中心點(diǎn)的y坐標(biāo)
    />

    <rotate
     android:fromDegrees="0" // 開始的角度
     android:toDegrees="180" // 結(jié)束的角度
     android:pivotX="20%" // 旋轉(zhuǎn)中心點(diǎn) x 坐標(biāo)
     android:pivotY="20%" // 旋轉(zhuǎn)中心點(diǎn) y 坐標(biāo)
    />

    <alpha
     android:fromAlpha="1.0" // 開始透明度 0.0-1.0
     android:toAlpha="0.0" //結(jié)束透明度 0.0-1.0
    />
</set>

動(dòng)畫加載 Java 示例代碼如下:

Animation translateAnimation = AnimationUtils.loadAnimation(this, R.anim.view_animation);
view.startAnimation(translateAnimation);

pivotX / pivotY 的取值有三個(gè)類型

  1. 數(shù)字, eg:android:pivotX="50", 該View左上角在x方向上平移50px的點(diǎn),對(duì)應(yīng)Java代碼中設(shè)置參數(shù) Animation.ABSOLUTE
  2. 百分比, eg:android:pivotX="50%", 該View左上角在x方向上平移自身寬度50%的點(diǎn),對(duì)應(yīng)Java代碼中設(shè)置參數(shù) Animation.RELATIVE_TO_SELF。
  3. 百分比p (parent), eg:android:pivotX="50%p"該View左上角在x方向上平移父布局寬度50%的點(diǎn),對(duì)應(yīng)Java代碼中設(shè)置參數(shù) Animation.RELATIVE_TO_PARENT。
1.2.2 實(shí)現(xiàn)方式2: 代碼動(dòng)態(tài)創(chuàng)建

Java 示例代碼如下:

// 組合動(dòng)畫設(shè)置
// 步驟1:創(chuàng)建組合動(dòng)畫shareInterpolator對(duì)象(設(shè)置為true)
AnimationSet setAnimation = new AnimationSet(true);

// 步驟2:設(shè)置組合動(dòng)畫的屬性
// 特別說明以下情況
// 因?yàn)樵谙旅娴男D(zhuǎn)動(dòng)畫設(shè)置了無限循環(huán)(RepeatCount = INFINITE)
// 所以動(dòng)畫不會(huì)結(jié)束,而是無限循環(huán)
// 所以組合動(dòng)畫的下面兩行設(shè)置是無效的
setAnimation.setRepeatMode(Animation.RESTART);
setAnimation.setRepeatCount(1);// 設(shè)置了循環(huán)一次,但無效

// 步驟3:逐個(gè)創(chuàng)建子動(dòng)畫(方式同單個(gè)動(dòng)畫創(chuàng)建方式,此處不作過多描述)

// 子動(dòng)畫1:旋轉(zhuǎn)動(dòng)畫
Animation rotate = new RotateAnimation(0,360,Animation.RELATIVE_TO_SELF,0.5f,Animation.RELATIVE_TO_SELF,0.5f);
rotate.setDuration(1000);
rotate.setRepeatMode(Animation.RESTART);
rotate.setRepeatCount(Animation.INFINITE);

// 子動(dòng)畫2:平移動(dòng)畫
Animation translate = new TranslateAnimation(TranslateAnimation.RELATIVE_TO_PARENT,-0.5f,
TranslateAnimation.RELATIVE_TO_PARENT,0.5f,
TranslateAnimation.RELATIVE_TO_SELF,0
,TranslateAnimation.RELATIVE_TO_SELF,0);
translate.setDuration(10000);

// 子動(dòng)畫3:透明度動(dòng)畫
Animation alpha = new AlphaAnimation(1,0);
alpha.setDuration(3000);
alpha.setStartOffset(7000); // 通過延遲播放達(dá)到順序效果

// 子動(dòng)畫4:縮放動(dòng)畫
Animation scale = new ScaleAnimation(1,0.5f,1,0.5f,Animation.RELATIVE_TO_SELF,0.5f,Animation.RELATIVE_TO_SELF,0.5f);
scale1.setDuration(1000);
scale1.setStartOffset(4000);  // 通過延遲播放達(dá)到順序效果

// 步驟4:將創(chuàng)建的子動(dòng)畫添加到組合動(dòng)畫里
setAnimation.addAnimation(alpha);
setAnimation.addAnimation(rotate);
setAnimation.addAnimation(translate);
setAnimation.addAnimation(scale);

// 步驟5:播放動(dòng)畫
mButton.startAnimation(setAnimation);
1.2.3 優(yōu)缺點(diǎn)

缺點(diǎn):

  1. 作用對(duì)象局限;根據(jù)包分類可知 android.view.animation,只針對(duì)View進(jìn)行動(dòng)畫操作。
  2. 只改變視覺效果,沒有改變屬性;例如點(diǎn)擊只在原始位置有效(經(jīng)API27測(cè)試,當(dāng)屬性動(dòng)畫View完全不可見時(shí),點(diǎn)擊位置和范圍為原始位置)。
  3. 動(dòng)畫效果單一,只有四種。
1.2.4 自定義 View 動(dòng)畫

自定義 View 動(dòng)畫是一件既簡(jiǎn)單又復(fù)雜的事情。
簡(jiǎn)單,是因?yàn)榕缮环N新動(dòng)畫只需要繼承Animation這個(gè)抽象類,然后重寫它的 initialize 和 applyTransformation方法,在 initialize 方法中做一些初始化工作,在 applyTransformation 中進(jìn)行相應(yīng)的矩陣變換即可,很多時(shí)候需要采用 Camera 來簡(jiǎn)化矩陣變換的過程。
復(fù)雜,是因?yàn)樽远x View 動(dòng)畫的過程主要是矩陣變換的過程,而矩陣變換是數(shù)學(xué)上的概念。

1.2.5 View 動(dòng)畫的特殊使用場(chǎng)景

比如在 ViewGroup 中可以控制子元素的出場(chǎng)效果,在 Activity 中可以實(shí)現(xiàn)不同 Activity 之間的切換效果。

  1. LayoutAnimation
    LayoutAnimation 作用于 ViewGroup,為 ViewGroup 指定一個(gè)動(dòng)畫,這樣當(dāng)它的子元素出場(chǎng)時(shí)都會(huì)具有這種動(dòng)畫效果。這種效果常常被用在ListView。
    步驟如下:

    1. 定義LayoutAnimation
    2. 定義子元素具體的入場(chǎng)動(dòng)畫
    3. 為 ViewGroup 指定 android:layoutAnimation 屬性
//  1  /res/anim/layout_animation
<?xml version="1.0" encoding="utf-8"?>
<layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
    android:animation="@anim/layout_animation_item"
    android:animationOrder="normal"
    android:delay="0.5" />

//  2  /res/anim/layout_animation_item
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="300"
    android:interpolator="@android:anim/accelerate_interpolator"
    android:shareInterpolator="true">
    
    <alpha
        android:fromAlpha="0.0"
        android:toAlpha="1.0" />
    <translate
        android:fromXDelta="500"
        android:toXDelta="0" />
</set>

// 3 在布局文件的ViewGroup中使用 layoutAnimation屬性
<ListView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layoutAnimation="@anim/layout_animation" />

除了在 XML 中指定 LayoutAnimation 外,還可以通過 LayoutAnimationController 來實(shí)現(xiàn)。

Animation animation = AnimationUtils.loadAnimation(this, R.anim.layout_animation_item);
LayoutAnimationController controller = new LayoutAnimationController(animation);
controller.setDelay(0.5f);
controller.setOrder(LayoutAnimationController.ORDER_NORMAL);
listView.setLayoutAnimation(controller);
  1. Activity 的切換效果
    Activity 有默認(rèn)的切換效果,但是這個(gè)效果我們可以通過 overridePendingTransition(int enterAnim, int exitAnim) 這個(gè)方法進(jìn)行自定義,這個(gè)方法必須在 startActivity(intent) 或者 finish() 之后被調(diào)用才能生效,之前無效。

Fragment 也可以添加動(dòng)畫,由于 Fragment 是在 API 11 中新引入的類,因此為了兼容性我們需要使用support-v4
這個(gè)兼容包,在這個(gè)情況下我們可以通過 FragmentTransaction 中的 setCustomAnimation(int enter, int exit) 方法來添加切換動(dòng)畫,這個(gè)動(dòng)畫需要是 View 動(dòng)畫,之所以不能采用屬性動(dòng)畫是因?yàn)閷傩詣?dòng)畫也是API 11 新引入的。

0x02 屬性動(dòng)畫

屬性動(dòng)畫框架類圖
PropertyValuesHolder

屬性動(dòng)畫是 API 11 新加入的特性,和 View 動(dòng)畫不同,它對(duì)作用對(duì)象進(jìn)行了擴(kuò)展,屬性動(dòng)畫可以對(duì)任何對(duì)象做動(dòng)畫,甚至還可以沒有對(duì)象。

屬性動(dòng)畫中有 ViewPropertyAnimator、 ValueAnimator、ObjectAnimatorAnimatorSet 等概念,通過它們可以實(shí)現(xiàn)絢麗的動(dòng)畫。

ObjectAnimator extends ValueAnimator extends Animator 位于 android.animation。

ViewPropertyAnimator 位于 android.view,其本質(zhì)內(nèi)部使用 ValueAnimator。

ViewPropertyAnimator < ObjectAnimator < ValueAnimator,使用難度越來越高,但越來越靈活。

三種使用方式舉例:

// ViewPropertyAnimator
view.animate().alphaBy(0.8f) 

// ObjectAnimator
Object.ofFloat(view, "alpha", 1.0f, 0.0f, 1.0f).start();

// ValueAnimator
ValueAnimator valueAnimator = ValueAnimator.ofFloat(1.0f, 0.0f, 1.0f);
value.addUpdateListener(new ValueAnimator.AnimatorUpdateListener(){
@Override
 public void onAnimationUpdate(ValueAnimator animation) {
        float animatedValue = (float) animation.getAnimatedValue();
        view.setAlpha(animatedValue);
 }
});
valueAnimator.start();

ValueAnimator 本身不作用于任何對(duì)象,也就是說直接使用它沒有任何動(dòng)畫效果。它可以對(duì)一個(gè)值做動(dòng)畫,然后我們可以監(jiān)聽其動(dòng)畫過程,在動(dòng)畫過程中修改我們對(duì)象的屬性值,這樣就相當(dāng)于我們的對(duì)象做了動(dòng)畫。

2.1 ViewPropertyAnimator
view.animate().alphaBy(0.8f);

為了滿足面向?qū)ο缶幊趟枷耄?strong>View 中引入了ViewPropertyAnimator,可以鏈?zhǔn)秸{(diào)用。

注意 ViewPropertyAnimator 沒有 setRepeatCount() 和 setRepeatMode(),不能重復(fù)

View 中的方法 功能 對(duì)應(yīng) ViewPropertyAnimator 的方法
setTranslationX() 設(shè)置x軸偏移 translationX() / translationXBy()
setTranslationY() 設(shè)置y軸偏移 translationY() / translationYBy()
setTranslationZ() 設(shè)置z軸偏移 translationZ() / translationZBy()
setX() 設(shè)置x軸絕對(duì)位置 x() / xBy()
setY() 設(shè)置y軸絕對(duì)位置 y() / yBy()
setZ() 設(shè)置z軸絕對(duì)位置 z() / zBy()
setRotation() 設(shè)置平面旋轉(zhuǎn) rotation() / rotationBy()
setRotationX() 設(shè)置沿x軸旋轉(zhuǎn) rotationX() / rotationXBy()
setRotationY() 設(shè)置沿y軸旋轉(zhuǎn) rotationY() / rotationYBy()
setScaleX() 設(shè)置橫向放縮 scaleX() / scaleXBy()
setScaleY() 設(shè)置縱向放縮 scaleY() / scaleYBy()
setAlpha() 設(shè)置透明度 alpha() / alphaBy()

注意

  1. View#setX()沒有動(dòng)畫漸變效果,直接將該View放到設(shè)置位置;而 ViewPropertyAnimator#x() 有平移過渡動(dòng)畫。
  2. ViewPropertyAnimator#x() 如果有動(dòng)畫執(zhí)行x()將取消。例如view.x(500).translationX(50),將只執(zhí)行translationX(50),不執(zhí)行x(500)
  3. ViewPropertyAnimator#By() 表示在當(dāng)前位置的基礎(chǔ)上進(jìn)行操作
2.2.1 實(shí)現(xiàn)方式1: XML定義

創(chuàng)建 XML 文件路徑:res/animator/view_animator.xml
XML示例代碼
如下:

<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:ordering="sequentially">
    
    <objectAnimator
        android:duration="2000"
        android:propertyName="translationX"
        android:valueFrom="0"
        android:valueTo="500"
        android:valueType="floatType" />
    
    <set android:ordering="together">
        
        <objectAnimator
            android:duration="3000"
            android:propertyName="rotation"
            android:valueFrom="0"
            android:valueTo="360"
            android:valueType="floatType" />
        
        <set android:ordering="sequentially">
            <objectAnimator
                android:duration="1500"
                android:propertyName="alpha"
                android:valueFrom="1"
                android:valueTo="0"
                android:valueType="floatType" />
            <objectAnimator
                android:duration="1500"
                android:propertyName="alpha"
                android:valueFrom="0"
                android:valueTo="1"
                android:valueType="floatType" />
        </set>
    </set>
</set>

動(dòng)畫加載 Java 示例代碼如下:

Animator animator = AnimatorInflater.loadAnimator(context,R.animator.view_animator);
animator.setTarget(view);
animator.start();
2.2.2 實(shí)現(xiàn)方式2: 代碼動(dòng)態(tài)創(chuàng)建
ObjectAnimator moveIn = ObjectAnimator.ofFloat(view, "translationX", 0, 500f);
ObjectAnimator rotate = ObjectAnimator.ofFloat(view, "rotation", 0f, 360f);
ObjectAnimator fadeInOut = ObjectAnimator.ofFloat(view, "alpha", 1f, 0f, 1f);
AnimatorSet animSet = new AnimatorSet();
animSet.play(rotate).with(fadeInOut).before(moveIn); //  with / before / after
animSet.setDuration(5000);
animSet.start();
2.3 屬性動(dòng)畫的原理

屬性動(dòng)畫要求動(dòng)畫作用的對(duì)象提供該屬性的 get 和 set 方法,屬性動(dòng)畫根據(jù)外界傳遞的該屬性的初始值和最終值,以動(dòng)畫的效果多次去調(diào)用 set 方法,每次傳遞給 set 方法的值都不一樣,確切來說是隨著時(shí)間的推移,所傳遞的值越來越接近最終的值。

自定義屬性示例:

public class SportsView extends View {  
    float progress = 0;

    ......

    // 創(chuàng)建 getter 方法
    public float getProgress() {
        return progress;
    }

    // 創(chuàng)建 setter 方法
    public void setProgress(float progress) {
        this.progress = progress;
        invalidate();
    }

    @Override
    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        ......

        canvas.drawArc(arcRectF, 135, progress * 2.7f, false, paint);

        ......
    }
}

......

// 創(chuàng)建 ObjectAnimator 對(duì)象
ObjectAnimator animator = ObjectAnimator.ofFloat(view, "progress", 0, 65);  
// 執(zhí)行動(dòng)畫
animator.start(); 

對(duì) object 的屬性 abc 做動(dòng)畫,如果想讓動(dòng)畫生效,需要滿足兩個(gè)條件:

  1. object 必須提供 setAbc 方法,如果動(dòng)畫的時(shí)候沒有傳遞初始值,那么還要提供 getAbc 方法,因此系統(tǒng)要去取 abc 屬性的初始值。(如果這條不滿足,程序直接Crash)
  2. object 的 setAbc 對(duì)屬性 abc 的改變必須反映出來(requestLayout/invalidate),才會(huì)有效果。(如果這條不滿足,動(dòng)畫無效果但不會(huì)Crash)

動(dòng)畫不生效,只滿足條件1而未滿足條件2,解決方法有 3 種:

  1. 給你的對(duì)象加上 get 和 set 方法,如果你有權(quán)限的話;
    這個(gè)方法最簡(jiǎn)單,加上 get 和 set 就搞定了,但針對(duì)Android SDK內(nèi)部實(shí)現(xiàn)的View往往沒有權(quán)限不可行。

  2. 用一個(gè)類來包裝原始對(duì)象,間接為其提供 get 和 set 方法;

private void performAnimation(View view) {

    ViewWrapper wrapper = new ViewWrapper(view);
    ObjectAnimator.ofInt(wrapper, "width", 0)
            .setDuration(5000)
            .start();
}


private static class ViewWrapper {
    private View mTarget;


    public ViewWrapper(View target) {
        this.mTarget = target;
    }

    public int getWidth() {
        return mTarget.getLayoutParams().width;
    }

    public void setWidth(int width) {
        mTarget.getLayoutParams().width = width;
        mTarget.requestLayout();
    }
}
  1. 采用 ValueAnimator,監(jiān)聽動(dòng)畫過程,自己實(shí)現(xiàn)屬性的改變;
private void valueAnimation(View targetView, int start, int end) {
    ValueAnimator valueAnimator = ValueAnimator.ofInt(1, 100);
    valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

        private IntEvaluator evaluator = new IntEvaluator();

        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            float fraction = animation.getAnimatedFraction();
            Integer curValue = evaluator.evaluate(fraction, start, end);
            targetView.getLayoutParams().width = curValue;
            targetView.requestLayout();
        }
    });
    valueAnimator.setDuration(5000).start();
}
2.4 時(shí)間插值器 TimeInterpolator / 類型估值器 TypeEvaluator

TimeInterpolator:時(shí)間插值器
根據(jù) 時(shí)間流逝的百分比 來計(jì)算 當(dāng)前屬性值改變的百分比。

TypeEvaluator:類型估值算法
根據(jù) 當(dāng)前屬性值改變的百分比 來計(jì)算改變后的 屬性值。

2.4.1 時(shí)間插值器 TimeInterpolator

android.animation.TimeInterpolator
android.view.animation.Interpolator

Interpolator extends TimeInterpolator

屬性動(dòng)畫內(nèi)部使用的是 TimeInterpolator,補(bǔ)間動(dòng)畫內(nèi)部使用的是 Interpolator。

TimeInterpolator 接口是屬性動(dòng)畫中新增的,用于兼容Interpolator 接口,這樣原先 Interpolator 的實(shí)現(xiàn)類就可以直接在屬性動(dòng)畫中使用

列舉官方插值器:

Java類 描述
LinearInterpolator 勻速
AccelerateInterpolator 加速
DecelerateInterpolator 減速
AccelerateDecelerateInterpolator 先加速后減速(默認(rèn))
AnticipateInterpolator 先退后然后加速前進(jìn)
OvershootInterpolator 完成動(dòng)畫,超出后回到結(jié)束位置
AnticipateOvershootInterpolator 先退后再加速前進(jìn),超出終點(diǎn)后再回終點(diǎn)
BounceInterpolator 最后階段彈球效果
CycleInterpolator 周期運(yùn)動(dòng)
PathInterpolator 自定義動(dòng)畫完成度/時(shí)間完成度曲線(0-1)
FastOutLinearInInterpolator 加速
LinearOutSlowInInterpolator 持續(xù)減速
FastOutSlowInInterpolator 先加速再減速

最后三個(gè)是Android5.0(API21)新增,和之前的類似,但是軌跡稍有區(qū)別

代碼舉例說明:

public class AccelerateDecelerateInterpolator implements Interpolator, NativeInterpolatorFactory {  
      // 僅貼出關(guān)鍵代碼
  ...
    public float getInterpolation(float input) {  
        return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f;
        // input的運(yùn)算邏輯如下:
        // 使用了余弦函數(shù),因input的取值范圍是0到1,那么cos函數(shù)中的取值范圍就是π到2π。
        // 而cos(π)的結(jié)果是-1,cos(2π)的結(jié)果是1
        // 所以該值除以2加上0.5后,getInterpolation()方法最終返回的結(jié)果值還是在0到1之間。只不過經(jīng)過了余弦運(yùn)算之后,最終的結(jié)果不再是勻速增加的了,而是經(jīng)歷了一個(gè)先加速后減速的過程
        // 所以最終,fraction值 = 運(yùn)算后的值 = 先加速后減速
        // 所以該差值器是先加速再減速的
    }  
}
2.4.2 類型估值器 TypeEvaluate

根據(jù)當(dāng)前屬性值變化的百分比初始值結(jié)束值來計(jì)算當(dāng)前屬性的具體數(shù)值。當(dāng)前屬性值 = 初始值 + (結(jié)束值 - 初始值) * 百分比

實(shí)現(xiàn)

  1. 實(shí)現(xiàn)TypeEvaluator<T> 接口
  2. 重寫public T evaluate(float fraction, T startValue, T endValue) 方法

官方示例

public class FloatEvaluator implements TypeEvaluator<Number> {
    public Float evaluate(float fraction, Number startValue, Number endValue) {
        float startFloat = startValue.floatValue();
        return startFloat + fraction * (endValue.floatValue() - startFloat);
    }
}

完整示例

public class Point {

    private float x;
    private float y;

    public Point(float x, float y) {
        this.x = x;
        this.y = y;
    }

    public float getX() {
        return x;
    }

    public float getY() {
        return y;
    }
}
public class PointEvaluator implements TypeEvaluator<Point> {

    @Override
    public Point evaluate(float fraction, Point startValue, Point endValue) {
        float startValueX = startValue.getX();
        float startValueY = startValue.getY();
        float currentX = startValueX + (endValue.getX() - startValueX) * fraction;
        float currentY = startValueY + (endValue.getY() - startValueY) * fraction;
        return new Point(currentX, currentY);
    }
}
public class PointView extends View {

    public static final float RADIUS = 70f;
    private Point ccurrentPoint;
    private Paint mPaint;

    public PointView(Context context) {
        this(context, null);
    }

    public PointView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.BLUE);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (ccurrentPoint == null) {
            ccurrentPoint = new Point(RADIUS, RADIUS);
        }
        canvas.drawCircle(ccurrentPoint.getX(), ccurrentPoint.getY(), RADIUS, mPaint);
    }

    public Point getCurrentPoint() {
        return ccurrentPoint;
    }

    public void setCurrentPoint(Point currentPoint) {
        this.ccurrentPoint = currentPoint;
        invalidate();
    }
}
Point pointStart = new Point(70, 70);
Point pointEnd = new Point(700, 1000);

// 使用 ObjectAnimator
ObjectAnimator objectAnimator = ObjectAnimator.ofObject(point_view, "currentPoint", new PointEvaluator(), pointStart, pointEnd);
objectAnimator.setDuration(1000);
objectAnimator.start();
      
// 使用 ValueAnimator
ValueAnimator valueAnimator = ValueAnimator.ofObject(new PointEvaluator(), pointStart, pointEnd);
valueAnimator.setDuration(1000);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
     @Override
     public void onAnimationUpdate(ValueAnimator animation) {
          Point animatedValue = (Point) animation.getAnimatedValue();
          point_view.setCurrentPoint(animatedValue);
     }
});
valueAnimator.start();
2.5 屬性動(dòng)畫的監(jiān)聽器

屬性動(dòng)畫提供了監(jiān)聽器用于監(jiān)聽動(dòng)畫的播放過程,主要:AnimatorUpdateListener、AnimatorPauseListener和 AnimatorListener。

Animator
public static interface AnimatorListener {
    default void onAnimationStart(Animator animation, boolean isReverse) {
        onAnimationStart(animation);
    }
    default void onAnimationEnd(Animator animation, boolean isReverse) {
        onAnimationEnd(animation);
    }
    void onAnimationStart(Animator animation);
    void onAnimationEnd(Animator animation);
    void onAnimationCancel(Animator animation);
    void onAnimationRepeat(Animator animation);
}

public static interface AnimatorPauseListener {
    void onAnimationPause(Animator animation);
    void onAnimationResume(Animator animation);
}

為了便于開發(fā),系統(tǒng)還提供了 AnimatorListenerAdapter 這個(gè)類,它是AnimatorListener 和 AnimatorPauseListener 的適配器類,這樣我們就可以有選擇的實(shí)現(xiàn)上面的6個(gè)方法。

ValueAnimator extends Animator

public static interface AnimatorUpdateListener {
    void onAnimationUpdate(ValueAnimator animation);
}

AnimatorUpdateListener 比較特殊,它會(huì)監(jiān)聽整個(gè)動(dòng)畫過程,動(dòng)畫是由許多幀組成的,每播放一幀,onAnimationUpdate 就會(huì)被調(diào)用一次。

2.5.1 添加監(jiān)聽器

ViewPropertyAnimator # setListener() / setUpdateListener()
ObjectAnimator # addListener() / addUpdateListener / addPauseListener()
VauleAnimator # addListener() / addUpdateListener / addPauseListener()

2.5.2 移除監(jiān)聽器

ViewPropertyAnimator # setListener(null) / setUpdateListener(null) 填null來移除
ObjectAnimator # removeListener() / removeUpdateListener() / removePauseListener()
VauleAnimator # removeListener() / removeUpdateListener() / removePauseListener()

ObjectAnimator 支持pause()方法暫停
ViewPropertyAnimator 不支持setRepeatMode() / setRepeatCount() 方法
ViewPropertyAnimator 獨(dú)有withStartAction(Runnable runnable) 和 withEndAction(Runnable runnable) 方法,可設(shè)置一次動(dòng)畫開始或結(jié)束的監(jiān)聽。即使重新開始動(dòng)畫,也不會(huì)回調(diào),是一次性的。而AnimatorListener是持續(xù)有效的。
withEndAction() 只有在動(dòng)畫正常結(jié)束才會(huì)調(diào)用,而在動(dòng)畫被取消時(shí)是不會(huì)執(zhí)行的。而 AnimatorListener.onAnimationEnd() 在取消之后也會(huì)被調(diào)用,在調(diào)用 onAnimationCancel()之后調(diào)用

2.6 PropertyValuesHolder 同一動(dòng)畫中改變多個(gè)屬性

ObjectAnimator.ofPropertyValuesHolder

關(guān)鍵字:一邊一邊,一個(gè)動(dòng)畫屬性同時(shí)執(zhí)行,區(qū)別多個(gè)動(dòng)畫先后執(zhí)行。一個(gè)動(dòng)畫需要共享開始時(shí)間/結(jié)束時(shí)間/Interpolator等等設(shè)定,PropertyValuesHolder不能有先后次序執(zhí)行動(dòng)畫了。

很多時(shí)候,在同一個(gè)動(dòng)畫中需要改變多個(gè)屬性,例如改變透明度的同時(shí)改變尺寸。

使用 ViewPropertyAnimator如下:

view.animate()
        .scaleX(0.0f)
        .scaleY(0.0f)
        .alpha(0.0f)

但是ObjectAnimator,是不能這么用的。需要使用PropertyValuesHolder來同時(shí)在一個(gè)動(dòng)畫里改變多個(gè)屬性

PropertyValuesHolder holder1 = PropertyValuesHolder.ofFloat("scaleX", 0.0f);  
PropertyValuesHolder holder2 = PropertyValuesHolder.ofFloat("scaleY", 0.0f);  
PropertyValuesHolder holder3 = PropertyValuesHolder.ofFloat("alpha", 0.0f);

ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(view, holder1, holder2, holder3)  
animator.start();  

ViewPropertyAnimator 動(dòng)畫完成之后會(huì)停留在結(jié)束位置,再次點(diǎn)擊執(zhí)行動(dòng)作操作不會(huì)執(zhí)行動(dòng)畫
ObjectAnimator 動(dòng)畫完成之后會(huì)停留在結(jié)束位置,再次點(diǎn)擊執(zhí)行動(dòng)作操作會(huì)從最原始狀態(tài)重新執(zhí)行一次動(dòng)畫。且Animator沒有Animation的setFillAfter() 和setFillBefore()方法
關(guān)于點(diǎn)擊范圍的測(cè)試說明(API27),屬性動(dòng)畫執(zhí)行后屬性發(fā)生變化,即點(diǎn)擊范圍和位置會(huì)更新,但經(jīng)測(cè)試當(dāng)View完全不可見時(shí),點(diǎn)擊位置和范圍為原始位置

ObjectAnimator.ofInt()
ObjectAnimator.ofFloat()
ObjectAnimator.ofMultiFloat()
ObjectAnimator.ofPropertyValuesHolder

2.7 AnimatorSet 多個(gè)動(dòng)畫配合執(zhí)行

關(guān)鍵字:一邊一邊,一個(gè)動(dòng)畫屬性同時(shí)執(zhí)行,區(qū)別多個(gè)動(dòng)畫先后執(zhí)行。一個(gè)動(dòng)畫需要共享開始時(shí)間/結(jié)束時(shí)間/Interpolator等等設(shè)定,PropertyValuesHolder不能有先后次序執(zhí)行動(dòng)畫了。

區(qū)別 AnimationSet,只能通過設(shè)置單個(gè)動(dòng)畫的 setStartOffset 來延遲時(shí)間進(jìn)行先后執(zhí)行順序。

animatorSet.play(a1) / playTogether(a1,a2) / playSequentially(a1,a2)
         .with(a3)
         .before(a4)
         .after(a5)

其中以 playXXX 開始得到 AnimatorSet.Builder 對(duì)象,然后調(diào)用with/before/after進(jìn)行管理。

2.8 PropertyValuesHolders.ofKeyframe() 把同一個(gè)屬性拆分

把一個(gè)屬性拆分成多段,執(zhí)行更加精細(xì)的屬性動(dòng)畫。

Keyframe keyframe1 = Keyframe.ofFloat(0.0f, 0);
Keyframe keyframe2 = Keyframe.ofFloat(0.5f, 100);
Keyframe keyframe3 = Keyframe.ofFloat(1.0f, 80);
PropertyValuesHolder holder = PropertyValuesHolder.ofKeyframe("translationX", keyframe1, keyframe2, keyframe3);
ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(mTextMessage, holder);
animator.start();

0x03 使用動(dòng)畫的注意

  1. OOM問題
    主要出現(xiàn)在幀動(dòng)畫中,但圖片數(shù)量過多且圖片較大時(shí),極易出現(xiàn)OOM,盡量避免使用幀動(dòng)畫

  2. 內(nèi)存泄漏
    屬性動(dòng)畫 中有一類無限循環(huán)的動(dòng)畫,這類動(dòng)畫需要在Activity退出時(shí)及時(shí)停止,否則將導(dǎo)致Activity無法釋放從而造成內(nèi)存泄漏。
    View動(dòng)畫 不存在這個(gè)問題。

  3. 兼容性問題
    動(dòng)畫在 3.0 以下的系統(tǒng)上(API 11)有兼容性問題,在某些特殊場(chǎng)景可能無法正常工作,因此要做好適配

  4. View動(dòng)畫的問題
    View動(dòng)畫是對(duì)View的影響做動(dòng)畫,并不是真正地改變View的狀態(tài),因此有時(shí)候會(huì)出現(xiàn)動(dòng)畫完成后View無法隱藏的現(xiàn)象,即setVisibility(View.GONE)失效,這時(shí)只需調(diào)用view.clearAnimation()清除View動(dòng)畫即可解決此問題。

  5. 不要使用px
    在進(jìn)行動(dòng)畫的過程中,要盡量使用dp。使用px會(huì)導(dǎo)致在不同的設(shè)備上有不同的效果。

  6. 動(dòng)畫元素的交互
    將 view 移動(dòng)(平移)后,在Android 3.0 以前的系統(tǒng)上,不管是 View動(dòng)畫 還是 屬性動(dòng)畫,新位置 均無法觸發(fā)單擊事件,同時(shí),老位置仍然可以觸發(fā)單擊事件。View只是在視覺上不存在了,移動(dòng)回原位置后,單擊事件繼續(xù)生效。
    從3.0開始,屬性動(dòng)畫的單擊事件觸發(fā)位置為移動(dòng)后的位置,但View動(dòng)畫仍然在原位置。

  7. 硬件加速
    在使用動(dòng)畫的過程中,開啟硬件加速,會(huì)提高動(dòng)畫的流暢性。
    但不可濫用!
    1.硬件層會(huì)比普通view繪制多做很多的工作。首先將view繪制到GPU的一個(gè)層中,然后GPU再把這個(gè)層繪制到window上。
    2.與所有的緩存一樣,GPU的硬件緩存也會(huì)有失敗幾率。如果動(dòng)畫進(jìn)行中調(diào)用 invalidate(),緩存的層會(huì)不得不重新渲染。如果不斷有無效的硬件層產(chǎn)生的話,還不如不使用硬件加速。因?yàn)樵黾佑布泳彺鏁?huì)增加額外的開銷。
    簡(jiǎn)單的view繪制 使用硬件加速會(huì)增加不必要的開銷,不建議開啟。
    使用示例:

// 設(shè)置硬件加速 
myView.setLayerType(View.LAYER_TYPE_HARDWARE, null);

// 設(shè)置動(dòng)畫
ObjectAnimator animator = ObjectAnimator.ofFloat(myView, View.TRANSLATION_X, 150);

// 設(shè)置一個(gè)回調(diào),在動(dòng)畫完成時(shí)取消硬件加速
animator.addListener(new AnimatorListenerAdapter() {  
  @Override
  public void onAnimationEnd(Animator animation) {
    myView.setLayerType(View.LAYER_TYPE_NONE, null);
  }
});

// 開始動(dòng)畫
animator.start();  

在API 14以上使用屬性動(dòng)畫,可以更簡(jiǎn)潔:

myView.animate()  
  .translationX(150)
  .withLayer()
  .start();

0x04 參考資料

感謝以下文章作者
HenCoder Android 自定義 View 1-6:屬性動(dòng)畫 Property Animation(上手篇)
Android:這是一份全面 & 詳細(xì)的補(bǔ)間動(dòng)畫使用教程
Android開發(fā)藝術(shù)探索 第7章 Android動(dòng)畫深入分析
Android 屬性動(dòng)畫詳解與源碼分析

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容