概要
いいねボタンやブックマークボタン等、さまざまなアプリでアイコンボタンが利用されていると思います。その中でも有名なアプリの中で利用されているアイコンボタンには押下時のアニメーションが実装されており、よりリッチな表現を実現しています。
今回はJetpack Composeを用いてそのようなアニメーション付きのアイコンボタンを作成します。例としてInstagramのいいねボタンを再現してみます。
Instagramのいいねボタン
今回作ったいいねボタン
まあまあ本物に寄せられたかな、、?
前提条件
未いいね状態アイコンといいね済み状態アイコンにはそれぞれ、Vector Assetのマテリアルアイコンを利用しました。追加手順等はこちら
いいねボタンComposableの全体コード
@Composable
fun FavoriteIcon(
modifier: Modifier = Modifier,
) {
// いいね状態
var isFavorite by rememberSaveable {
mutableStateOf(false)
}
// 未いいね状態アイコンのアニメーション定義
val notFavoriteAnimationScale = animateFloatAsState(
// いいね状態によって表示/非表示を切り替える
targetValue = if (!isFavorite) 1.0F else 0F,
animationSpec = if (!isFavorite) {
keyframes {
durationMillis = 400
0.0f at 0 // 0msの時に0倍の大きさで表示
1.2f at 250 // 0ms〜250msにかけて0倍 → 1.2倍へニュルっと拡大
1.0f at 400 // 250ms〜400msにかけて1.2倍 → 1.0倍へニュルっと縮小
}
} else {
snap()
},
label = "",
)
// いいね済み状態アイコンのアニメーション定義
val favoriteAnimationScale = animateFloatAsState(
// いいね状態によって表示/非表示を切り替える
targetValue = if (isFavorite) 1.0F else 0F,
animationSpec = if (isFavorite) {
keyframes {
durationMillis = 400
0.0f at 0 // 0msの時に0倍の大きさで表示
1.2f at 250 // 0ms〜250msにかけて0倍 → 1.2倍へニュルっと拡大
1.0f at 400 // 250ms〜400msにかけて1.2倍 → 1.0倍へニュルっと縮小
}
} else {
snap()
},
label = "",
)
val iconSize = 300.dp
Box(
modifier = modifier
.size(iconSize)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
) {
// 押下時のいいね状態の変更処理
isFavorite = !isFavorite
},
) {
// 未いいね状態アイコン、いいね済み状態アイコンを重ねて配置しておく
// 未いいね状態アイコン
Icon(
modifier = Modifier
.size(iconSize)
// ここでアニメーションを適用している
.scale(notFavoriteAnimationScale.value),
painter = painterResource(id = R.drawable.baseline_favorite_border_24),
contentDescription = null,
tint = Color(0xFF000000)
)
// いいね済み状態アイコン
Icon(
modifier = Modifier
.size(iconSize)
// ここでアニメーションを適用している
.scale(favoriteAnimationScale.value),
painter = painterResource(id = R.drawable.baseline_favorite_24),
contentDescription = null,
tint = Color(0xFFFC3A3A),
)
}
}
コードのポイント
アニメーションの定義
// 未いいね状態アイコンのアニメーション定義
val notFavoriteAnimationScale = animateFloatAsState(
// いいね状態によって表示/非表示を切り替える
targetValue = if (!isFavorite) 1.0F else 0F,
animationSpec = if (!isFavorite) {
keyframes {
durationMillis = 400
0.0f at 0 // 0msの時に0倍の大きさで表示
1.2f at 250 // 0ms〜250msにかけて0倍 → 1.2倍へニュルっと拡大
1.0f at 400 // 250ms〜400msにかけて1.2倍 → 1.0倍へニュルっと縮小
}
} else {
snap()
},
label = "",
)
// いいね済み状態アイコンのアニメーション定義
val favoriteAnimationScale = animateFloatAsState(
// いいね状態によって表示/非表示を切り替える
targetValue = if (isFavorite) 1.0F else 0F,
animationSpec = if (isFavorite) {
keyframes {
durationMillis = 400
0.0f at 0 // 0msの時に0倍の大きさで表示
1.2f at 250 // 0ms〜250msにかけて0倍 → 1.2倍へニュルっと拡大
1.0f at 400 // 250ms〜400msにかけて1.2倍 → 1.0倍へニュルっと縮小
}
} else {
snap()
},
label = "",
)
notFavoriteAnimationScale
では未いいね状態アイコンのアニメーションを、favoriteAnimationScale
ではいいね済み状態アイコンのアニメーションをそれぞれ定義しています。これらのアニメーション定義を、アニメーション追加対象のComposableにてModifier.scale()を用いて付与することでアニメーションの実現を行います。
アニメーションの実現にはanimateFloatAsStateを利用しています。その中のパラメータであるtargetValue
、animationSpec
にて具体的な振る舞いを定義することができます。
-
targetValue
アニメーション終了時の大きさを指定しています。 -
animationSpec
時間の経過とともに値が変化するアニメーションを指定できます。
ここではKeyframesを利用して、経過時間ごとのアイコンの状態を指定しています。durationMillis
でアニメーションの全体時間を指定し、その下に何ミリ秒時点で何倍のサイズのアイコンを表示するかを列挙しています(1.2f at 250
等)。
またsnap()は各アイコンを非表示にするために利用しています。snap()はアニメーション終了時の状態へすぐに切り替えるためのメソッドです。
同じBox内に変化前後のアイコンを重ねて配置する
Box(
modifier = modifier
.size(iconSize)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
) {
// 押下時のいいね状態の変更処理
isFavorite = !isFavorite
},
) {
// 未いいね状態アイコン、いいね済み状態アイコンを重ねて配置しておく
// 未いいね状態アイコン
Icon(
modifier = Modifier
.size(iconSize)
// ここでアニメーションを適用している
.scale(notFavoriteAnimationScale.value),
painter = painterResource(id = drawable.baseline_favorite_border_24),
contentDescription = null,
tint = Color(0xFF000000)
)
// いいね済み状態アイコン
Icon(
modifier = Modifier
.size(iconSize)
// ここでアニメーションを適用している
.scale(favoriteAnimationScale.value),
painter = painterResource(id = drawable.baseline_favorite_24),
contentDescription = null,
tint = Color(0xFFFC3A3A),
)
}
Boxの中に未いいね状態アイコンといいね済み状態アイコンを重ねて定義しておきます。
アニメーション定義の部分で、isFavorite
(いいね状態)を元にアイコンの表示/非表示をハンドリングしているため、必ずどちらかのアイコンが表示、どちらかが非表示の状態になっています。
参考文献