使用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,
            )
    )
}
相关推荐
NRatel3 分钟前
Unity 游戏提升 Android TargetVersion 相关记录
android·游戏·unity·提升版本
叽哥2 小时前
Kotlin学习第 1 课:Kotlin 入门准备:搭建学习环境与认知基础
android·java·kotlin
风往哪边走3 小时前
创建自定义语音录制View
android·前端
用户2018792831673 小时前
事件分发之“官僚主义”?或“绕圈”的艺术
android
用户2018792831673 小时前
Android事件分发为何喜欢“兜圈子”?不做个“敞亮人”!
android
Kapaseker5 小时前
你一定会喜欢的 Compose 形变动画
android
QuZhengRong5 小时前
【数据库】Navicat 导入 Excel 数据乱码问题的解决方法
android·数据库·excel
zhangphil6 小时前
Android Coil3视频封面抽取封面帧存Disk缓存,Kotlin(2)
android·kotlin
程序员码歌13 小时前
【零代码AI编程实战】AI灯塔导航-总结篇
android·前端·后端
书弋江山14 小时前
flutter 跨平台编码库 protobuf 工具使用
android·flutter