1. 始める前に
この Codelab では、Android アプリにシンプルなアニメーションを追加する方法について説明します。アニメーションを使用することで、アプリはよりインタラクティブかつ魅力的で、ユーザーが簡単に解釈できるものになります。画面上いっぱいに情報が表示されていても、変更箇所のそれぞれにアニメーションを適用すれば、ユーザーは何が変更されたかを容易に把握できます。
アプリのユーザー インターフェースで使用できるアニメーションにはさまざまなものがあります。アイテムの表示と非表示をフェードイン / フェードアウトで表現したり、アイテムを画面上で(時には画面外に出ていくように)動かしたり、注意を引く仕方で変形させたりできます。これにより、アプリの UI が表現力豊かなものになり、使いやすくなります。
また、アニメーションでアプリの外観を洗練させることができます。これはエレガントな印象を与えるだけでなく、ユーザーにもメリットがあります。
前提条件
- 関数、ラムダ、ステートレス コンポーザブルなど、Kotlin に関する知識。
- Jetpack Compose でレイアウトを作成する方法に関する基本的な知識。
- Jetpack Compose でリストを作成する方法に関する基本的な知識。
- マテリアル デザインに関する基本的な知識。
学習内容
- Jetpack Compose で簡単なスプリング アニメーションを作成する方法。
作成するアプリの概要
- Jetpack Compose でのマテリアル テーマ設定の Codelab で作成した Woof アプリに対し、ユーザーの操作に応答するシンプルなアニメーションを追加します。
必要なもの
- Android Studio の最新の安定版
- スターター コードをダウンロードするためのインターネット接続。
2. アプリの概要
Jetpack Compose を使用したマテリアル テーマ設定の Codelab では、マテリアル デザインを使用して、犬とその情報のリストを表示する Woof アプリを作成しました。

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

スターター コードを取得する
まず、スターター コードをダウンロードします。
または、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. 展開アイコンを追加する
このセクションでは、アプリに展開アイコン  と閉じるアイコン
 と閉じるアイコン  を追加します。
 を追加します。

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

Gradle 依存関係を追加する
プロジェクトに material-icons-extended ライブラリ依存関係を追加します。このライブラリの Icons.Filled.ExpandLess アイコン  と
 と Icons.Filled.ExpandMore アイコン  を使用します。
 を使用します。
- [Project] ペインで、[Gradle Scripts] > [build.gradle.kts (Module :app)] を開きます。
- build.gradle.kts (Module :app)ファイルの最後までスクロールします。- dependencies{}ブロックに次の行を追加します。
implementation("androidx.compose.material:material-icons-extended")
アイコン コンポーザブルを追加する
マテリアル アイコン ライブラリの展開アイコンを表示する関数を追加して、これをボタンとして使用します。
- MainActivity.ktで、- DogItem()関数の後に、- DogItemButton()という新しいコンポーズ可能な関数を作成します。
- 展開された状態を示す Boolean、ボタンの onClick ハンドラのラムダ式、オプションのModifierを次のように渡します。
@Composable
private fun DogItemButton(
   expanded: Boolean,
   onClick: () -> Unit,
   modifier: Modifier = Modifier
) {
 
}
- DogItemButton()関数内に、- onClickという名前付きパラメータを受け入れる- IconButton()コンポーザブルを追加します。これは後置ラムダ構文を使用するラムダで、アイコンが押されたときに呼び出される省略可能な- modifierです。- IconButton's onClickと- modifier value parametersに- DogItemButtonに渡されたものと同じ値を設定します。
@Composable
private fun DogItemButton(
   expanded: Boolean,
   onClick: () -> Unit,
   modifier: Modifier = Modifier
){
   IconButton(
       onClick = onClick,
       modifier = modifier
   ) {
   }
}
- IconButton()ラムダブロック内に- Iconコンポーザブルを追加し、- imageVector value-parameterを- Icons.Filled.ExpandMoreに設定します。これが、リストアイテム の最後に表示されます。Android Studio に の最後に表示されます。Android Studio に- Icon()コンポーザブルのパラメータに関する警告が表示されますが、これは次のステップで修正します。
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material.icons.Icons
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
IconButton(
   onClick = onClick,
   modifier = modifier
) {
   Icon(
       imageVector = Icons.Filled.ExpandMore
   )
}
- パラメータ値 tintを追加し、アイコンの色をMaterialTheme.colorScheme.secondaryに設定します。名前付きパラメータcontentDescriptionを追加し、文字列リソースR.string.expand_button_content_descriptionに設定します。
IconButton(
   onClick = onClick,
   modifier = modifier
){
   Icon(
       imageVector = Icons.Filled.ExpandMore,
       contentDescription = stringResource(R.string.expand_button_content_description),
       tint = MaterialTheme.colorScheme.secondary
   )
}
アイコンを表示する
DogItemButton() コンポーザブルをレイアウトに追加して表示します。
- DogItem()の先頭に- varを追加して、リストアイテムの展開状態を保存します。初期値を- falseに設定します。
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
var expanded by remember { mutableStateOf(false) }
- リストアイテム内にアイコンボタンを表示します。DogItem()コンポーザブルのRowブロックの最後で、DogInformation()を呼び出した後にDogItemButton()を追加します。コールバック用にexpanded状態と空のラムダを渡します。onClickアクションは後のステップで定義します。
Row(
   modifier = Modifier
       .fillMaxWidth()
       .padding(dimensionResource(R.dimen.padding_small))
) {
   DogIcon(dog.imageResourceId)
   DogInformation(dog.name, dog.age)
   DogItemButton(
       expanded = expanded,
       onClick = { /*TODO*/ }
   )
}
- [Design] ペインで WoofPreview()を確認します。

展開ボタンがリストアイテムの末端に配置されていません。これは次のステップで修正します。
展開ボタンを配置する
展開ボタンをリストアイテムの末端に配置するには、Modifier.weight() 属性を使用してスペーサーをレイアウトに追加する必要があります。
Woof アプリでは、各リストアイテムの行に、犬の写真、犬の情報、展開ボタンが表示されます。展開ボタンが適切な位置に配置されるよう、そのボタンアイコンの前に、重みが 1f の Spacer コンポーザブルを追加します。このスペーサーが行内で唯一重みを持つ子要素であるため、重みを持たない他の子要素の幅が決定された後、行内の残りのスペースを埋めることになります。

スペーサーをリストアイテムの行に追加する
- DogItem()の- DogInformation()から- DogItemButton()の間に- Spacerを追加します。- weight(1f)で- Modifierを渡します。- Modifier.weight()により、行の残りのスペースがスペーサーで埋められます。
import androidx.compose.foundation.layout.Spacer
Row(
   modifier = Modifier
       .fillMaxWidth()
       .padding(dimensionResource(R.dimen.padding_small))
) {
   DogIcon(dog.imageResourceId)
   DogInformation(dog.name, dog.age)
   Spacer(modifier = Modifier.weight(1f))
   DogItemButton(
       expanded = expanded,
       onClick = { /*TODO*/ }
   )
}
- [Design] ペインで WoofPreview()を確認します。展開ボタンがリストアイテムの末端に配置されるようになりました。

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

- 新しいコンポーズ可能な関数を作成します。これは、犬の好きなことの文字列リソース ID とオプションの Modifierを受け取るDogHobby()です。
@Composable
fun DogHobby(
   @StringRes dogHobby: Int,
   modifier: Modifier = Modifier
) {
}
- DogHobby()関数内で- Columnを作成し、- DogHobby()に渡された修飾子を渡します。
@Composable
fun DogHobby(
   @StringRes dogHobby: Int,
   modifier: Modifier = Modifier
){
   Column(
       modifier = modifier
   ) { 
   }
}
- Columnブロック内に 2 つの- Textコンポーザブルを追加します。一つは好きなことの情報の上に About テキストを表示し、もう一つは好きなことの情報を表示します。
最初の text を strings.xml ファイルの about に設定し、style を labelSmall として設定します。2 つ目の text は、渡された dogHobby に設定し、style を bodyLarge に設定します。
Column(
   modifier = modifier
) {
   Text(
       text = stringResource(R.string.about),
       style = MaterialTheme.typography.labelSmall
   )
   Text(
       text = stringResource(dogHobby),
       style = MaterialTheme.typography.bodyLarge
   )
}
- DogItem()では、- DogHobby()コンポーザブルは、- DogIcon()、- DogInformation()、- Spacer()、- DogItemButton()を含む- Rowの下位になります。これを行うには、- Rowを- Columnでラップして、好きなことを- Rowの下に追加できるようにします。
Column() {
   Row(
       modifier = Modifier
           .fillMaxWidth()
           .padding(dimensionResource(R.dimen.padding_small))
   ) {
       DogIcon(dog.imageResourceId)
       DogInformation(dog.name, dog.age)
       Spacer(modifier = Modifier.weight(1f))
       DogItemButton(
           expanded = expanded,
           onClick = { /*TODO*/ }
       )
   }
}
- Rowの後に、- Columnの 2 番目の子として- DogHobby()を追加します。- dog.hobbiesを渡します。これには、渡された犬に固有の好きなことと、- DogHobby()コンポーザブルのパディングが設定された- modifierが含まれます。
Column() {
   Row() {
      ...
   }
   DogHobby(
       dog.hobbies,
       modifier = Modifier.padding(
           start = dimensionResource(R.dimen.padding_medium),
           top = dimensionResource(R.dimen.padding_small),
           end = dimensionResource(R.dimen.padding_medium),
           bottom = dimensionResource(R.dimen.padding_medium)
       )
   )
}
完全な DogItem() 関数は次のようになるはずです。
@Composable
fun DogItem(
   dog: Dog,
   modifier: Modifier = Modifier
) {
   var expanded by remember { mutableStateOf(false) }
   Card(
       modifier = modifier
   ) {
       Column() {
           Row(
               modifier = Modifier
                   .fillMaxWidth()
                   .padding(dimensionResource(R.dimen.padding_small))
           ) {
               DogIcon(dog.imageResourceId)
               DogInformation(dog.name, dog.age)
               Spacer(Modifier.weight(1f))
               DogItemButton(
                   expanded = expanded,
                   onClick = { /*TODO*/ },
               )
           }
           DogHobby(
               dog.hobbies, 
               modifier = Modifier.padding(
                   start = dimensionResource(R.dimen.padding_medium),
                   top = dimensionResource(R.dimen.padding_small),
                   end = dimensionResource(R.dimen.padding_medium),
                   bottom = dimensionResource(R.dimen.padding_medium)
               )
           )
       }
   }
}
- [Design] ペインで WoofPreview()を確認します。犬の好きなことが表示されるようになりました。

5. ボタンのクリックで好きなことの表示 / 非表示を切り替える
アプリの各リストアイテムに展開ボタンが表示されるようになりましたが、ボタンにはまだ何の機能もありません。このセクションでは、ユーザーが展開ボタンをクリックしたときに、好きなことの情報を表示または非表示にするオプションを追加します。
- コンポーズ可能な関数 DogItem()のDogItemButton()関数呼び出し内でonClick()ラムダ式を定義し、ボタンがクリックされたときにexpanded状態値(ブール値)をtrueに変更し、ボタンが再度クリックされるとfalseに戻るようにします。
DogItemButton(
   expanded = expanded,
   onClick = { expanded = !expanded }
)
- DogItem()関数で、- DogHobby()関数呼び出しを- expandedブール値の- ifチェックでラップします。
@Composable
fun DogItem(
   dog: Dog,
   modifier: Modifier = Modifier
) {
   var expanded by remember { mutableStateOf(false) }
   Card(
       ...
   ) {
       Column(
           ...
       ) {
           Row(
               ...
           ) {
               ...
           }
           if (expanded) {
               DogHobby(
                   dog.hobbies, modifier = Modifier.padding(
                       start = dimensionResource(R.dimen.padding_medium),
                       top = dimensionResource(R.dimen.padding_small),
                       end = dimensionResource(R.dimen.padding_medium),
                       bottom = dimensionResource(R.dimen.padding_medium)
                   )
               )
           }
       }
   }
}
これで、expanded の値が true の場合にのみ、犬の好きなことの情報が表示されるようになりました。
- プレビューで UI の外観を確認できます。また、UI を操作することもできます。UI プレビューを操作するには、[Design] ペインで WoofPreview テキストにカーソルを合わせ、[Design] ペインの右上隅にあるインタラクティブ モードのボタン  をクリックします。クリックすると、インタラクティブ モードでプレビューが開始されます。 をクリックします。クリックすると、インタラクティブ モードでプレビューが開始されます。

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

このままでは、リストアイテムを展開しても、展開ボタンのアイコンは変化しません。ユーザー エクスペリエンスを向上させるために、ExpandMore で下向きの矢印  を表示し、
 を表示し、ExpandLess で上向きの矢印  を表示するようにアイコンを変更します。
 を表示するようにアイコンを変更します。
- DogItemButton()関数に、- expanded状態に基づいて- imageVector値を更新する- ifステートメントを次のように追加します。
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,
           ...
       )
   }
}
前のコード スニペットで if-else をどのように記述しているか確認してください。
if (expanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore
これは、次のコードで中かっこ { } を使用する場合と同じです。
if (expanded) {
`Icons.Filled.ExpandLess`
} else {
`Icons.Filled.ExpandMore`
}
if-else ステートメントにコードが 1 行ある場合、中かっこは省略可能です。
- デバイスまたはエミュレータでアプリを実行するか、もう一度プレビューでインタラクティブ モードを使用します。ExpandMoreのアイコン と とExpandLessのアイコン が交互に表示されます。 が交互に表示されます。

これで、アイコンの更新が完了しました。
リストアイテムを展開した際に、高さが瞬間的に変わったことに気づいたでしょうか。高さが瞬間的に変わってしまうと、洗練されたアプリには見えません。次はこの問題を解決するために、アプリにアニメーションを追加します。
6. アニメーションを追加する
アニメーションを使用すると、アプリ内で何が起こっているのかを、視覚的な手掛かりを通じてユーザーにわかりやすく伝えることができます。アニメーションは、新しいコンテンツがロードされたときや、新しいアクションが利用可能になったときなど、UI の状態が変化したときに特に役立ちます。また、アニメーションにより、アプリの外観が洗練されたものになります。
このセクションでは、スプリング アニメーションを追加して、リストアイテムの高さの変化をアニメーションにします。
スプリング アニメーション
スプリング アニメーションは、ばねの力で動く物理学ベースのアニメーションです。スプリング アニメーションでは、適用されるばねの力に基づいて動きの値と速度が計算されます。
たとえば、画面上でアプリアイコンをドラッグし、アイコンから指を離すと、目に見えない力によってアイコンが元の位置に勢いよく戻ります。
次のアニメーションは、ばねの効果を示しています。アイコンから指を離すと、アイコンがばねのように再び元の場所に戻ります。

ばねの効果
ばねの力は、次の 2 つの特性に応じて決まります。
- 減衰率: ばねの弾力性。
- 剛性のレベル: ばねの固さ。ばねの収縮の速さを示す。
以下は、減衰率と剛性のレベルが異なるアニメーションの例です。
| 
 | 
 | 
| 
 | 
 | 
コンポーズ可能な関数 DogItem() の DogHobby() 関数呼び出しを見てみましょう。このコンポジションには、expanded のブール値に基づく、犬の好きなことに関する情報が含まれています。リストアイテムの高さは、好きなことの情報の表示 / 非表示に応じて変化します。現時点では、表示 / 非表示の遷移がスムーズではありません。このセクションでは、animateContentSize 修飾子を使用して、展開された状態と閉じられた状態とでよりスムーズな遷移が行われるようにします。
// No need to copy over
@Composable
fun DogItem(...) {
  ...
    if (expanded) {
       DogHobby(
          dog.hobbies, 
          modifier = Modifier.padding(
              start = dimensionResource(R.dimen.padding_medium),
              top = dimensionResource(R.dimen.padding_small),
              end = dimensionResource(R.dimen.padding_medium),
              bottom = dimensionResource(R.dimen.padding_medium)
          )
      )
   }
}
- MainActivity.ktの- DogItem()で、- modifierパラメータを- Columnレイアウトに追加します。
@Composable
fun DogItem(
   dog: Dog, 
   modifier: Modifier = Modifier
) {
   ...
   Card(
       ...
   ) {
       Column(
          modifier = Modifier
       ){
           ...
       }
   }
}
- 修飾子を animateContentSize修飾子と連結して、サイズ(リストアイテムの高さ)の変化をアニメーション化します。
import androidx.compose.animation.animateContentSize
Column(
   modifier = Modifier
       .animateContentSize()
)
この実装により、アプリのリストアイテムの高さをアニメーション化することはできるものの、そのアニメーションは非常に微細でアプリの実行時に識別するのは困難です。この問題を解決するために、オプションの animationSpec パラメータを使用して、アニメーションをカスタマイズします。
- Woof では、弾む動作なしでゆっくりと動くアニメーションにします。そうするために、animateContentSize()関数呼び出しにanimationSpecパラメータを追加します。それをDampingRatioNoBouncyでスプリング アニメーションに設定して弾みをなくし、StiffnessMediumパラメータでスプリングを少し強固にします。
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
Column(
   modifier = Modifier
       .animateContentSize(
           animationSpec = spring(
               dampingRatio = Spring.DampingRatioNoBouncy,
               stiffness = Spring.StiffnessMedium
           )
       )
)
- [Design] ペインで WoofPreview()を確認し、インタラクティブ モードを使用するか、エミュレータまたはデバイスでアプリを実行してスプリング アニメーションの動作を確認します。

お疲れさまでした。アニメーション付きの美しいアプリをお楽しみください。
7. (省略可)他のアニメーションを試す
animate*AsState
animate*AsState() 関数は、Compose で単一の値をアニメーション化するための最もシンプルなアニメーション API です。終了値(またはターゲット値)を指定するだけで、API は現在の値から指定された終了値までのアニメーションを開始します。
Compose には、Float、Color、Dp、Size、Offset、Int など、多数のデータ型に使える animate*AsState() 関数が用意されています。汎用型を受け入れる animateValueAsState() を使用すると、他のデータ型のサポートを簡単に追加できます。
リストアイテムの展開時に色を変更するには、animateColorAsState() 関数を試してみてください。
- DogItem()で色を宣言し、その初期化を- animateColorAsState()関数に委任します。
import androidx.compose.animation.animateColorAsState
@Composable
fun DogItem(
   dog: Dog,
   modifier: Modifier = Modifier
) {
   var expanded by remember { mutableStateOf(false) }
   val color by animateColorAsState()
   ...
}
- expandedのブール値に応じて、- targetValue名前付きパラメータを設定します。リストアイテムが展開されている場合は、リストアイテムを- tertiaryContainer色に設定します。展開されていない場合は、- primaryContainer色に設定します。
import androidx.compose.animation.animateColorAsState
@Composable
fun DogItem(
   dog: Dog,
   modifier: Modifier = Modifier
) {
   var expanded by remember { mutableStateOf(false) }
   val color by animateColorAsState(
       targetValue = if (expanded) MaterialTheme.colorScheme.tertiaryContainer
       else MaterialTheme.colorScheme.primaryContainer,
   )
   ...
}
- colorをバックグラウンド修飾子として- Columnに設定します。
@Composable
fun DogItem(
   dog: Dog, 
   modifier: Modifier = Modifier
) {
   ...
   Card(
       ...
   ) {
       Column(
           modifier = Modifier
               .animateContentSize(
                   ...
                   )
               )
               .background(color = color)
       ) {...}
}
- リストアイテムが展開されているときの色の変化を確認します。閉じられているリストアイテムは primaryContainer色、展開されているリストアイテムはtertiaryContainer色です。

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 を付けて、ソーシャル メディアで共有しましょう。
 
   写真撮影:
 写真撮影: 
 弾力が強い
弾力が強い 弾力なし
弾力なし 剛性が高い
剛性が高い 剛性がとても低い
剛性がとても低い