使用Compose创造有趣的动画:使用Compose共享元素

背景

前几天有一个朋友在评论区给我发送一个效果让我实现,在这里给大家展示一下,是一个非常经典的共享元素的例子。

我实现的效果和原本的有一些出入,但是整体一致, 原文地址:www.kbo.sk/components/...

注意,共享元素应该是在compose 1.7之后才能使用,所以本文使用的依赖是:compose.version=1.7.0-rc01,不需要其他依赖库。

本文使用的是Compose for Desktop,代码可能会有一些出入,但是整体问题不大,可以把 painterResource替换成需要Res的。

正文

本文虽然实现了基本效果,但是还是有一些瑕疵:

1.点击图片切换时,会有一瞬间发白,没有原版看着舒服,这个我还不清楚是什么原因。

2.动画快要结束的时候,会有轻微抖动,但是如果你仔细看原版,其实它的效果也有这个问题。

效果图

主要功能

  1. 页面状态切换 :通过 pageNo 来管理当前页面的状态(Page1, Page2, Page3),并使用 AnimatedContent 来进行状态变化时的过渡动画。
  2. 共享元素过渡 :使用了 SharedTransitionScopesharedBounds 以及 sharedElement 来实现不同页面之间的共享元素动画。比如封面图片、歌曲标题等元素在页面切换时保持一致并执行动画过渡。
  3. 动画效果 :代码中使用了 scaleInscaleOut 动画来实现页面切换时的缩放效果,并且在 AnimatedVisibility 中对播放按钮进行了动画控制,使用图标的动态缩放来模拟播放与暂停状态的切换。
  4. 页面布局 :三个页面(Page1, Page2, Page3)在视觉上逐步展开或缩小,并且调整了各组件(如图片和文本)的大小和排列方式。布局通过 RowColumn 来实现,按钮、文本、图片等组件根据不同页面的需求动态布局。

实现细节与难点

  • SharedTransitionApi 的使用 :这段代码运用了 ExperimentalSharedTransitionApi,让组件在不同页面之间切换时能够以动画形式保持一致性。比如封面图片、歌曲名称和控制按钮在不同页面中是共享的,通过 sharedElement 保证这些元素在页面之间保持连贯的过渡体验。

    • sharedBoundssharedElement :这些 API 用于定义跨页面的共享元素和它们的动画。sharedBounds 绑定了组件的边界,使其在两个状态之间平滑缩放,而 sharedElement 则定义了具体共享的内容(例如同一个 Image 在两个页面间的缩放变化)。
  • AnimatedContent 过渡动画AnimatedContent 是用于处理页面内容变化时的过渡动画。在这段代码中,AnimatedContent 包裹了整个内容区域,并通过 scaleInscaleOut 实现页面切换时的缩放动画。每次点击图片切换页面时,pageNo 会更新并触发内容动画切换。

  • 组件的动画效果 :代码中使用了 AnimatedVisibility 来处理播放控制按钮的显示和隐藏。例如,播放和暂停按钮会根据状态使用 scaleInscaleOut 动画实现缩放效果,提供一种更自然的交互体验。

  • 状态管理和布局调整 :每个页面的布局略有不同。例如,Page1 中图片较小,Page2 中图片增大并增加了进度条等控件,而 Page3 则进一步简化布局。这些布局切换通过 Jetpack Compose 的 RowColumnModifier 来管理,并动态调整各组件的大小和位置。

参考

Compose共享元素入门

完整代码

kotlin 复制代码
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun MusicPlayer() {
    var pageNo by remember { mutableIntStateOf(0) }
    SharedTransitionLayout {
        AnimatedContent(
            targetState = pageNo,
            modifier = Modifier
                .animateContentSize(),
            transitionSpec = {
                scaleIn() togetherWith scaleOut()
            },
            contentAlignment = Alignment.Center
        ) {
            when (pageNo) {
                0 -> {
                    Page1(
                        animatedVisibilityScope = this@AnimatedContent
                    ) {
                        pageNo = 1
                    }
                }

                1 -> {
                    Page2(
                        animatedVisibilityScope = this@AnimatedContent
                    ) {
                        pageNo = 2
                    }
                }

                2 -> {
                    Page3(
                        animatedVisibilityScope = this@AnimatedContent
                    ) {
                        pageNo = 0
                    }
                }

                else -> throw IndexOutOfBoundsException()
            }
        }
    }
}

@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun SharedTransitionScope.Page1(
    animatedVisibilityScope: AnimatedVisibilityScope,
    pageChanged: () -> Unit
) {
    Row(
        modifier = Modifier
            .sharedBounds(
                sharedContentState = rememberSharedContentState("bound"),
                animatedVisibilityScope = animatedVisibilityScope,
                resizeMode = RemeasureToBounds
            )
            .width(284.dp)
            .background(Color.Black, shape = RoundedCornerShape(16.dp))
            .padding(8.dp),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.SpaceBetween,
    ) {
        Image(
            painter = painterResource("Rodeo.jpg"),
            contentDescription = null,
            contentScale = ContentScale.Crop,
            modifier = Modifier
                .sharedBounds(
                    sharedContentState = rememberSharedContentState("image"),
                    animatedVisibilityScope = animatedVisibilityScope,
                    resizeMode = RemeasureToBounds
                )
                .width(52.dp)
                .aspectRatio(1f)
                .clip(shape = RoundedCornerShape(8.dp))
                .clickable {
                    pageChanged()
                },
        )
        Column(
            modifier = Modifier,
            verticalArrangement = Arrangement.SpaceEvenly,
            horizontalAlignment = Alignment.CenterHorizontally,
        ) {
            Text(
                text = "I Can Tell",
                color = Color.White,
                modifier = Modifier
                    .sharedElement(
                        state = rememberSharedContentState(key = "title1"),
                        animatedVisibilityScope = animatedVisibilityScope
                    )
            )
            Text(
                text = "Travis Scott",
                color = Color.LightGray,
                modifier = Modifier
                    .sharedElement(
                        state = rememberSharedContentState(key = "title2"),
                        animatedVisibilityScope = animatedVisibilityScope
                    )
            )
        }
        PlayController(animatedVisibilityScope)
    }
}

@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun SharedTransitionScope.Page2(
    animatedVisibilityScope: AnimatedVisibilityScope,
    pageChanged: () -> Unit
) {
    Row(
        modifier = Modifier
            .sharedBounds(
                sharedContentState = rememberSharedContentState("bound"),
                animatedVisibilityScope = animatedVisibilityScope,
                resizeMode = RemeasureToBounds
            )
            .width(348.dp)
            .background(Color.Black, shape = RoundedCornerShape(16.dp))
            .padding(8.dp),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.SpaceBetween,
    ) {
        Image(
            painter = painterResource("Rodeo.jpg"),
            contentDescription = null,
            contentScale = ContentScale.Crop,
            modifier = Modifier
                .sharedBounds(
                    sharedContentState = rememberSharedContentState("image"),
                    animatedVisibilityScope = animatedVisibilityScope,
                    resizeMode = RemeasureToBounds
                )
                .width(148.dp)
                .aspectRatio(1f)
                .clip(shape = RoundedCornerShape(8.dp))
                .clickable {
                    pageChanged()
                },
        )
        Column(
            modifier = Modifier
                .fillMaxWidth(),//.padding(start = 8.dp, end = 8.dp)
            verticalArrangement = Arrangement.SpaceBetween,
            horizontalAlignment = Alignment.CenterHorizontally,
        ) {
            Text(
                text = "I Can Tell",
                color = Color.White,
                modifier = Modifier
                    .sharedElement(
                        state = rememberSharedContentState(key = "title1"),
                        animatedVisibilityScope = animatedVisibilityScope
                    )
            )
            Text(
                text = "Travis Scott",
                color = Color.LightGray,
                modifier = Modifier
                    .sharedElement(
                        state = rememberSharedContentState(key = "title2"),
                        animatedVisibilityScope = animatedVisibilityScope
                    )
            )
            Spacer(
                modifier = Modifier.height(12.dp)
            )
            Row(
                modifier = Modifier
                    .fillMaxWidth(),
                verticalAlignment = Alignment.CenterVertically,
                horizontalArrangement = Arrangement.SpaceEvenly,
            ) {
                PlayController(animatedVisibilityScope)
            }
            Spacer(
                modifier = Modifier.height(18.dp)
            )
            Row(
                modifier = Modifier
                    .fillMaxWidth(),
                verticalAlignment = Alignment.CenterVertically,
                horizontalArrangement = Arrangement.SpaceEvenly,
            ) {
                Text(
                    text = "0:28",
                    fontSize = 12.sp,
                    color = Color.White.copy(alpha = 0.8f),
                )
                LinearProgressIndicator(
                    progress = 0.183f,
                    modifier = Modifier
                        .fillMaxWidth(0.6f)
                        .clip(shape = RoundedCornerShape(100.dp)),
                    color = Color(191, 255, 255, 255),
                    backgroundColor = Color.LightGray,
                )
                Text(
                    text = "2:33",
                    fontSize = 12.sp,
                    color = Color.White.copy(alpha = 0.8f),
                )
            }
        }
    }
}

@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun SharedTransitionScope.Page3(
    animatedVisibilityScope: AnimatedVisibilityScope,
    pageChanged: () -> Unit
) {
    Column(
        modifier = Modifier
            .sharedBounds(
                sharedContentState = rememberSharedContentState("bound"),
                animatedVisibilityScope = animatedVisibilityScope,
                resizeMode = RemeasureToBounds
            )
            .width(220.dp)
            .background(Color.Black, shape = RoundedCornerShape(16.dp))
            .padding(8.dp),
        verticalArrangement = Arrangement.SpaceBetween,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .background(
                    Brush.verticalGradient(
                        listOf(
                            Color(208, 192, 200), Color(103, 96, 100)
                        ),
                    ),
                    shape = RoundedCornerShape(8.dp)
                )
                .padding(12.dp),
            contentAlignment = Alignment.Center
        ) {
            Image(
                painter = painterResource("Rodeo.jpg"),
                contentDescription = null,
                contentScale = ContentScale.Crop,
                modifier = Modifier
                    .sharedBounds(
                        sharedContentState = rememberSharedContentState("image"),
                        animatedVisibilityScope = animatedVisibilityScope,
                        resizeMode = RemeasureToBounds
                    )
                    .width(80.dp)
                    .aspectRatio(1f)
                    .clip(shape = RoundedCornerShape(8.dp))
                    .clickable {
                        pageChanged()
                    },
            )
        }
        Text(
            text = "I Can Tell",
            color = Color.White,
            modifier = Modifier
                .sharedElement(
                    state = rememberSharedContentState(key = "title1"),
                    animatedVisibilityScope = animatedVisibilityScope
                )
        )
        Text(
            text = "Travis Scott",
            color = Color.LightGray,
            modifier = Modifier
                .sharedElement(
                    state = rememberSharedContentState(key = "title2"),
                    animatedVisibilityScope = animatedVisibilityScope
                )
        )
        Row(
            modifier = Modifier.fillMaxWidth(),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.SpaceEvenly
        ) {
            PlayController(animatedVisibilityScope)
        }
    }
}


@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun SharedTransitionScope.PlayController(animatedVisibilityScope: AnimatedVisibilityScope) {
    Icon(
        painter = painterResource("back.svg"),
        contentDescription = null,
        tint = Color.LightGray,
        modifier = Modifier
            .sharedElement(
                state = rememberSharedContentState(key = "back"),
                animatedVisibilityScope = animatedVisibilityScope,
            )
    )
    var start by remember { mutableStateOf(true) }

    IconButton(
        onClick = { start = !start },
        modifier = Modifier
            .sharedElement(
                state = rememberSharedContentState(key = "start"),
                animatedVisibilityScope = animatedVisibilityScope,
            )
            .background(Color.Red, CircleShape)
            .size(40.dp)
    ) {
        // 第一个图标
        AnimatedVisibility(
            visible = start,
            enter = scaleIn(),
            exit = scaleOut()
        ) {
            Icon(
                painter = painterResource("start.svg"),
                contentDescription = null,
                tint = Color.White,
            )
        }
        // 第二个图标
        AnimatedVisibility(
            visible = !start,
            enter = scaleIn(),
            exit = scaleOut()
        ) {
            Icon(
                painter = painterResource("stop.svg"),
                contentDescription = null,
                tint = Color.White,
            )
        }
    }

    Icon(
        painter = painterResource("forward.svg"),
        contentDescription = null,
        tint = Color.LightGray,
        modifier = Modifier
            .sharedElement(
                state = rememberSharedContentState(key = "forward"),
                animatedVisibilityScope = animatedVisibilityScope,
            )
    )
}
相关推荐
TroubleMaker1 小时前
OkHttp源码学习之retryOnConnectionFailure属性
android·java·okhttp
叶羽西3 小时前
Android Studio IDE环境配置
android·ide·android studio
发飙的蜗牛'3 小时前
23种设计模式
android·java·设计模式
花追雨13 小时前
Android -- 双屏异显之方法一
android·双屏异显
小趴菜822713 小时前
安卓 自定义矢量图片控件 - 支持属性修改矢量图路径颜色
android
氤氲息13 小时前
Android v4和v7冲突
android
KdanMin13 小时前
高通Android 12 Launcher应用名称太长显示完整
android
chenjk413 小时前
Android不可擦除分区写文件恢复出厂设置,无法读写问题
android
袁震13 小时前
Android-Glide缓存机制
android·缓存·移动开发·glide
工程师老罗13 小时前
Android笔试面试题AI答之SQLite(2)
android·jvm·sqlite