AnimatedVectorDrawableでアイコンのON/OFFアニメーションを作る


やりたいこと

  • Googleのアイコンでよく見る、ON→OFF状態にアニメーションで斜線が引かれるアイコンを作りたい

参考

https://github.com/alexjlockwood/adp-delightful-details
https://www.androiddesignpatterns.com/2016/11/introduction-to-icon-animation-techniques.html

画像の準備

使用する画像はすべてSVGで作成し、VectorDrawableとして取り込みます。

アイコンの画像

ON/OFFで同じ画像を使用するため使用する画像は一枚です。

VectorDrawableのPathに関しては、stringリソースにまとめておくと他のファイルで使いまわせたり、VectorDrawableのxmlファイルが見やすくなり、便利です。

vector_paths.xml
<resources>
    <string name="enable_mask_clip_path">
        M 0 0 L 240 0 L 240 240 ・・・
    </string>
    <string name="disable_mask_clip_path">
        M 0 0 L 240 0 L 240 ・・・
    </string>
    <string name="cross_path">
        M46.1701454,41.4327542 L202.939622,203.496893
    </string>
    <string name="disable_cross_path">
        M 39.901 47.514 L 197.384 210.162 L 210.321 196.863 L 52.634 34.86 Z
    </string>
    <string name="ic_notification_path">
        M 120 220 C 131 220 140 211 140 200 ・・・
    </string>
</resources>

クリッピングするための画像

クリッピング用画像で透過にした部分だけアイコン画像が描画されなくなるので、斜線部分だけ透過にし他はベタ塗りにした画像を作成します。

クリッピング前画像


クリッピング後画像

斜線を引くための画像

クリッピングとは別に斜線を引くための画像です。VectorDrawableのxml側で幅を指定するため、画像としては幅のない直線のpathの画像を作成します。
(青い部分がパスで他の部分は透過です。)

斜線の画像

drawableファイルの作成

VectorDrawable

2つファイルを作ります

vd_notification_enabled.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:name="root"
    ・・・>
    <clip-path
        android:name="maskClipPath"
        android:pathData="@string/enable_mask_clip_path" />
    <path
        android:name="crossPath"
        android:pathData="@string/cross_path"
        android:strokeColor="@android:color/black"
        android:strokeWidth="20"
        android:trimPathEnd="0" />
    <path
        android:fillColor="@android:color/black"
        android:pathData="@string/ic_notification_path"
        android:strokeWidth="1" />
</vector>

ON状態のVectorDrawableです。

  • 一番下のpath : アイコンのパス
  • maskClipPath : クリッピング領域の指定
    • クリッピング領域がかからない画像を指定します。
  • crossPath : 斜線のパス
    • 斜線のパス(cross_path)は1つですが、trimPathEnd=0とすることで、斜線の終点が始点と同じ、つまり線が引かれていない状態を表すことができます。
      trimPathEndはmin=0,max=1の値をとり、終点をパスのどの位置にするかを指定することができます。
      trimPathStart/End/Offsetの挙動に関しては、下記の「Trimming stroked paths」の項で確認するとわかりやすいです。
      https://www.androiddesignpatterns.com/2016/11/introduction-to-icon-animation-techniques.html
vd_notification_disabled.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:name="root"
    android:alpha="0.3"
    ・・・>
    <clip-path
        android:name="maskClipPath"
        android:pathData="@string/disable_mask_clip_path" />
    <path
        android:name="crossPath"
        android:pathData="@string/cross_path"
        android:strokeColor="@android:color/black"
        android:strokeWidth="20"/>
    <path
        android:fillColor="@android:color/black"
        android:pathData="@string/ic_notification_path"
        android:strokeWidth="1" />
</vector>

OFF状態のVectorDrawableです。ON状態のものと構成は一緒ですが、
・全体にアルファ値(0.3)を指定
・maskClipPathにOFF状態のものを指定
・斜線のパスにtrimPathEndを指定していない
点が異なります。trimPathEndは指定しないと1になるため、斜線が最後まで引かれている状態を表します。

AnimatedVectorDrawable

2つファイルを作ります

avd_notfication_enabled_to_diabled.xml
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:aapt="http://schemas.android.com/aapt"
    android:drawable="@drawable/vd_notification_enabled">
    <target android:name="root">
        <aapt:attr name="android:animation">
            <objectAnimator
                android:duration="350"
                android:interpolator="@android:interpolator/fast_out_slow_in"
                android:propertyName="alpha"
                android:valueFrom="1"
                android:valueTo="0.3" />
        </aapt:attr>
    </target>

    <target android:name="maskClipPath">
        <aapt:attr name="android:animation">
            <objectAnimator
                android:duration="350"
                android:interpolator="@android:interpolator/fast_out_slow_in"
                android:propertyName="pathData"
                android:valueFrom="@string/enable_mask_clip_path"
                android:valueTo="@string/disable_mask_clip_path"
                android:valueType="pathType" />
        </aapt:attr>
    </target>

    <target android:name="crossPath">
        <aapt:attr name="android:animation">
            <objectAnimator
                android:duration="350"
                android:interpolator="@android:interpolator/linear_out_slow_in"
                android:propertyName="trimPathEnd"
                android:valueFrom="0"
                android:valueTo="1" />
        </aapt:attr>
    </target>

</animated-vector>

ON状態からOFF状態へ遷移するアニメーションを定義します。

  • 一番外側のanimated-vectorタグに遷移前のVectorDrawableを指定
  • VectorDrawableの各要素をtargetで指定してそれぞれアニメーションを定義
    • “root”にalphaアニメーションを指定
      • 透過するアニメーション
    • “maskClipPath”にpathDataアニメーションを指定
      • クリッピング領域が変化するアニメーション
    • “crossPath”にtrimPathEndアニメーションを指定
      • 斜線が引かれるアニメーション
avd_notfication_disabled_to_enabled.xml
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:aapt="http://schemas.android.com/aapt"
    android:drawable="@drawable/vd_notification_disabled">

    <target android:name="root">
        <aapt:attr name="android:animation">
            <objectAnimator
                android:duration="350"
                android:interpolator="@android:interpolator/fast_out_slow_in"
                android:propertyName="alpha"
                android:valueFrom="0.3"
                android:valueTo="1" />
        </aapt:attr>
    </target>

    <target android:name="maskClipPath">
        <aapt:attr name="android:animation">
            <objectAnimator
                android:duration="350"
                android:interpolator="@android:interpolator/linear_out_slow_in"
                android:propertyName="pathData"
                android:valueFrom="@string/disable_mask_clip_path"
                android:valueTo="@string/enable_mask_clip_path"
                android:valueType="pathType" />
        </aapt:attr>
    </target>

    <target android:name="crossPath">
        <aapt:attr name="android:animation">
            <objectAnimator
                android:duration="350"
                android:interpolator="@android:interpolator/linear_out_slow_in"
                android:propertyName="trimPathEnd"
                android:valueFrom="1"
                android:valueTo="0" />
        </aapt:attr>
    </target>

</animated-vector>

ON状態からOFF状態へ遷移するアニメーションを定義します。
OFF状態からON状態のアニメーションの逆を記述します。

AnimatedStateListDrawable

状態によるアイコンの切り替え(アニメーション付き)を定義します。

asl_notification.xml
<animated-selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/enable"
        android:drawable="@drawable/vd_notification_enabled"
        android:state_checked="true" />
    <item
        android:id="@+id/disable"
        android:drawable="@drawable/vd_notification_disabled" />
    <transition
        android:drawable="@drawable/avd_notification_disabled_to_enabled"
        android:fromId="@id/disable"
        android:toId="@id/enable" />
    <transition
        android:drawable="@drawable/avd_notification_enabled_to_disabled"
        android:fromId="@id/enable"
        android:toId="@id/disable" />
</animated-selector>

itemタグでそれぞれの状態のVectorDrawableを定義、
transitionタグで遷移時のAnimatedVectorDrawableを定義します。
今回はON状態としてstate_checkedを使用しました。

コードでの状態切り替え

AnimatedStateListDrawableに対して、コードから状態の切り替えを行います。

fun onClickIcon(view: ImageView) {
    isNotificationEnable = !isNotificationEnable
    val stateSet = intArrayOf(android.R.attr.state_checked * if (isNotificationEnable) 1 else -1)
    view.setImageState(stateSet, true)
}

ImageView#setImageState(int[] state, boolean merge)を用いて状態の切り替えを行います。今回はstate_checkedを状態として使ったので、上記のようなコードになっています。

完成

補足

  • アニメーションにpathDataを用いているため、5.0以上でのみ使用可能です。