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

1. 始める前に

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

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

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

前提条件

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

学習内容

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

作成するアプリの概要

必要なもの

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

2. アプリの概要

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

36c6cabd93421a92.png

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

c0d0a52463332875.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.展開アイコンを追加する

このセクションでは、アプリに展開アイコン 30c384f00846e69b.png閉じるアイコン f88173321938c003.png を追加します。

def59d71015c0fbe.png

アイコン

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

ノートの上の鉛筆 写真撮影: Angelina Litvin(出典: Unsplash

白黒の鉛筆アイコン

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

マテリアル アイコンのライブラリ

Gradle 依存関係を追加する

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

  1. [Project] ペインで、[Gradle Scripts] > [build.gradle.kts (Module :app)] を開きます。
  2. build.gradle.kts (Module :app) ファイルの最後までスクロールします。dependencies{} ブロックに次の行を追加します。
implementation("androidx.compose.material:material-icons-extended")

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

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

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

}
  1. DogItemButton() 関数内に、onClick という名前付きパラメータを受け入れる IconButton() コンポーザブルを追加します。これは後置ラムダ構文を使用するラムダで、アイコンが押されたときに呼び出される省略可能な modifier です。IconButton's onClickmodifier value parametersDogItemButton に渡されたものと同じ値を設定します。
@Composable
private fun DogItemButton(
   expanded: Boolean,
   onClick: () -> Unit,
   modifier: Modifier = Modifier
){
   IconButton(
       onClick = onClick,
       modifier = modifier
   ) {

   }
}
  1. IconButton() ラムダブロック内に Icon コンポーザブルを追加し、imageVector value-parameterIcons.Filled.ExpandMore に設定します。これが、リストアイテム f88173321938c003.png の最後に表示されます。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
   )
}
  1. パラメータ値 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() コンポーザブルをレイアウトに追加して表示します。

  1. 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) }
  1. リストアイテム内にアイコンボタンを表示します。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*/ }
   )
}
  1. [Design] ペインで WoofPreview() を確認します。

5bbf09cd2828b6.png

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

展開ボタンを配置する

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

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

733f6d9ef2939ab5.png

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

  1. 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*/ }
   )
}
  1. [Design] ペインで WoofPreview() を確認します。展開ボタンがリストアイテムの末端に配置されるようになりました。

8df42b9d85a5dbaa.png

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

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

bba8146c6332cc37.png

  1. 新しいコンポーズ可能な関数を作成します。これは、犬の好きなことの文字列リソース ID とオプションの Modifier を受け取る DogHobby() です。
@Composable
fun DogHobby(
   @StringRes dogHobby: Int,
   modifier: Modifier = Modifier
) {
}
  1. DogHobby() 関数内で Column を作成し、DogHobby() に渡された修飾子を渡します。
@Composable
fun DogHobby(
   @StringRes dogHobby: Int,
   modifier: Modifier = Modifier
){
   Column(
       modifier = modifier
   ) { 

   }
}
  1. Column ブロック内に 2 つの Text コンポーザブルを追加します。一つは好きなことの情報の上に About テキストを表示し、もう一つは好きなことの情報を表示します。

最初の textstrings.xml ファイルの about に設定し、stylelabelSmall として設定します。2 つ目の text は、渡された dogHobby に設定し、stylebodyLarge に設定します。

Column(
   modifier = modifier
) {
   Text(
       text = stringResource(R.string.about),
       style = MaterialTheme.typography.labelSmall
   )
   Text(
       text = stringResource(dogHobby),
       style = MaterialTheme.typography.bodyLarge
   )
}
  1. DogItem() では、DogHobby() コンポーザブルは、DogIcon()DogInformation()Spacer()DogItemButton() を含む Row の下位になります。これを行うには、RowColumn でラップして、好きなことを 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*/ }
       )
   }
}
  1. 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)
               )
           )
       }
   }
}
  1. [Design] ペインで WoofPreview() を確認します。犬の好きなことが表示されるようになりました。

リストアイテムを展開した Woof のプレビュー

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

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

  1. コンポーズ可能な関数 DogItem()DogItemButton() 関数呼び出し内で onClick() ラムダ式を定義し、ボタンがクリックされたときに expanded 状態値(ブール値)を true に変更し、ボタンが再度クリックされると false に戻るようにします。
DogItemButton(
   expanded = expanded,
   onClick = { expanded = !expanded }
)
  1. 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 の場合にのみ、犬の好きなことの情報が表示されるようになりました。

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

74e1624d68fb4131.png

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

Woof のリストアイテムが展開および縮小するアニメーション

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

  1. 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 行ある場合、中かっこは省略可能です。

  1. デバイスまたはエミュレータでアプリを実行するか、もう一度プレビューでインタラクティブ モードを使用します。ExpandMore のアイコン c761ef298c2aea5a.pngExpandLess のアイコン b380f933be0b6ff4.png が交互に表示されます。

de5dc4a953f11e65.gif

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

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

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)
          )
      )
   }
}
  1. MainActivity.ktDogItem() で、modifier パラメータを Column レイアウトに追加します。
@Composable
fun DogItem(
   dog: Dog, 
   modifier: Modifier = Modifier
) {
   ...
   Card(
       ...
   ) {
       Column(
          modifier = Modifier
       ){
           ...
       }
   }
}
  1. 修飾子を animateContentSize 修飾子と連結して、サイズ(リストアイテムの高さ)の変化をアニメーション化します。
import androidx.compose.animation.animateContentSize

Column(
   modifier = Modifier
       .animateContentSize()
)

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

  1. 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
           )
       )
)
  1. [Design] ペインで WoofPreview() を確認し、インタラクティブ モードを使用するか、エミュレータまたはデバイスでアプリを実行してスプリング アニメーションの動作を確認します。

c0d0a52463332875.gif

お疲れさまでした。アニメーション付きの美しいアプリをお楽しみください。

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

animate*AsState

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

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

リストアイテムの展開時に色を変更するには、animateColorAsState() 関数を試してみてください。

  1. 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()
   ...
}
  1. 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,
   )
   ...
}
  1. color をバックグラウンド修飾子として Column に設定します。
@Composable
fun DogItem(
   dog: Dog, 
   modifier: Modifier = Modifier
) {
   ...
   Card(
       ...
   ) {
       Column(
           modifier = Modifier
               .animateContentSize(
                   ...
                   )
               )
               .background(color = color)
       ) {...}
}
  1. リストアイテムが展開されているときの色の変化を確認します。閉じられているリストアイテムは primaryContainer 色、展開されているリストアイテムは tertiaryContainer 色です。

animateAsState アニメーション

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 を付けて、ソーシャル メディアで共有しましょう。

関連リンク