Toolbar のメニューアイコンをアニメーションする

Toolbar にあるメニューアイコンを押した時にアニメーションを実装したいと思ったことはありませんか？ 例えば何らかのデータを更新するメニューだったとして、プログレスバーを別の場所に表示するより、メニューのアイコンが変化した方がユーザには分かりやすい場合もあると思います。そこで、Toolbar にあるメニューアイコンをアニメーションする方法をご紹介したいと思います。

# 1. [View#startAnimation(Animation)](https://developer.android.com/reference/android/view/View.html#startAnimation(android.view.animation.Animation)) でアニメーションする

一番最初に思いついたのは、単純に startAnimation でアニメーションを開始する方法です。メニューの View は [Activity#findViewById(int)](https://developer.android.com/reference/android/app/Activity.html#findViewById(int)) に該当のメニュー ID で検索すると取得できます。

```java
View menuView = activity.findViewById(menuItem.getItemId());
// アニメーションを開始
menuView.startAnimation(animation);

// アニメーションを終了
animation.cancel();
animation.reset();
menuView.clearAnimation();
```

![update_portrait.gif](https://qiita-image-store.s3.amazonaws.com/0/58593/06e17399-f48d-6d59-d5ab-00f72ee91ba2.gif)


これで Toolbar に表示されているメニューにアニメーションが行われます。

# 2. アイコンだけアニメーションする

しかし、menuView は TextView クラスを継承した ActionMenuItemView で、テキストを表示することも可能です。このテキストが表示されていると、テキストも一緒に回転してしまう不具合がありました。

![update_landscape_with_text.gif](https://qiita-image-store.s3.amazonaws.com/0/58593/43ffdb2a-653c-3be1-f38b-2854c00076c9.gif)

これを回避するためにアイコンだけアニメーションするようにします。
~~menuView~~ menuItem にアニメーションできる Drawable をセットした後、そのアイコンを取得してアニメーションを開始します。
アイコンは ~~[TextView.getCompoundDrawables()](https://developer.android.com/reference/android/widget/TextView.html#getCompoundDrawables()) の left、つまり 0 番目で取得することができます。~~ [MenuItem#getIcon()](https://developer.android.com/reference/android/view/MenuItem.html#getIcon()) で取得できます。


```java:アイコンだけのアニメーション
menuItem.setIcon(animatableDrawable);

Drawable leftDrawable = menuItem.getIcon();
// アニメーションを開始する
((AnimatableDrawable) leftDrawable).start();

// アニメーションを終了する
((AnimatableDrawable) leftDrawable).stop();
```

![update_landscape.gif](https://qiita-image-store.s3.amazonaws.com/0/58593/84e730c8-c798-a2ea-bc66-c0a18e3f9165.gif)

AnimatableDrawable は [Y.A.M の 雑記帳: カスタムDrawableで複雑なプログレスを作る](http://y-anz-m.blogspot.jp/2015/05/drawable.html) が参考になります。

```java:AnimatableDrawable.java

public class AnimatableDrawable extends BitmapDrawable implements Animatable {

    private ValueAnimator valueAnimator;

    private boolean canAnimationFinish;

    public AnimatableDrawable(Resources res, BitmapDrawable original) {
        super(res, original.getBitmap());
        init();
    }

    private void init() {
        valueAnimator = ValueAnimator.ofFloat(0f, 1.0f);
        valueAnimator.setDuration(540);
        valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
        valueAnimator.setInterpolator(new RotateInterpolator());
        valueAnimator.addListener(new UpdateAnimatorListener());
    }

    @Override
    public void start() {
        if (valueAnimator.isStarted()) {
            return;
        }

        canAnimationFinish = false;
        valueAnimator.start();

        invalidateSelf();
    }

    @Override
    public void stop() {
        canAnimationFinish = true;
    }

    @Override
    public boolean isRunning() {
        return valueAnimator.isRunning();
    }

    @Override
    public void draw(@NonNull Canvas canvas) {
        Bitmap bitmap = getBitmap();
        if (bitmap == null) {
            return;
        }

        if (valueAnimator.isStarted()) {
            int width = bitmap.getWidth();
            int height = bitmap.getHeight();
            float degrees = -360 * valueAnimator.getAnimatedFraction();
            float px = width / 2;
            float py = height / 2;
            Matrix matrix = new Matrix();
            matrix.postTranslate(-width / 2, -height / 2);
            matrix.postRotate(degrees);
            matrix.postTranslate(px, py);
            canvas.drawBitmap(bitmap, matrix, null);

            invalidateSelf();
        } else {
            super.draw(canvas);
        }
    }

    @Override
    public void setAlpha(int i) {

    }

    @Override
    public void setColorFilter(ColorFilter colorFilter) {

    }

    @Override
    public int getOpacity() {
        return PixelFormat.TRANSLUCENT;
    }

    private class UpdateAnimatorListener implements Animator.AnimatorListener {

        @Override
        public void onAnimationStart(Animator animator) {
            ((RotateInterpolator) animator.getInterpolator()).stage = RotateInterpolator.Stage.start;
        }

        @Override
        public void onAnimationEnd(Animator animator) {

        }

        @Override
        public void onAnimationCancel(Animator animator) {

        }

        @Override
        public void onAnimationRepeat(Animator animator) {
            RotateInterpolator interpolator = (RotateInterpolator) animator.getInterpolator();
            if (interpolator.stage == RotateInterpolator.Stage.start) {
                interpolator.stage = RotateInterpolator.Stage.repeating;
            } else if (canAnimationFinish) {
                if (interpolator.stage == RotateInterpolator.Stage.repeating) {
                    interpolator.stage = RotateInterpolator.Stage.end;
                } else {
                    animator.end();
                }
            }
        }
    }

    private static class RotateInterpolator implements Interpolator {

        private Stage stage = Stage.start;

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

        enum Stage {
            start {
                @Override
                public float getInterpolation(float input) {
                    return input * input;
                }
            },
            repeating {
                @Override
                public float getInterpolation(float input) {
                    return input * 2;
                }
            },
            end {
                @Override
                public float getInterpolation(float input) {
                    return 1.0f - (1.0f - input) * (1.0f - input);
                }
            },

            ;

            public abstract float getInterpolation(float input);
        }
    }
}
```

# まとめ

ユーザの操作に何も反応がないとユーザが動いているのか不安になることも考えられます。プログレスバーを出すほどでもないし、かといって何もないのは…というのであればアニメーションを実装してみてはいかがでしょうか？