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

  • 19
    いいね
  • 0
    コメント

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

1. View#startAnimation(Animation) でアニメーションする

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

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

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

update_portrait.gif

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

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

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

update_landscape_with_text.gif

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

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

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

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

update_landscape.gif

AnimatableDrawable は Y.A.M の 雑記帳: カスタムDrawableで複雑なプログレスを作る が参考になります。

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);
        }
    }
}

まとめ

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

この投稿は Goodpatch Advent Calendar 201610日目の記事です。