Twitter が Android で「いいね」アニメーションをどのように実現しているのか調査した件（AnimatedStateListDrawable）

# 概要

[Twitter](https://play.google.com/store/apps/details?id=com.twitter.android) が「いいね」ボタンのアニメーションをどのように実装しているのか調査してみました :mag:

<blockquote class="twitter-tweet" data-lang="ja"><p lang="en" dir="ltr">You can say a lot with a heart. Introducing a new way to show how you feel on Twitter: <a href="https://t.co/WKBEmORXNW">https://t.co/WKBEmORXNW</a> <a href="https://t.co/G4ZGe0rDTP">pic.twitter.com/G4ZGe0rDTP</a></p>&mdash; Twitter (@Twitter) <a href="https://twitter.com/Twitter/status/661558661131558915?ref_src=twsrc%5Etfw">2015年11月3日</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

# 調べてみた結果 :computer:

[GitHub](https://github.com/) で公開されている [Twitter Kit for Android](https://github.com/twitter/twitter-kit-android) のコミット「[Add like heart animations for API 21+](https://github.com/twitter/twitter-kit-android/commit/ec82b24029ea2f3a519d054882938f3d1aaac951)（2015 年 11 月 9 日）」によると、[AnimatedStateListDrawable](https://developer.android.com/reference/android/graphics/drawable/AnimatedStateListDrawable.html) を用いているようでした。

## [AnimatedStateListDrawable](https://developer.android.com/reference/android/graphics/drawable/AnimatedStateListDrawable.html) とは

API レベル 21 ([Lollipop](https://developer.android.com/about/versions/android-5.0.html)) から追加された機能で、画像をパラパラ漫画の要領でアニメーションにします。

![parapara_manga.png](https://qiita-image-store.s3.amazonaws.com/0/6881/b4472610-1f58-7869-bb9b-a2d5132bde1f.png)

[Twitter](https://play.google.com/store/apps/details?id=com.twitter.android) の「いいね」ボタンは [`res/drawable-xhdpi`](https://github.com/twitter/twitter-kit-android/tree/master/tweet-ui/src/main/res/drawable-xhdpi) 内にある 60 枚の PNG 画像を使用していました。

## Android 4.4 ([KitKat](https://developer.android.com/about/versions/kitkat.html)) 以下の対応はどうしているのか

[Twitter](https://play.google.com/store/apps/details?id=com.twitter.android) は [`res/drawable`](https://github.com/twitter/twitter-kit-android/tree/master/tweet-ui/src/main/res/drawable) と [`res/drawable-v21`](https://github.com/twitter/twitter-kit-android/tree/master/tweet-ui/src/main/res/drawable-v21) パッケージ直下に同名の XML ファイルを作成して、中身を [`<selector >`](https://github.com/twitter/twitter-kit-android/blob/master/tweet-ui/src/main/res/drawable/tw__like_action.xml) タグと [`<animated-selector >`](https://github.com/twitter/twitter-kit-android/blob/master/tweet-ui/src/main/res/drawable-v21/tw__like_action.xml) タグで書き分ける事により、Android 4.4 以下でアニメーション非対応とし、Android 5.0 以上でアニメーションを有効にしていました。

## ボタンの ON/OFF

ボタン ON 時のアニメーションが実施されている途中にボタンをタップすると、アニメーションがキャンセルされ OFF 状態に切り替わりました。

# サンプル

[Twitter Kit for Android](https://github.com/twitter/twitter-kit-android) の [ImageButton](https://developer.android.com/reference/android/widget/ImageButton.html) を拡張した[クラス](https://github.com/twitter/twitter-kit-android/blob/master/tweet-ui/src/main/java/com/twitter/sdk/android/tweetui/ToggleImageButton.java)とコミット「[Add like heart animations for API 21+](https://github.com/twitter/twitter-kit-android/commit/ec82b24029ea2f3a519d054882938f3d1aaac951)」を参考にアニメーション付きボタンを作ってみます。

## リソース

39 枚の PNG 画像を用いてボタン ON 時のアニメーションを作成します。

<img width="1281" alt="スクリーンショット 2017-10-08 3.02.17.png" src="https://qiita-image-store.s3.amazonaws.com/0/6881/bb55da94-a96c-716b-162c-580c057cd164.png">

## コード

``` res/values/styles.xml
<resources>
    <declare-styleable name="AnimationImageButton">
        <attr name="state_toggled_on" format="boolean" />
    </declare-styleable>
</resources>
```

``` res/drawable-v21/star_animation.xml
<animated-selector xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
        android:id="@+id/animation_on"
        app:state_toggled_on="true">
        <bitmap android:src="@drawable/star_animation_01" />
    </item>

    <item android:id="@+id/animation_off">
        <bitmap android:src="@drawable/off" />
    </item>

    <transition
        android:fromId="@+id/animation_off"
        android:toId="@+id/animation_on">
        <animation-list>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_01" />
            </item>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_02" />
            </item>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_03" />
            </item>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_04" />
            </item>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_05" />
            </item>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_06" />
            </item>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_07" />
            </item>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_08" />
            </item>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_09" />
            </item>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_10" />
            </item>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_11" />
            </item>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_12" />
            </item>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_13" />
            </item>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_14" />
            </item>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_15" />
            </item>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_16" />
            </item>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_17" />
            </item>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_18" />
            </item>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_19" />
            </item>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_20" />
            </item>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_21" />
            </item>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_22" />
            </item>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_23" />
            </item>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_24" />
            </item>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_25" />
            </item>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_26" />
            </item>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_27" />
            </item>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_28" />
            </item>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_29" />
            </item>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_30" />
            </item>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_31" />
            </item>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_32" />
            </item>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_33" />
            </item>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_34" />
            </item>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_35" />
            </item>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_36" />
            </item>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_37" />
            </item>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_38" />
            </item>
            <item android:duration="30">
                <bitmap android:src="@drawable/star_animation_39" />
            </item>
        </animation-list>
    </transition>
</animated-selector>
```

``` res/drawable/star_animation.xml
<selector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
    <item android:drawable="@drawable/star_animation_01" app:state_toggled_on="true" />
    <item android:drawable="@drawable/off" android:state_enabled="false" />
</selector>
```

``` AnimationImageButton.java
package com.sample.application;

import android.content.Context;
import android.support.v7.widget.AppCompatImageButton;
import android.util.AttributeSet;

/**
 * アニメーション付き {@link AppCompatImageButton}
 */
public class AnimationImageButton extends AppCompatImageButton {

    private static final int[] STATE_TOGGLED_ON;
    boolean isToggledOn;

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

    public AnimationImageButton(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public AnimationImageButton(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        setImageResource(R.drawable.star_animation);
        setToggledOn(false);
    }

    public int[] onCreateDrawableState(int extraSpace) {
        int[] drawableState = super.onCreateDrawableState(extraSpace + 2);
        if (isToggledOn) {
            mergeDrawableStates(drawableState, STATE_TOGGLED_ON);
        }

        return drawableState;
    }

    public boolean performClick() {
        toggle();
        return super.performClick();
    }

    public void setToggledOn(boolean isToggledOn) {
        this.isToggledOn = isToggledOn;
        setContentDescription(isToggledOn ? "ON" : "OFF");
        refreshDrawableState();
    }

    public void toggle() {
        this.setToggledOn(!isToggledOn);
    }

    public boolean isToggledOn() {
        return isToggledOn;
    }

    static {
        STATE_TOGGLED_ON = new int[]{R.attr.state_toggled_on};
    }
}
```

``` res/layout/activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.sample.application.AnimationImageButton
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    <!-- アニメーション付きボタン -->
</android.support.constraint.ConstraintLayout>
```

``` MainActivity.java
package com.sample.application;

import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.view.View;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

            }
        });
    }
}
```

## 実行結果 :arrow_forward:

![animation.gif](https://qiita-image-store.s3.amazonaws.com/0/6881/bd52eb92-f28c-d42c-5f88-718417600ce0.gif)

# 関連項目

iOS と Android、[React Native](https://facebook.github.io/react-native/) で高品質なアニメーションを簡単に作成できる [Lottie](https://airbnb.design/lottie/) というライブラリがあります。