Jetpack Compose でのシンプルなアニメーション

1. 始める前に

この Codelab では、Android アプリにシンプルなアニメーションを追加する方法について説明します。アニメーションを使用することで、アプリはよりインタラクティブかつ魅力的で、ユーザーが簡単に解釈できるものになります。画面上いっぱいに情報が表示されいても、変更箇所の一つ一つにアニメーションを適用すれば、ユーザーは何が変更されたかを容易に把握できます。

アプリのユーザー インターフェースで使用できるアニメーションにはさまざまなものがあります。アイテムの表示と非表示をフェードイン / フェードアウトで表現したり、アイテムを画面上で(時には画面外に出ていくように)動かしたり、注意を引く仕方で変形させたりできます。これにより、アプリの UI が表現力豊かなものになり、使いやすくなります。

また、アニメーションでアプリの外観を洗練させることができます。これはエレガントな印象を与えるだけでなく、ユーザーにもメリットがあります。

あるタスクに対する報酬をユーザーに提供する際、それをアニメーションで表現すれば、ユーザー ジャーニーにおける重要な場面がより意味のあるものになります。

キーパッドの入力操作に反応するアニメーション要素で、操作が成功したかどうかを示すフィードバックを提供できます。

リストアイテムにアニメーションを取り入れて、コンテンツが読み込み中であることを視覚的に示します。

スワイプで開く操作をアニメーションで表示することで、必要となる操作を誘導できます。

アイコンをアニメーションにすることで、そのアイコンの意味を面白い形で補完したり、加えたりできます。

前提条件

  • 関数、ラムダ、ステートレス コンポーザブルなど、Kotlin に関する知識。
  • Jetpack Compose でレイアウトを作成する方法に関する基本的な知識。
  • Jetpack Compose でリストを作成する方法に関する基本的な知識。
  • マテリアル デザインに関する基本的な知識。

学習内容

  • Jetpack Compose で簡単なスプリング アニメーションを作成する方法。

作成するアプリの概要

  • Jetpack Compose Codelab でのマテリアル テーマ設定によって作成した Woof アプリを基に、ユーザーの操作に応答するシンプルなアニメーションを追加します。

必要なもの

  • Android Studio の最新バージョン
  • スターター コードをダウンロードするためのインターネット接続。

2. アプリの概要

Jetpack Compose Codelab のマテリアル テーマ設定を基に、マテリアル デザインを使用して、犬とその情報のリストを表示する Woof アプリを作成しました。

7252aa244a54ad90.png

この Codelab では、Woof アプリにアニメーションを追加します。また、好きなことに関する情報を追加して、リストアイテムを展開するとその情報が表示されるようにします。さらに、展開時のリストアイテムをアニメーション化するためのスプリング アニメーションも追加します。

1e9cf1dbc490924a.gif

スターター コードを取得する

まず、スターター コードをダウンロードします。

または、GitHub リポジトリのクローンを作成してコードを入手することもできます。

$ git clone
https://github.com/google-developer-training/basic-android-kotlin-compose-training-woof.git
$ cd basic-android-kotlin-compose-training-woof
$ git checkout material

コードは Woof app GitHub リポジトリで確認できます。

3.展開アイコンを追加する

スプリング アニメーションを作成する最初のステップとして、展開アイコン f88173321938c003.png を追加します。展開アイコンは、ユーザーがリストアイテムを展開するためのボタンになります。

9fbd3fb0daf35fd3.png

アイコン

アイコンとは、目的の機能を視覚的に伝達することで、ユーザーがユーザー インターフェースを理解することを助けるシンボルです。多くの場合、アイコンはユーザーがよく知っていると思われる現実の物体から着想を得ています。ほとんどのアイコンのデザインは、ユーザーが即座に理解できる最小限のレベルまで簡略化されています。たとえば、現実の鉛筆は字を書くために使用されるので、通常、鉛筆のアイコンは作成編集を表します。

写真撮影: Angelina Litvin(出典: Unsplash

白黒の鉛筆アイコン

マテリアル デザインでは、多くのニーズに対応した多数のアイコンが一般的なカテゴリ別に用意されています。

bfdb896506790c69.png

Gradle 依存関係を追加する

プロジェクトに material-icons-extended ライブラリ依存関係を追加します。このライブラリの Icons.Filled.ExpandLess アイコン 30c384f00846e69b.pngIcons.Filled.ExpandMore アイコン f88173321938c003.png を使用します。

  1. プロジェクト ペインで、[Gradle Scripts] > [build.gradle (Module: Woof.app)] を開きます。

f7fe58e936bbad3e.png

  1. build.gradle (Module: Woof.app) ファイルの最後までスクロールします。dependencies{} ブロックに次の行を追加します。
implementation "androidx.compose.material:material-icons-extended:$compose_version"

アイコン コンポーザブルを追加する

マテリアル アイコン ライブラリの展開アイコンを表示する関数を追加して、これをボタンとして使用します。

  1. MainActivity.kt で、DogItem() 関数の後に、DogItemButton() という新しいコンポーズ可能な関数を作成します。
  2. 展開された状態を示す Boolean、ボタンのクリック イベントのラムダ式、オプションの Modifier を次のように渡します。
@Composable
private fun DogItemButton(
    expanded: Boolean,
    onClick: () -> Unit,
    modifier: Modifier = Modifier
) {

}
  1. DogItemButton() 関数内に、onClick という名前付きパラメータを受け入れる IconButton() コンポーザブルを追加します。これは、アイコンが押されたときに呼び出される末尾のラムダ構文を使用するラムダで、これを渡される onClick 引数に設定します。
@Composable
private fun DogItemButton(
   // ...
) {
   IconButton(onClick = onClick) {

   }
}
  1. IconButton() ラムダブロック内に、imageVector という名前付きパラメータを持つ Icon コンポーザブルを追加し、Icons.Filled.ExpandMore に設定します。これが、リストアイテムの末尾に表示されるアイコンボタン f88173321938c003.png になります。Android Studio に Icon() コンポーザブルのパラメータに関する警告が表示されますが、これは後で修正します。
  2. 名前付きパラメータ tint を追加し、アイコンの色を MaterialTheme.colors.secondary に設定します。名前付きパラメータ contentDescription を追加し、文字列リソース R.string.expand_button_content_description に設定します。
import androidx.compose.material.Icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ExpandMore

IconButton(onClick = onClick) {
   Icon(
       imageVector = Icons.Filled.ExpandMore,
       tint = MaterialTheme.colors.secondary,
       contentDescription = stringResource(R.string.expand_button_content_description)
   )
}

アイコンを表示する

DogItemButton() コンポーザブルをレイアウトに追加して表示します。

  1. リストアイテムの展開の状態を保存するため、コンポーズ可能な関数 DogItem() の先頭に var を追加します。初期値を false に設定します。
var expanded by remember { mutableStateOf(false) }
  1. コンポーズ可能な関数 DogItem()Row ブロックの最後で、DogItemButton() 関数を呼び出し、展開された状態と空のラムダをコールバック用に渡します。このコードにより、リストアイテムにアイコンボタンが表示されます。
  2. リストアイテム内にアイコンボタンを表示するには、Row ブロックの最後のコンポーズ可能な関数 DogItem() で、DogInformation() を呼び出した後に DogItemButton() を呼び出します。コールバック用に expanded 状態と空のラムダを渡します。このラムダ関数は、後のステップで定義します。
Row(
   modifier = Modifier
       .fillMaxWidth()
       .padding(8.dp)
) {
   DogIcon(dog.imageResourceId)
   DogInformation(dog.name, dog.age)
   DogItemButton(
      expanded = expanded,
      onClick = { }
   )
}
  1. [Design] ペインでプレビューの [Build & Refresh] をクリックします。

a49643f08701a8d.png

展開ボタンがリストアイテムの末端に配置されていません。これは次のステップで修正します。

展開ボタンを配置する

展開ボタンをリストアイテムの末端に配置するには、Modifier.weight() 属性を使用してスペーサーをレイアウトに追加する必要があります。

Woof アプリの各リストアイテムの行に、犬の写真、犬の情報、展開ボタンが表示されています。重み 1f のスペーサー コンポーザブルを展開ボタンの前に追加して、ボタンアイコンを適切な位置に配置します。このスペーサーが行内で唯一重みを持つ子要素であるため、重みを持たない他の子要素の長さが決定された後、行内の残りのスペースを埋めます。

6c2b523849f0f626.png

スペーサーをリストアイテムの行に追加する

  1. コンポーズ可能な関数 DogItem()Row ブロックの末尾に、Spacer を追加します。weight(1f)Modifier を渡します。Modifier.weight() により、行の残りのスペースがスペーサーで埋められます。
Row(
   modifier = Modifier
       .fillMaxWidth()
       .padding(8.dp)
) {
   DogIcon(dog.imageResourceId)
   DogInformation(dog.name, dog.age)
   Spacer(Modifier.weight(1f))
   DogItemButton(
      expanded = expanded,
      onClick = { }
   )
}
  1. [Design] ペインでプレビューの [Build & Refresh] をクリックします。展開ボタンがリストアイテムの末端に表示されるようになりました。

f6a140413de9ad54.png

4. 好きなことを表示するコンポーザブルを追加する

このタスクでは、Text コンポーザブルを追加して犬の好きなことの情報を表示します。

66ea5cc5c7253d55.png

  1. 犬の好きなことの文字列リソース ID とオプションの Modifier を受け取る、DogHobby() という新しいコンポーズ可能な関数を作成します。
  2. DogHobby() 関数内で次のパディング属性を含む列を作成して、列と子コンポーザブルの間にスペースを追加します。
import androidx.annotation.StringRes

@Composable
fun DogHobby(@StringRes dogHobby: Int, modifier: Modifier = Modifier) {
   Column(
       modifier = modifier.padding(
           start = 16.dp,
            top = 8.dp,
            bottom = 16.dp,
            end = 16.dp
       )
   ) { }
}
  1. 列ブロック内に 2 つの Text コンポーザブルを追加します。一つは好きなことの情報の上に About テキストを表示するコンポーザブルで、もう一つは好きなことの情報を表示するコンポーザブルです。

3051387c4b9c7455.png

  1. About のテキストは、スタイルを h3(見出し 3)に、色を onBackground に設定します。好きなことの情報は、スタイルを body1 に設定します。
Column(
   modifier = modifier.padding(
       //..
   )
) {
   Text(
       text = stringResource(R.string.about),
       style = MaterialTheme.typography.h3,
   )
   Text(
       text = stringResource(dogHobby),
       style = MaterialTheme.typography.body1,
   )
}
  1. 完成したコンポーズ可能な関数 DogHobby() は次のようになります。
@Composable
fun DogHobby(@StringRes dogHobby: Int, modifier: Modifier = Modifier) {
   Column(
       modifier = modifier.padding(
           start = 16.dp,
           top = 8.dp,
           bottom = 16.dp,
           end = 16.dp
       )
   ) {
       Text(
           text = stringResource(R.string.about),
           style = MaterialTheme.typography.h3
       )
       Text(
           text = stringResource(dogHobby),
           style = MaterialTheme.typography.body1
       )
   }
}
  1. DogHobby() コンポーザブルを表示するには、DogItem()RowColumn でラップします。Row の後に、2 番目の子として DogHobby() 関数を呼び出し、dog.hobbies をパラメータとして渡します。
Column() {
   Row(
       //..
   ) {
       //..
   }
   DogHobby(dog.hobbies)
}

完全な DogItem() 関数は次のようになります。

@Composable
fun DogItem(dog: Dog, modifier: Modifier = Modifier) {
   var expanded by remember { mutableStateOf(false) }
   Card(
        elevation = 4.dp,
       modifier = modifier.padding(8.dp)
   ) {
       Column() {
           Row(
                Row(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(8.dp)
           ) {
               DogIcon(dog.imageResourceId)
               DogInformation(dog.name, dog.age)
               Spacer(Modifier.weight(1f))
               DogItemButton(
                   expanded = expanded,
                   onClick = { expanded = !expanded },
               )
           }
           DogHobby(dog.hobbies)
       }
   }
}
  1. [Design] ペインでプレビューの [Build & Refresh] をクリックします。犬の好きなことが表示されるようになりました。

9e2e68a4bc4a8ae1.png

5. ボタンのクリックで好きなことの表示 / 非表示を切り替える

アプリの各リストアイテムに展開ボタンが表示されるようになりましたが、ボタンにはまだ何の機能もありません。このセクションでは、ユーザーが展開ボタンをクリックしたときに、好きなことの情報を表示または非表示にするオプションを追加します。

  1. コンポーズ可能な関数 DogItem()DogItemButton() 関数呼び出し内で onClick() ラムダ式を定義し、ボタンがクリックされたときに expanded 状態値(ブール値)を true に変更し、ボタンが再度クリックされると false に戻るようにします。
DogItemButton(
   expanded = expanded,
   onClick = { expanded = !expanded }
)
  1. DogItemButton() 関数で、DogHobby() 関数呼び出しを expanded ブール値の if チェックでラップします。
// No need to copy over
@Composable
fun DogItem(dog: Dog, modifier: Modifier = Modifier) {
   var expanded by remember { mutableStateOf(false) }
   Card(
       //..
   ) {
       Column() {
           Row(
               //..
           ) {
               //..
           }
           if (expanded) {
               DogHobby(dog.hobbies)
           }
       }
   }
}

上のコードでは、expanded の値が true である場合に限り、犬の好きなことに関する情報が表示されます。

  1. プレビューで UI の外観を確認できます。また、UI を操作することもできます。UI プレビューを操作するには、[Design] ペインの右上にあるインタラクティブ モード ボタン 42379dbe94a7a497.png をクリックします。クリックすると、インタラクティブ モードでプレビューが開始されます。

2a4ad1f3d2d0bff7.png

  1. 展開ボタンをクリックして、プレビューを操作します。展開ボタンをクリックすると、犬の好きなことに関する情報の表示 / 非表示が切り替わります。

6ee6774b5b14c7e1.gif

このままでは、リストアイテムを展開しても、展開ボタンのアイコンは変化しません。ユーザー エクスペリエンスを向上させるために、ExpandMore で下向きの矢印 c761ef298c2aea5a.png を表示し、ExpandLess で上向きの矢印 b380f933be0b6ff4.png を表示するようにアイコンを変更します。

  1. DogItemButton() 関数で、expanded の状態を基準とするように imageVector の値を次のように更新します。
import androidx.compose.material.icons.filled.ExpandLess

@Composable
private fun DogItemButton(
   //..
) {
   IconButton(onClick = onClick) {
       Icon(
           imageVector = if (expanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore,
           //..
       )
   }
}
  1. デバイスまたはエミュレータでアプリを実行するか、もう一度プレビューでインタラクティブ モードを使用します。展開ボタンが、ExpandMore のアイコン c761ef298c2aea5a.pngExpandLess のアイコン b380f933be0b6ff4.png を交互に表示するようになりました。

bf8bb280a774a6d4.gif

これで、アイコンの更新が完了しました。

リストアイテムを展開した際に、高さが瞬間的に変わったことに気づいたでしょうか。高さが瞬間的に変わってしまうと、洗練されたアプリには見えません。次はこの問題を解決するために、アプリにアニメーションを追加します。

6. アニメーションを追加する

アニメーションを使用すると、アプリ内で何が起こっているのかを、視覚的な手掛かりを通じてユーザーにわかりやすく伝えることができます。アニメーションは、新しいコンテンツがロードされたときや、新しいアクションが利用可能になったときなど、UI の状態が変化したときに特に役立ちます。また、アニメーションにより、アプリの外観が洗練されたものになります。

このセクションでは、スプリング アニメーションを追加して、リストアイテムの高さの変化をアニメーションにします。

スプリング アニメーション

スプリング アニメーションは、ばねの力で動く物理学ベースのアニメーションです。スプリング アニメーションでは、適用されるばねの力に基づいて動きの値と速度が計算されます。

たとえば、画面上でアプリアイコンをドラッグし、アイコンから指を離すと、目に見えない力によってアイコンが元の位置に勢いよく戻ります。

次のアニメーションは、ばねの効果を示しています。アイコンから指を離すと、アイコンがばねのように再び元の場所に戻ります。

7b52f63dc639c28d.gif

ばねの効果

ばねの力は、次の 2 つの特性に応じて決まります。

  • 減衰率: ばねの弾力性。
  • 剛性のレベル: ばねの固さ。ばねの収縮の速さを示す。

以下は、減衰率と剛性のレベルが異なるアニメーションの例です。

ばねの効果弾力が強い

ばねの効果弾力なし

剛性が高い

剛性がとても低い

それでは、スプリング アニメーションをアプリに追加しましょう。

  1. MainActivity.ktDogItem() で、modifier パラメータを Column レイアウトに追加します。
@Composable
fun DogItem(dog: Dog, modifier: Modifier = Modifier) {
   //..
   Card(
       //..
   ) {
       Column(
          modifier = Modifier
       ){
           //..
       }
   }
}

コンポーズ可能な関数 DogItem() 内の DogHobby() 関数呼び出しを確認します。このコンポジションには、expanded のブール値に基づく、犬の好きなことに関する情報が含まれています。リストアイテムの高さは、好きなことの情報の表示 / 非表示に応じて変化します。animateContentSize 修飾子を使用して、新しい高さと古い高さの間の遷移を組み込みます。

// No need to copy over
@Composable
fun DogItem(...) {

        //..
           if (expanded) {
               DogHobby(dog.hobbies)
           }
}
  1. 修飾子に animateContentSize 修飾子を連結して、サイズ(リストアイテムの高さ)の変化をアニメーションにします。
import androidx.compose.animation.animateContentSize

Column(
           modifier = Modifier
               .animateContentSize()
       ) {
            //..
       }

この実装により、アプリのリストアイテムの高さをアニメーション化することはできるものの、そのアニメーションは実際のアプリで識別するのが困難なほど微妙なものです。この問題を解決するために、オプションの animationSpec パラメータを使用して、アニメーションをカスタマイズします。

  1. animateContentSize() 関数呼び出しに animationSpec パラメータを追加します。DampingRatioMediumBouncy パラメータと StiffnessLow パラメータを使用し、このパラメータをスプリング アニメーションに設定します。
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring

Column(
   modifier = Modifier
       .animateContentSize(
           animationSpec = spring(
               dampingRatio = Spring.DampingRatioMediumBouncy,
               stiffness = Spring.StiffnessLow
           )
       )
)
  1. [Design] ペインでプレビューの [Build & Refresh] をクリックし、インタラクティブ モードを使用するか、エミュレータまたはデバイスでアプリを実行してスプリング アニメーションの動作を確認します。

8cf711b8821b4696.gif

エミュレータまたはデバイスでアプリを再実行して、アニメーション付きの美しいアプリを確認してみましょう。

1e9cf1dbc490924a.gif

7. (省略可)他のアニメーションを試す

animate*AsState

animate*AsState() 関数は、Compose で単一の値をアニメーション化するための最もシンプルなアニメーション API です。終了値(またはターゲット値)を指定するだけで、API は現在の値から指定された終了値までのアニメーションを開始します。

Compose には、FloatColorDpSizeOffsetInt など、多数のデータ型に使える animate*AsState() 関数が用意されています。汎用型を受け入れる animateValueAsState() を使用すると、他のデータ型のサポートを簡単に追加できます。

リストアイテムが展開されるときの色をアニメーション化するには、animateColorAsState() 関数を使用します。

ヒント:

  1. 色を宣言し、その初期化を animateColorAsState() 関数に委任します。
  2. expanded のブール値に応じて、targetValue 名前付きパラメータを設定します。
@Composable
fun DogItem(dog: Dog, modifier: Modifier = Modifier) {
   //..
   val color by animateColorAsState(
       targetValue = if (expanded) Green25 else MaterialTheme.colors.surface,
   )
   Card(
       //..
   ) {...}
}
  1. 上のコード例でバックグラウンド修飾子として宣言した colorColumn に設定します。
@Composable
fun DogItem(dog: Dog, modifier: Modifier = Modifier) {
   //..
   Card(
       //..
   ) {
       Column(
           modifier = Modifier
               .animateContentSize(
                   //..
                   )
               )
               .background(color = color)
       ) {...}
}

8. 解答コードを取得する

この Codelab の完成したコードをダウンロードするには、次の git コマンドを使用します。

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-woof.git

または、リポジトリを ZIP ファイルとしてダウンロードし、Android Studio で開くこともできます。

解答コードを確認する場合は、GitHub で表示します

9. おわりに

これで、犬に関する情報の表示 / 非表示を切り替えるボタンを追加できました。スプリング アニメーションを使ってユーザー エクスペリエンスを向上させました。また、[Design] ペインでインタラクティブ モードを使用する方法も学習しました。

別の種類の Jetpack Compose アニメーションを試すことも可能です。作成したら、#AndroidBasics を付けて、ソーシャル メディアで共有しましょう。

関連リンク