背景
前几天有一个朋友在评论区给我发送一个效果让我实现,在这里给大家展示一下,是一个非常经典的共享元素的例子。
我实现的效果和原本的有一些出入,但是整体一致, 原文地址:www.kbo.sk/components/...
注意,共享元素应该是在compose 1.7之后才能使用,所以本文使用的依赖是:compose.version=1.7.0-rc01,不需要其他依赖库。
本文使用的是Compose for Desktop,代码可能会有一些出入,但是整体问题不大,可以把 painterResource替换成需要Res的。
正文
本文虽然实现了基本效果,但是还是有一些瑕疵:
1.点击图片切换时,会有一瞬间发白,没有原版看着舒服,这个我还不清楚是什么原因。
2.动画快要结束的时候,会有轻微抖动,但是如果你仔细看原版,其实它的效果也有这个问题。
效果图
主要功能
- 页面状态切换 :通过
pageNo
来管理当前页面的状态(Page1, Page2, Page3),并使用AnimatedContent
来进行状态变化时的过渡动画。 - 共享元素过渡 :使用了
SharedTransitionScope
和sharedBounds
以及sharedElement
来实现不同页面之间的共享元素动画。比如封面图片、歌曲标题等元素在页面切换时保持一致并执行动画过渡。 - 动画效果 :代码中使用了
scaleIn
和scaleOut
动画来实现页面切换时的缩放效果,并且在AnimatedVisibility
中对播放按钮进行了动画控制,使用图标的动态缩放来模拟播放与暂停状态的切换。 - 页面布局 :三个页面(Page1, Page2, Page3)在视觉上逐步展开或缩小,并且调整了各组件(如图片和文本)的大小和排列方式。布局通过
Row
和Column
来实现,按钮、文本、图片等组件根据不同页面的需求动态布局。
实现细节与难点
-
SharedTransitionApi
的使用 :这段代码运用了ExperimentalSharedTransitionApi
,让组件在不同页面之间切换时能够以动画形式保持一致性。比如封面图片、歌曲名称和控制按钮在不同页面中是共享的,通过sharedElement
保证这些元素在页面之间保持连贯的过渡体验。sharedBounds
与sharedElement
:这些 API 用于定义跨页面的共享元素和它们的动画。sharedBounds
绑定了组件的边界,使其在两个状态之间平滑缩放,而sharedElement
则定义了具体共享的内容(例如同一个Image
在两个页面间的缩放变化)。
-
AnimatedContent
过渡动画 :AnimatedContent
是用于处理页面内容变化时的过渡动画。在这段代码中,AnimatedContent
包裹了整个内容区域,并通过scaleIn
和scaleOut
实现页面切换时的缩放动画。每次点击图片切换页面时,pageNo
会更新并触发内容动画切换。 -
组件的动画效果 :代码中使用了
AnimatedVisibility
来处理播放控制按钮的显示和隐藏。例如,播放和暂停按钮会根据状态使用scaleIn
和scaleOut
动画实现缩放效果,提供一种更自然的交互体验。 -
状态管理和布局调整 :每个页面的布局略有不同。例如,Page1 中图片较小,Page2 中图片增大并增加了进度条等控件,而 Page3 则进一步简化布局。这些布局切换通过 Jetpack Compose 的
Row
、Column
和Modifier
来管理,并动态调整各组件的大小和位置。
参考
完整代码
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,
)
)
}