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.展開アイコンを追加する
このセクションでは、アプリに展開アイコン と閉じるアイコン を追加します。
アイコン
アイコンとは、目的の機能を視覚的に伝達することで、ユーザーがユーザー インターフェースを理解することを助けるシンボルです。多くの場合、アイコンはユーザーがよく知っていると思われる現実の物体から着想を得ています。ほとんどのアイコンのデザインは、ユーザーが即座に理解できる最小限のレベルまで簡略化されています。たとえば、現実の鉛筆は字を書くために使用されるので、通常、鉛筆のアイコンは作成や編集を表します。
写真撮影: Angelina Litvin(出典: Unsplash) |
マテリアル デザインでは、多くのニーズに対応した多数のアイコンが一般的なカテゴリ別に用意されています。
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 に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 を付けて、ソーシャル メディアで共有しましょう。