Androidアプリ開発 アニメーション FloatingActionButtonのアニメーション

Androidアプリを作成するとき、Material DesignではFloatingActionButtonを使うことがあります。
さらにアニメーションを使うと、ユーザーが視覚的にアクションを把握しやすくなります。
この記事は、AndroidアプリでFloatingActionButtonをアニメーションさせる実装方法を記載した記事です。
環境はAndroid 8.1 (API level 27) です。
環境
- macOS Sierra
- android sdk 27
- Oracle jdk version 1.8.0_72
- Android Studio 3.0.1
- kotlin 1.1.51
難易度
初心者向け
サンプルコード
動画
記事の内容を実装したアプリケーションの動画です。
アニメーションの種類
Androidには、sdkのバージョンで色々なアニメーションが用意されています。 また、それぞれのアニメーションは実装方法も異なります。
この記事では、ObjectAnimatorを使ってアニメーションを実装します。
ObjectAnimatorはAndroid 3.0(API level 11)からサポートされているアニメーションですが、現在でもよく利用されているアニメーションです。
アプリ設計
FloatingActionButtonのアニメーションは、以下の仕様を満たすこととします。
FloatingActionボタン押下(open)
- FloatingActionボタンが90度回転する
FloatingActionボタン押下(close)
- FloatingActionボタンが-90度回転して元の向きに戻る
実装
最初に、FloatingActionボタン押下(open)処理を実装します。
class FloatingActionButtonActivity : AppCompatActivity() { enum class FloatingActionState { NORMAL, ANIMATED } private lateinit var fab: FloatingActionButton private lateinit var state: FloatingActionState private lateinit var openingAnimation: Animator override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_floating_action_button) setSupportActionBar(toolbar) state = FloatingActionState.NORMAL fab = findViewById<FloatingActionButton>(R.id.fab) fab.setOnClickListener(toggleFloatingActionButtonListener) openingAnimation = createOpenFloatingActionButton() } override fun onPause() { super.onPause() if (openingAnimation.isRunning) { openingAnimation.cancel() } } // SAMコンストラクタ var toggleFloatingActionButtonListener = View.OnClickListener { if (state == FloatingActionState.NORMAL && !openingAnimation.isRunning) { openFloatingActionButton() } } private fun createOpenFloatingActionButton(): Animator { val anim = AnimatorInflater.loadAnimator(applicationContext, R.animator.fab_open) anim.setTarget(fab) anim.interpolator = DecelerateInterpolator() anim.addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator?) { state = FloatingActionState.ANIMATED } override fun onAnimationCancel(animation: Animator?) { animation?.end() state = FloatingActionState.ANIMATED } }) return anim } private fun openFloatingActionButton() { openingAnimation.start() } }
■ 実装の解説
1. Animator実装
FloatingActionButtonを90°回転させるアニメーションを作成します。
private fun createOpenFloatingActionButton(): Animator { val anim = AnimatorInflater.loadAnimator(applicationContext, R.animator.fab_open) return anim }
AnimatorInflater#loadAnimatorでxmlに記述したアニメーションを取得します。
Animatorクラスなので、xmlファイルはres/animatorフォルダに配置します。
Animationクラスとは異なることに注意してきださい。Animationは、res/animatorフォルダにxmlを配置します。
ターゲットを90°回転させるアニメーションの場合、以下のようなxmlを作成します。
<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android"> <objectAnimator android:duration="300" android:propertyName="rotation" android:valueFrom="0.0" android:valueTo="90.0" /> </set>
ObjectAnimatorを使って90.0°(valueFrom=0.0, valueTo=90.0)回転(ropertyName=rotation)する設定です。
3. ターゲットの設定
Animator#setTargetでアニメーションするオブジェクトを設定します。
private fun createOpenFloatingActionButton(): Animator { val anim = AnimatorInflater.loadAnimator(applicationContext, R.animator.fab_open) anim.setTarget(fab) return anim }
setTargetはオブジェクトを弱い参照で保持します。
4. Interpolatorの設定
Interpolatorを利用すると、アニメーションの動きを補正できます。
private fun createOpenFloatingActionButton(): Animator { val anim = AnimatorInflater.loadAnimator(applicationContext, R.animator.fab_open) anim.setTarget(fab) anim.interpolator = DecelerateInterpolator() return anim }
DecelerateInterpolatorを利用すると、勢いよく速度が増加した後に減速します。
5. addListenerの実装
Animator#addListenerメソッドを使ってアニメーションの終了時やキャンセル時の処理を実装します。
private fun createOpenFloatingActionButton(): Animator { val anim = AnimatorInflater.loadAnimator(applicationContext, R.animator.fab_open) anim.setTarget(fab) anim.interpolator = DecelerateInterpolator() anim.addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator?) { state = FloatingActionState.ANIMATED } }) return anim }
AnimatorListenerは、AnimatorListenerでなくAnimatorListenerAdapterを使うことをオススメします。AnimatorListenerAdapterを使うと、必要なメソッドだけをオーバーライドすれば良いので、可読性が向上します。
ここでは、アニメーションが終了したらstate変数にANIMATEDの状態を設定しています。
6. Animatorの細部実装
ボタンを連続クリックされると、アプリが落ちる原因になるので、状態管理もしっかりと実装しましょう。また、onPause時はアニメーションを停止させます。なぜなら、アニメーションを別スレッドで動かすとアプリがクラッシュする可能性があるからです。
override fun onPause() { super.onPause() if (openingAnimation.isRunning) { openingAnimation.cancel() } }
onPause()内でAnimator#endを呼び出してアニメーションを停止しても良いのですが、cancel() → end()としたほうがコードが読みやすいし、拡張性もあるのかな。。。と個人的に思います。
private fun createOpenFloatingActionButton(): Animator { val anim = AnimatorInflater.loadAnimator(applicationContext, R.animator.fab_open) anim.setTarget(fab) anim.interpolator = DecelerateInterpolator() anim.addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator?) { state = FloatingActionState.ANIMATED } override fun onAnimationCancel(animation: Animator?) { animation?.end() state = FloatingActionState.ANIMATED } }) return anim }
7. SAMコンストラクタ
View.OnClickListenerにはSAMコンストラクタを利用しています。
fab.setOnClickListener(toggleFloatingActionButtonListener) // SAM constructor var toggleFloatingActionButtonListener = View.OnClickListener { if (state == FloatingActionState.NORMAL && !openingAnimation.isRunning) { openFloatingActionButton() } }
もちろん、メソッドを利用しても問題ありません。ただ、SAMコンストラクタを使った方が、kotlinらしく書けるし、インスタンスを生成するコストを省略できます。
8. apply関数の適用
Animatorの生成と設定をapply関数でまとめても良いと思います。
private fun createOpenFloatingActionButton(): Animator { return AnimatorInflater.loadAnimator(applicationContext, R.animator.fab_open).apply { setTarget(fab) interpolator = DecelerateInterpolator() addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator?) { state = FloatingActionState.ANIMATED } override fun onAnimationCancel(animation: Animator?) { animation?.end() state = FloatingActionState.ANIMATED } }) } }
最後に、FloatingActionボタン押下(close)処理を実装します。
class FloatingActionButtonActivity : AppCompatActivity() { enum class FloatingActionState { NORMAL, ANIMATED } private lateinit var fab: FloatingActionButton private lateinit var state: FloatingActionState private lateinit var openingAnimation: Animator private lateinit var closingAnimation: Animator override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_floating_action_button) setSupportActionBar(toolbar) state = FloatingActionState.NORMAL fab = findViewById<FloatingActionButton>(R.id.fab) fab.setOnClickListener(toggleFloatingActionButtonListener) openingAnimation = createOpenFloatingActionButton() closingAnimation = createCloseFloatingActionButton() } override fun onPause() { super.onPause() if (openingAnimation.isRunning) { openingAnimation.cancel() } if (closingAnimation.isRunning) { closingAnimation.cancel() } } // SAMコンストラクタ var toggleFloatingActionButtonListener = View.OnClickListener { if (state == FloatingActionState.NORMAL && !openingAnimation.isRunning) { openFloatingActionButton() } else if (state == FloatingActionState.ANIMATED && !closingAnimation.isRunning) { closeFloatingActionFragment() } } private fun createOpenFloatingActionButton(): Animator { val anim = AnimatorInflater.loadAnimator(applicationContext, R.animator.fab_open) anim.setTarget(fab) anim.interpolator = DecelerateInterpolator() anim.addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator?) { state = FloatingActionState.ANIMATED } override fun onAnimationCancel(animation: Animator?) { animation?.end() state = FloatingActionState.ANIMATED } }) return anim } private fun createCloseFloatingActionButton(): Animator { val anim = AnimatorInflater.loadAnimator(applicationContext, R.animator.fab_close) anim.setTarget(fab) anim.interpolator = AccelerateInterpolator() anim.addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator?) { state = FloatingActionState.NORMAL } override fun onAnimationCancel(animation: Animator?) { animation?.end() state = FloatingActionState.NORMAL } }) return anim } private fun openFloatingActionButton() { openingAnimation.start() } private fun closeFloatingActionFragment() { closingAnimation.start() } }
FloatingActionボタン押下(open)処理とほぼ同じなので、詳細な説明は省きます。
アニメーションが終了したらstate変数にNORMALの状態を設定して、最初の状態に戻しています。
結論
アニメーションを使うと、ユーザーが視覚的にアクションを判断できるようになるので、操作の邪魔にならないよううまく使っていきましょう。