6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ZOZOAdvent Calendar 2023

Day 23

[Jetpack Compose] アニメーション付いいねボタンを作る

Last updated at Posted at 2023-12-23

概要

いいねボタンやブックマークボタン等、さまざまなアプリでアイコンボタンが利用されていると思います。その中でも有名なアプリの中で利用されているアイコンボタンには押下時のアニメーションが実装されており、よりリッチな表現を実現しています。

今回はJetpack Composeを用いてそのようなアニメーション付きのアイコンボタンを作成します。例としてInstagramのいいねボタンを再現してみます。

Instagramのいいねボタン

                                           

今回作ったいいねボタン

まあまあ本物に寄せられたかな、、?

前提条件

未いいね状態アイコンといいね済み状態アイコンにはそれぞれ、Vector Assetのマテリアルアイコンを利用しました。追加手順等はこちら

未いいね状態アイコン
image.png

いいね済み状態アイコン
image.png

いいねボタン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を利用しています。その中のパラメータであるtargetValueanimationSpecにて具体的な振る舞いを定義することができます。

  • 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(いいね状態)を元にアイコンの表示/非表示をハンドリングしているため、必ずどちらかのアイコンが表示、どちらかが非表示の状態になっています。

参考文献

6
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?