Android
GoodpatchDay 10

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

More than 1 year has passed since last update.

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



まとめ

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