Android 5.0+ 高階動畫開發系列 向量圖動畫

郭朝發表於2017-01-28

春節假期剛到,就趕緊抽出點時間寫點技術文章,這篇文章已經醞釀了很長時間了。我們經常可以看見很多擁有酷炫動畫的App,並且給人感覺具有高逼格的氣息,自從Google更新Android 5.0以來,Android世界變的異常豐富多彩,本篇主要講解 Android5.0/6.0 以後實現酷炫動畫的新技術。有向量圖動畫VectorDrawable,靜態VectorDrawable,動態VectorDrawable,軌跡動畫,路徑變換動畫,並指出了目前常見的一些相容性問題。乾貨滿滿。

學習向量圖動畫之前,我們需要先回顧一下屬性動畫的知識點,動態向量圖動畫是需要結合屬性動畫來實現的。

1.屬性動畫框架

使用動態的VectorDrawable需要結合 屬性動畫 來實現,所以下面先回顧一下屬性動畫。
Android 3.0 中加入的新的屬性動畫系統。這個新的動畫系統使得任何物件做任何型別的屬性的動畫都變得容易,包括那些在 Android 3.0 中加入 View 中的新屬性。在 Android 3.1 中,又加入了一些工具類使得 View 物件做屬性動畫更加容易。

傳統動畫(Animation)是系統不斷呼叫onDraw方法重繪介面以實現動畫效果,不適合做互動動畫效果,只適用於做顯示動畫;而屬性動畫(Animator)則是操作一個屬性的get、set方法去真實的改變一個屬性。這裡只針對屬性動畫(Animator)進行說明。

注意:屬性動畫在Google提供的APIDemo中已經有非常多的案例實現,建議多加揣摩學習。

1.ObjectAnimator的使用

屬性動畫中最簡單最常用的就是ObjectAnimator:

1)作用單個屬性的動畫實現:

/**
  * 引數1: 操縱的控制元件
  * 引數2: 操縱的屬性, 常見的有偏移translationX、translationY, 絕對值x、y, 3D旋轉rotation、
  *     水平\豎直方向旋轉rotationX\rotationY, 水平\豎直方向縮放scaleX\scaleX,透明度alpha
  * 引數3,4: 變化範圍
  * setDuration: 設定顯示時長
  */
ObjectAnimator.ofFloat(imageView, "translationX", 0F, 200F).setDuration(1000).start();

同樣屬性動畫也可以在res/animator資料夾下進行建立anim.xml並配置(多個objectAnimator可以用set進行包裹):

<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:androd="http://schemas.android.com/apk/res/android"
    androd:duration="1000"
    androd:propertyName="translationX"
    androd:valueFrom="0F"
    androd:valueTo="200F"
    androd:valueType="floatType">
</objectAnimator>

這段XML實現的效果和我們剛才通過程式碼來實現的組合動畫的效果是一模一樣的,每個引數的含義都非常清楚,相信大家都是一看就懂,我就不再一一解釋了。

XML檔案是編寫好了,那麼我們如何在程式碼中把檔案載入進來並將動畫啟動呢?只需呼叫如下程式碼即可:

Animator animator = AnimatorInflater.loadAnimator(context, R.animator.anim);
animator.setTarget(view);
animator.start();

呼叫AnimatorInflater的loadAnimator來將XML動畫檔案載入進來,然後再呼叫setTarget()方法將這個動畫設定到某一個物件上面,最後再呼叫start()方法啟動動畫就可以了,就是這麼簡單。

2)同時作用多個屬性的動畫實現:

所有動畫同時播放:

ObjectAnimator animator1 = ObjectAnimator.ofFloat(imageView, "translationX", 0F, 200F);
ObjectAnimator animator2 = ObjectAnimator.ofFloat(imageView, "translationY", 0F, 200F);
AnimatorSet set = new AnimatorSet();
set.playTogether(animator1, animator2);
set.setDuration(1000).start();

所有動畫按順序播放:

ObjectAnimator animator1 = ObjectAnimator.ofFloat(imageView, "translationX", 0F, 200F);
ObjectAnimator animator2 = ObjectAnimator.ofFloat(imageView, "translationY", 0F, 200F);
AnimatorSet set = new AnimatorSet();
set.playSequentially(animator1, animator2);
set.setDuration(1000).start();

有的同時播放有的按順序:

ObjectAnimator animator1 = ObjectAnimator.ofFloat(imageView, "translationX", 0F, 200F);
ObjectAnimator animator2 = ObjectAnimator.ofFloat(imageView, "translationY", 0F, 200F);
ObjectAnimator animator3 = ObjectAnimator.ofFloat(imageView, "rotation", 0F, 360F);
AnimatorSet set = new AnimatorSet();
set.play(animator1).with(animator2);//同時
set.play(animator3).after(animator1);//之後, 另外還有before之前
set.setDuration(1000).start();

3)屬性動畫的結束監聽事件

animator.addListener(new AnimatorListenerAdapter() {
    @Override
    public void onAnimationEnd(Animator animation) {
        super.onAnimationEnd(animation);
    }
});

2.ValueAnimator的使用

ValueAnimator不會作用於任何一個屬性,簡單來說,它就是“數值發生器”,實際上在屬性動畫中,產生每一步的具體動畫實現效果都是通過ValueAnimator計算出來的。ObjectAnimator是繼承自ValueAnimator的,ValueAnimator並沒有ObjectAnimator使用的廣泛。
ValueAnimator通過動畫已經繼續的時間和總時間的比值產生一個0~1點時間因子,有了這樣的時間因子,經過相應的變換,就可以根據startValue和endValue來生成中間相應的值。

1)顯示ValueAnimator生成出來的數值(這裡沒有指定插值器,預設線性增長):

ValueAnimator animator = ValueAnimator.ofInt(0, 100);
animator.setDuration(5000);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator valueAnimator) {
        Integer value = (Integer) valueAnimator.getAnimatedValue();
        textView.setText(""+value);
    }
});
animator.start();

2)自定義數值生成器

ValueAnimator animator = ValueAnimator.ofObject(new TypeEvaluator() {
    /**
     * 通過重寫evaluate方法返回各種各樣的值
     * @param fraction 時間因子 0到1之間變化到數值
     * @param startValue
     * @param endValue
     * @return
     */
    @Override
    public Object evaluate(float fraction, Object startValue, Object endValue) {
        return null;
    }
});

3.屬性動畫常用方法\類總結

ValueAnimator\ObjectAnimator
AnimatorUpdateListener\AnimatorListenerAdapter   做監聽器的
PropertyValuesHolder\AnimatorSet    控制動態集合的顯示效果、順序、流程的
TypeEvaluators          值計算器
Interpolators           插值器

Interpolator圖示:

這些常用 方法\類官方API 都有詳細說明,建議多閱讀 官方API文件。https://developer.android.google.cn/reference/android/animation/package-summary.html

下面正式開始 向量圖動畫 的開發:

2.向量圖動畫(VectorDrawable)

- Vector(向量圖) 對比 Bitmap(點陣圖)
繪製效率 Vector依賴於CUP計算,適合影像簡單的情況。Bitmap可藉助於GPU加速,適合影像複雜的情況。
適用情況 Vector適用於ICON、Button、ImageView的小圖片,或者需要動畫效果時。Bitmap由於在GPU中有快取功能,所以Bitmap不能做頻繁的重繪。
載入速度 SVG快於PNG,但PNG有硬體加速,平均下來載入速度的提升彌補了繪製的速度缺陷。

VectorDrawable,向量圖動畫。使用需要新增相容庫,在app的build.gradle檔案相關節點下新增:

android {
    defaultConfig {
        vectorDrawables.useSupportLibrary = true
    }
}
dependencies {
    compile 'com.android.support:appcompat-v7:25.1.0'//23.2及以上
}

3.SVG和Vector

SVG是一套語法規範,在前端中使用。Vector只實現了SVG語法的Path標籤(為了提高解析效率),在Android中使用。

Vector的語法通過字母和數字的組合來描述一個路徑,不同字母則代表不同含義,例如:

M = moveto(M X,Y): 將畫筆移動到指定的座標位置
L = lineto(L X,Y): 畫直線到指定的座標位置
Z = closepath(): 關閉路徑

Vector還提供了一些封裝好的方法:

H = horizontal lineto(H X): 畫水平線到指定的X座標位置
V = vertical lineto(V Y): 畫垂直線到指定的Y座標位置

例如下面這個 Vector Asset 代表一個黑色的正方形:

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="24.0"
    android:viewportHeight="24.0">
    <path
        android:name="square"
        android:fillColor="#FF000000"
        android:pathData="M10,10 L20,10 L20,20 L10,20 z"/>
</vector>

解釋:

1)如上是依次以 M10,10 -> L20,10 -> L20,20 -> L10,20 -> z 進行繪製;
2)width/height 代表vector的大小;viewportWidth/viewportHeight 則代表把vector均勻分為24整份,pathData就按照這裡的標準來繪製。

4.使用靜態的VectorDrawable

在像ImageView、ImageButton這樣的控制元件中使用是非常簡單的:

<!-- 注意:這裡用的是srcCompat -->
app:srcCompat="@drawable/vector_image"

如果要在Button這種帶點選效果的控制元件中使用,則需要通過selector來進行設定,並在對應Activity中開啟下面的設定:

static {
    AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
}

5.使用動態的VectorDrawable

這裡需要 結合 屬性動畫 來實現動態的VectorDrawable效果:

1)建立VectorDrawable檔案 arrow.xml

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportHeight="24.0"
    android:viewportWidth="24.0">
    <group android:name="left">
        <path
            android:fillColor="#FF000000"
            android:pathData="M6,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
    </group>
    <group android:name="right">
        <path
            android:fillColor="#FF000000"
            android:pathData="M18,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
    </group>
</vector>

Android Studio 的 Preview視窗顯示效果如下:

arrow

2)為VectorDrawable建立屬性動畫:

左邊小球的屬性動畫 anim_left.xml 如下:

<?xml version="1.0" encoding="utf-8"?>
<objectAnimator
    xmlns:androd="http://schemas.android.com/apk/res/android"
    androd:duration="1000"
    androd:interpolator="@android:interpolator/overshoot"
    androd:propertyName="translateX"
    androd:repeatCount="infinite"
    androd:repeatMode="reverse"
    androd:valueFrom="0"
    androd:valueTo="10"
    androd:valueType="floatType">
</objectAnimator>
<!--
    duration="1000"          持續時間/毫秒
    interpolator             修飾動畫效果,定義動畫的變化率(加速,減速,重複,彈跳)
    propertyName="translateX"屬性名(還有前面回顧屬性動畫提到的屬性,另外還有顏色漸變fillColor/軌跡繪製trimPathStart)
    repeatCount="infinite"   無限次
    repeatMode="reverse"     重複模式:迴圈使用
    valueFrom="0"            起始值
    valueTo="10"             結束值
    valueType="floatType"    變化值的型別:浮點型變化
 -->

右邊小球的屬性動畫 anim_right.xml 如下:

<?xml version="1.0" encoding="utf-8"?>
<objectAnimator
    xmlns:androd="http://schemas.android.com/apk/res/android"
    androd:duration="1000"
    androd:interpolator="@android:interpolator/overshoot"
    androd:propertyName="translateX"
    androd:repeatCount="infinite"
    androd:repeatMode="reverse"
    androd:valueFrom="0"
    androd:valueTo="-10"
    androd:valueType="floatType">
</objectAnimator>

3)下來我們需要配置動畫粘合劑 animated-vector(arrow_anim.xml),讓屬性動畫作用於VectorDrawable:

<?xml version="1.0" encoding="utf-8"?>
<animated-vector
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/arrow">
    <target
        android:animation="@animator/anim_left"
        android:name="left"/>
    <target
        android:animation="@animator/anim_right"
        android:name="right"/>
</animated-vector>

4)粘合到一起之後,我們就可以在Activity的Layout檔案中引用了:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ImageView
        android:layout_width="match_parent"
        android:layout_height="45dp"
        app:srcCompat="@drawable/arrow_anim"
        android:onClick="anim"/>
</RelativeLayout>

5)在Activity中新增點選事件anim:

public void anim(View view) {
    ImageView imageView = (ImageView) view;
    Drawable drawable = imageView.getDrawable();
    if (drawable instanceof Animatable) {
        ((Animatable) drawable).start();
    }
}

到此,動態的VectorDrawable就編寫完畢了,我們執行程式,點選ImageView,出現瞭如下的效果:

VectorDrawable

有木有很贊。

6.VectorDrawable實現軌跡動畫

軌跡動畫關鍵的配置就是 objectAnimator 中 androd:propertyName=”trimPathStart” 屬性。

1)建立VectorDrawable檔案 path.xml

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="200dp"
    android:height="200dp"
    android:viewportHeight="500"
    android:viewportWidth="500">
    <group
        android:scaleX="5.0"
        android:scaleY="5.0">
        <path
            android:name="start"
            android:pathData="M 50.0,90.0 L 82.9193546357,27.2774101308 L 12.5993502926,35.8158045183 L 59.5726265715,88.837672697 L 76.5249063296,20.0595700732 L 10.2916450361,45.1785327898 L 68.5889268818,85.4182410261 L 68.5889268818,14.5817589739 L 10.2916450361,54.8214672102 L 76.5249063296,79.9404299268 L 59.5726265715,11.162327303 L 12.5993502926,64.1841954817 L 82.9193546357,72.7225898692 L 50.0,10.0 L 17.0806453643,72.7225898692 L 87.4006497074,64.1841954817 L 40.4273734285,11.162327303 L 23.4750936704,79.9404299268 L 89.7083549639,54.8214672102 L 31.4110731182,14.5817589739 L 31.4110731182,85.4182410261 L 89.7083549639,45.1785327898 L 23.4750936704,20.0595700732 L 40.4273734285,88.837672697 L 87.4006497074,35.8158045183 L 17.0806453643,27.2774101308 L 50.0,90.0Z"
            android:strokeColor="#000000"
            android:strokeWidth="2"/>
    </group>
</vector>

Android Studio 的 Preview視窗顯示效果如下:

2)為VectorDrawable建立屬性動畫:

屬性動畫 anim_path.xml 如下:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:androd="http://schemas.android.com/apk/res/android">
    <objectAnimator
        androd:duration="10000"
        androd:propertyName="trimPathStart"
        androd:repeatCount="infinite"
        androd:repeatMode="reverse"
        androd:valueFrom="1"
        androd:valueTo="0"
        androd:valueType="floatType">
    </objectAnimator>
    <objectAnimator
        androd:duration="10000"
        androd:propertyName="strokeColor"
        androd:repeatCount="infinite"
        androd:repeatMode="reverse"
        androd:valueFrom="@android:color/holo_red_dark"
        androd:valueTo="@android:color/holo_blue_dark"
        androd:valueType="colorType">
    </objectAnimator>
</set>

3)下來我們需要配置動畫粘合劑 animated-vector(path_anim.xml),讓屬性動畫作用於VectorDrawable:

<?xml version="1.0" encoding="utf-8"?>
<animated-vector
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/path">
    <target
        android:animation="@animator/anim_path"
        android:name="start"/>
</animated-vector>

4)粘合到一起之後,我們就可以在Activity的Layout檔案中引用了:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ImageView
        android:layout_width="match_parent"
        android:layout_height="100dp"
        app:srcCompat="@drawable/path_anim"
        android:onClick="anim"/>
</RelativeLayout>

5)在Activity中新增點選事件anim:

public void anim(View view) {
    ImageView imageView = (ImageView) view;
    Drawable drawable = imageView.getDrawable();
    if (drawable instanceof Animatable) {
        ((Animatable) drawable).start();
    }
}

到此,動態的VectorDrawable就編寫完畢了,我們執行程式,點選ImageView,出現瞭如下的效果:

7.VectorDrawable實現路徑變換動畫

軌跡動畫關鍵的配置就是 objectAnimator 中 androd:propertyName=”pathData” 和 androd:valueType=”pathType”屬性。這裡我們實現五角星向五邊形的變換動畫。

1)建立五角星VectorDrawable檔案 fivestar.xml

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="120dp"
    android:height="120dp"
    android:viewportHeight="64"
    android:viewportWidth="64">
    <group>
        <path
            android:name="star"
            android:fillColor="#22e171"
            android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z"
            android:strokeColor="#000000"
            android:strokeWidth="1"/>
    </group>
</vector>

Android Studio 的 Preview視窗顯示效果如下:

star

2)為VectorDrawable建立屬性動畫:

五角星的屬性動畫 anim_fivestar.xml 如下:

<?xml version="1.0" encoding="utf-8"?>
<objectAnimator
    xmlns:androd="http://schemas.android.com/apk/res/android"
    androd:duration="3000"
    androd:propertyName="pathData"
    androd:valueFrom="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z"
    androd:valueTo="M 48,54 L 31,54 15,54 10,35 6,23 25,10 32,4 40,10 58,23 54,35 z"
    androd:valueType="pathType">
</objectAnimator>

3)下來我們需要配置動畫粘合劑 animated-vector(fivestar_anim.xml),讓屬性動畫作用於VectorDrawable:

<?xml version="1.0" encoding="utf-8"?>
<animated-vector
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/fivestar">
    <target
        android:animation="@animator/anim_fivestar"
        android:name="star"/>
</animated-vector>

4)粘合到一起之後,我們就可以在Activity的Layout檔案中引用了:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ImageView
        android:layout_width="match_parent"
        android:layout_height="100dp"
        app:srcCompat="@drawable/fivestar_anim"
        android:onClick="animL"/>
</RelativeLayout>

5)在Activity中新增點選事件anim:

/**
  * 指該方法適用Android版本大於等於Android L
  * @param view
  */
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public void animL(View view) {
    ImageView imageView = (ImageView) view;
    AnimatedVectorDrawable drawable = (AnimatedVectorDrawable) getDrawable(R.drawable.fivestar_anim);
    imageView.setImageDrawable(drawable);
    if (drawable != null) {
        drawable.start();
    }
}

這裡的點選事件和前面介紹過的略有不同,需要考慮到VectorDrawable實現路徑變換動畫的相容性問題,故路徑變換動畫目前存在相容性問題。不能在4.X版本執行,這一點格外注意。不過我們同樣希望Google可以在後續版本中優化路徑變換動畫,提高相容性。

到此,動態的VectorDrawable就編寫完畢了,我們執行程式,點選ImageView,出現瞭如下的效果:

8.動態VectorDrawable的相容性問題

1.向下相容問題

1)路徑變換動畫(Path Morphing)
在Android pre-L版本下是無法使用的,例如將圓形變換成三角形的動畫。

2)路徑插值器(Path Interpolation)
在Android pre-L版本只能使用系統的插值器(一般情況下,系統提供的27種插值器已經足夠我們使用了),不能自定義。

2.向上相容問題

1)路徑變換動畫(Path Morphing)
在Android L版本以上需要使用程式碼配置。

3.抽取string相容問題

不支援從String.xml中讀取

<PathData>

同時這裡也希望Google可以在後面的版本中修復這些動態VectorDrawable的相容性問題。

相關文章