Skip to content

Commit be0c31f

Browse files
committed
refactor(动画): 优化共享元素过渡动画
- 移除不必要的导入并增加新的导入以支持共享元素动画 - 重构 ScreenSharedTransition 组件以使用 ScreenTransition - 优化共享元素的修饰符方法,支持更多自定义选项 - 更新文本共享元素的动画方式,使用新的 TextBoundsTransform- 调整默认屏幕过渡动画的参数,提升动画效果
1 parent 584e0ef commit be0c31f

File tree

11 files changed

+164
-74
lines changed

11 files changed

+164
-74
lines changed

composeApp/src/commonMain/kotlin/App.kt

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package io.github.vrcmteam.vrcm
22

3-
import androidx.compose.animation.*
3+
import androidx.compose.animation.AnimatedContentTransitionScope
4+
import androidx.compose.animation.ContentTransform
5+
import androidx.compose.animation.EnterTransition
6+
import androidx.compose.animation.ExitTransition
47
import androidx.compose.foundation.layout.padding
58
import androidx.compose.foundation.layout.systemBarsPadding
69
import androidx.compose.runtime.Composable
@@ -9,8 +12,8 @@ import androidx.compose.ui.unit.dp
912
import cafe.adriel.voyager.core.screen.Screen
1013
import cafe.adriel.voyager.navigator.Navigator
1114
import io.github.vrcmteam.vrcm.presentation.animations.AuthAnimeToHomeTransition
12-
import io.github.vrcmteam.vrcm.presentation.animations.DefaultScreenTransition
1315
import io.github.vrcmteam.vrcm.presentation.animations.HomeToAuthAnimeTransition
16+
import io.github.vrcmteam.vrcm.presentation.animations.slideScreenTransition
1417
import io.github.vrcmteam.vrcm.presentation.compoments.ScreenSharedTransition
1518
import io.github.vrcmteam.vrcm.presentation.compoments.SnackBarToastBox
1619
import io.github.vrcmteam.vrcm.presentation.extensions.isTransitioningFromTo
@@ -24,7 +27,6 @@ import io.github.vrcmteam.vrcm.presentation.screens.world.WorldProfileScreen
2427
import io.github.vrcmteam.vrcm.presentation.settings.SettingsProvider
2528
import org.koin.compose.KoinContext
2629

27-
@OptIn(ExperimentalSharedTransitionApi::class)
2830
@Composable
2931
fun App() {
3032
KoinContext {
@@ -48,8 +50,8 @@ fun App() {
4850

4951
fun AnimatedContentTransitionScope<Screen>.selectTransition(navigator: Navigator): ContentTransform =
5052
when {
51-
isTransitioningOn<HomeScreen, UserProfileScreen>() -> DefaultScreenTransition
52-
isTransitioningOn<HomeScreen, WorldProfileScreen>() -> DefaultScreenTransition
53+
isTransitioningOn<HomeScreen, UserProfileScreen>() -> slideScreenTransition(navigator)
54+
isTransitioningOn<HomeScreen, WorldProfileScreen>() -> slideScreenTransition(navigator)
5355
isTransitioningFromTo<HomeScreen, AuthAnimeScreen>() -> HomeToAuthAnimeTransition
5456
isTransitioningFromTo<AuthAnimeScreen, HomeScreen>() -> AuthAnimeToHomeTransition
5557
else -> ContentTransform(EnterTransition.None, ExitTransition.None)
Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,86 @@
11
package io.github.vrcmteam.vrcm.presentation.animations
22

33
import androidx.compose.animation.*
4-
import androidx.compose.animation.core.FiniteAnimationSpec
5-
import androidx.compose.animation.core.tween
4+
import androidx.compose.animation.SharedTransitionScope.OverlayClip
5+
import androidx.compose.animation.SharedTransitionScope.SharedContentState
6+
import androidx.compose.animation.core.*
7+
import androidx.compose.animation.core.Spring.StiffnessMediumLow
8+
import androidx.compose.ui.geometry.Rect
9+
import androidx.compose.ui.graphics.Path
610
import androidx.compose.ui.graphics.TransformOrigin
11+
import androidx.compose.ui.unit.Density
712
import androidx.compose.ui.unit.IntOffset
13+
import androidx.compose.ui.unit.LayoutDirection
814
import cafe.adriel.voyager.core.stack.StackEvent
915
import cafe.adriel.voyager.navigator.Navigator
1016
import cafe.adriel.voyager.transitions.SlideOrientation
1117

1218

13-
val HomeToAuthAnimeTransition = fadeIn(tween(600,300)) + slideIn(tween(600)) { IntOffset(0, (it.height * 0.2f).toInt()) } togetherWith
14-
fadeOut(tween(600))
19+
val HomeToAuthAnimeTransition =
20+
fadeIn(tween(600, 300)) + slideIn(tween(600)) { IntOffset(0, (it.height * 0.2f).toInt()) } togetherWith
21+
fadeOut(tween(600))
1522
val AuthAnimeToHomeTransition = fadeIn(tween(600)) togetherWith
1623
fadeOut(tween(600)) + slideOut(tween(600)) { IntOffset(0, (it.height * 0.2f).toInt()) }
1724
val DefaultScreenTransition = (fadeIn(animationSpec = tween(220, delayMillis = 90)) +
18-
scaleIn(initialScale = 0.92f, animationSpec = tween(220, delayMillis = 90)))
19-
.togetherWith(fadeOut(animationSpec = tween(90)))
25+
scaleIn(initialScale = 0.92f, animationSpec = tween(220, delayMillis = 90)))
26+
.togetherWith(fadeOut(animationSpec = tween(90)))
27+
28+
private const val BoundsAnimationDurationMillis = 500
29+
30+
@OptIn(ExperimentalSharedTransitionApi::class, ExperimentalAnimationSpecApi::class)
31+
val TextBoundsTransform = BoundsTransform { initialBounds, targetBounds ->
32+
keyframes {
33+
durationMillis = BoundsAnimationDurationMillis
34+
initialBounds at 0 using ArcMode.ArcBelow using FastOutSlowInEasing
35+
targetBounds at BoundsAnimationDurationMillis
36+
}
37+
}
38+
39+
@OptIn(ExperimentalSharedTransitionApi::class)
40+
val DefaultBoundsTransform = BoundsTransform { _, _ -> DefaultSpring }
41+
42+
val DefaultSpring = spring(
43+
stiffness = StiffnessMediumLow,
44+
visibilityThreshold = Rect.VisibilityThreshold
45+
)
46+
47+
@OptIn(ExperimentalSharedTransitionApi::class)
48+
val ParentClip: OverlayClip =
49+
object : OverlayClip {
50+
override fun getClipPath(
51+
state: SharedContentState,
52+
bounds: Rect,
53+
layoutDirection: LayoutDirection,
54+
density: Density,
55+
): Path? {
56+
return state.parentSharedContentState?.clipPathInOverlay
57+
}
58+
}
59+
2060

2161
fun slideScreenTransition(
2262
navigator: Navigator,
23-
orientation: SlideOrientation = SlideOrientation.Horizontal,
24-
inAnimationSpec: FiniteAnimationSpec<IntOffset> = tween(),
25-
outAnimationSpec: FiniteAnimationSpec<Float> = tween()
63+
orientation: SlideOrientation = SlideOrientation.Vertical,
2664
): ContentTransform {
2765
val initialOffset = when (navigator.lastEvent) {
2866
StackEvent.Pop -> { size: Int -> -size }
2967
else -> { size: Int -> size }
3068
}
31-
32-
return when (orientation) {
33-
SlideOrientation.Horizontal ->
34-
slideInHorizontally(inAnimationSpec, initialOffset) togetherWith
35-
scaleOut(outAnimationSpec, 0.8f, TransformOrigin(0.3f, 0.5f)) + fadeOut(outAnimationSpec,0.8f)
69+
val targetOffset = when (navigator.lastEvent) {
70+
StackEvent.Pop -> { size: Int -> size }
71+
else -> { size: Int -> -size }
72+
}
73+
val animationIntSpec = tween<IntOffset>()
74+
val animationFloatSpec = tween<Float>()
75+
return when (orientation) {
76+
SlideOrientation.Horizontal -> slideInHorizontally(animationIntSpec, initialOffset) togetherWith scaleOut(
77+
animationFloatSpec,
78+
0.8f,
79+
TransformOrigin(0.3f, 0.5f)
80+
) + fadeOut(animationFloatSpec, 0.8f)
3681

3782
SlideOrientation.Vertical ->
38-
slideInVertically(inAnimationSpec, initialOffset) togetherWith
39-
scaleOut(outAnimationSpec,0.8f, TransformOrigin(0.5f, 0.3f)) + fadeOut(outAnimationSpec,0.8f)
83+
slideInVertically(initialOffsetY= initialOffset) togetherWith slideOutVertically(targetOffsetY = targetOffset)
84+
4085
}
41-
}
86+
}

composeApp/src/commonMain/kotlin/presentation/compoments/ProfileScaffold.kt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -255,12 +255,11 @@ private fun ProfileIcon(
255255
) {
256256
AImage(
257257
modifier = Modifier
258-
.enableIf(!isHidden) { then(imageModifier) }
259258
.align(Alignment.Center)
260-
.clip(CircleShape)
261259
.size(iconSize)
262-
.clickable(onClick = onClickIcon)
263-
,
260+
.enableIf(!isHidden) { then(imageModifier) }
261+
.clip(CircleShape)
262+
.clickable(onClick = onClickIcon),
264263
imageData = ImageRequest.Builder(koinInject<PlatformContext>())
265264
.data(avatarThumbnailImageUrl)
266265
.crossfade(600)
Lines changed: 63 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,39 @@
11
package io.github.vrcmteam.vrcm.presentation.compoments
22

33
import androidx.compose.animation.*
4+
import androidx.compose.animation.SharedTransitionScope.OverlayClip
5+
import androidx.compose.animation.SharedTransitionScope.PlaceHolderSize
6+
import androidx.compose.animation.SharedTransitionScope.PlaceHolderSize.Companion.contentSize
47
import androidx.compose.runtime.Composable
58
import androidx.compose.runtime.CompositionLocalProvider
69
import androidx.compose.runtime.ProvidableCompositionLocal
710
import androidx.compose.runtime.staticCompositionLocalOf
8-
import androidx.compose.ui.Alignment
911
import androidx.compose.ui.Modifier
1012
import cafe.adriel.voyager.core.screen.Screen
1113
import cafe.adriel.voyager.navigator.Navigator
14+
import cafe.adriel.voyager.transitions.ScreenTransition
15+
import io.github.vrcmteam.vrcm.presentation.animations.DefaultBoundsTransform
16+
import io.github.vrcmteam.vrcm.presentation.animations.DefaultScreenTransition
17+
import io.github.vrcmteam.vrcm.presentation.animations.ParentClip
1218

1319
@OptIn(ExperimentalSharedTransitionApi::class)
1420
@Composable
1521
fun ScreenSharedTransition(
1622
navigator: Navigator,
1723
modifier: Modifier = Modifier,
18-
contentAlignment: Alignment = Alignment.TopStart,
19-
contentKey: (Screen) -> Any = { it.key },
20-
transitionSpec: AnimatedContentTransitionScope<Screen>.() -> ContentTransform,
21-
content: @Composable (Screen) -> Unit = {screen ->
22-
screen.Content()
23-
},
24+
transitionSpec: AnimatedContentTransitionScope<Screen>.() -> ContentTransform = { DefaultScreenTransition },
2425
) {
2526
SharedTransitionLayout(modifier) {
26-
AnimatedContent(
27-
targetState = navigator.lastItem,
28-
transitionSpec = transitionSpec,
29-
contentAlignment = contentAlignment,
30-
contentKey = contentKey,
31-
) { screen ->
32-
navigator.saveableState("transition", screen) {
33-
CompositionLocalProvider(
34-
LocalSharedTransitionScope provides this@SharedTransitionLayout,
35-
LocalAnimatedVisibilityScope provides this
36-
){
37-
content(screen)
38-
}
27+
ScreenTransition(
28+
navigator = navigator,
29+
modifier = modifier,
30+
transition = transitionSpec,
31+
){ screen ->
32+
CompositionLocalProvider(
33+
LocalSharedTransitionScope provides this@SharedTransitionLayout,
34+
LocalAnimatedVisibilityScope provides this
35+
) {
36+
screen.Content()
3937
}
4038
}
4139
}
@@ -44,17 +42,56 @@ fun ScreenSharedTransition(
4442
}
4543

4644

47-
4845
@OptIn(ExperimentalSharedTransitionApi::class)
4946
val LocalSharedTransitionScope: ProvidableCompositionLocal<SharedTransitionScope> =
50-
staticCompositionLocalOf { error( "SharedTransitionScope is not provided") }
47+
staticCompositionLocalOf { error("SharedTransitionScope is not provided") }
5148

5249
val LocalAnimatedVisibilityScope: ProvidableCompositionLocal<AnimatedVisibilityScope> =
53-
staticCompositionLocalOf { error( "AnimatedVisibilityScope is not provided") }
50+
staticCompositionLocalOf { error("AnimatedVisibilityScope is not provided") }
51+
52+
@OptIn(ExperimentalSharedTransitionApi::class)
53+
@Composable
54+
fun Modifier.sharedElementBy(
55+
key: String,
56+
boundsTransform: BoundsTransform = DefaultBoundsTransform,
57+
placeHolderSize: PlaceHolderSize = contentSize,
58+
renderInOverlayDuringTransition: Boolean = true,
59+
zIndexInOverlay: Float = 0f,
60+
clipInOverlayDuringTransition: OverlayClip = ParentClip
61+
): Modifier =
62+
with(LocalSharedTransitionScope.current) {
63+
this@sharedElementBy.sharedElement(
64+
state = rememberSharedContentState(key),
65+
animatedVisibilityScope = LocalAnimatedVisibilityScope.current,
66+
boundsTransform = boundsTransform,
67+
placeHolderSize = placeHolderSize,
68+
renderInOverlayDuringTransition = renderInOverlayDuringTransition,
69+
zIndexInOverlay = zIndexInOverlay,
70+
clipInOverlayDuringTransition = clipInOverlayDuringTransition
71+
)
72+
}
73+
5474

5575
@OptIn(ExperimentalSharedTransitionApi::class)
5676
@Composable
57-
inline fun Modifier.sharedElementBy(key: String): Modifier =
58-
with(LocalSharedTransitionScope.current){
59-
this@sharedElementBy.sharedElement(rememberSharedContentState(key),LocalAnimatedVisibilityScope.current)
77+
fun Modifier.sharedBoundsBy(
78+
key: String,
79+
boundsTransform: BoundsTransform = DefaultBoundsTransform,
80+
placeHolderSize: PlaceHolderSize = contentSize,
81+
renderInOverlayDuringTransition: Boolean = true,
82+
zIndexInOverlay: Float = 0f,
83+
clipInOverlayDuringTransition: OverlayClip = ParentClip
84+
): Modifier =
85+
with(LocalSharedTransitionScope.current) {
86+
this@sharedBoundsBy.sharedBounds(
87+
sharedContentState = rememberSharedContentState(key),
88+
animatedVisibilityScope = LocalAnimatedVisibilityScope.current,
89+
boundsTransform = boundsTransform,
90+
placeHolderSize = placeHolderSize,
91+
renderInOverlayDuringTransition = renderInOverlayDuringTransition,
92+
zIndexInOverlay = zIndexInOverlay,
93+
clipInOverlayDuringTransition = clipInOverlayDuringTransition
94+
)
6095
}
96+
97+

composeApp/src/commonMain/kotlin/presentation/compoments/UserSearchList.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.github.vrcmteam.vrcm.presentation.compoments
22

3+
import androidx.compose.animation.ExperimentalSharedTransitionApi
34
import androidx.compose.foundation.clickable
45
import androidx.compose.foundation.layout.*
56
import androidx.compose.foundation.lazy.*
@@ -124,6 +125,7 @@ fun UserList(
124125

125126
}
126127

128+
@OptIn(ExperimentalSharedTransitionApi::class)
127129
@Composable
128130
fun LazyItemScope.UserListItem(friend: IUser, toProfile: (IUser) -> Unit) {
129131
ListItem(
@@ -150,7 +152,7 @@ fun LazyItemScope.UserListItem(friend: IUser, toProfile: (IUser) -> Unit) {
150152
tint = GameColor.Rank.fromValue(friend.trustRank)
151153
)
152154
Text(
153-
modifier = Modifier.sharedElementBy("${friend.id}UserName"),
155+
modifier = Modifier.sharedBoundsBy("${friend.id}UserName"),
154156
text = friend.displayName,
155157
style = MaterialTheme.typography.titleMedium,
156158
maxLines = 1,
@@ -160,7 +162,7 @@ fun LazyItemScope.UserListItem(friend: IUser, toProfile: (IUser) -> Unit) {
160162
},
161163
supportingContent = {
162164
Text(
163-
modifier = Modifier.sharedElementBy("${friend.id}UserStatusDescription"),
165+
modifier = Modifier.sharedBoundsBy("${friend.id}UserStatusDescription"),
164166
text = friend.statusDescription.ifBlank { friend.status.value },
165167
style = MaterialTheme.typography.bodyMedium,
166168
maxLines = 1

composeApp/src/commonMain/kotlin/presentation/extensions/Modifiers.kt

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ fun Modifier.drawSate(
2828
isInLine: Boolean = true,
2929
alignment: Alignment = Alignment.BottomEnd,
3030
enable: Boolean = true,
31-
onDraw: ContentDrawScope.(Float, Offset) -> Unit
32-
) = if (enable) this.drawWithContent {
31+
onDraw: ContentDrawScope.(Float, Offset) -> Unit,
32+
) = if (enable) this.drawWithContent {
3333
val borderRadius = size.maxDimension * percentage
3434
val borderDiameter = borderRadius * 2
3535
val borderTopStart = if (isInLine) Offset(-borderRadius, -borderRadius) else Offset.Zero
@@ -43,7 +43,7 @@ fun Modifier.drawSate(
4343
layoutDirection = layoutDirection
4444
)
4545
)
46-
onDraw(borderRadius,borderOffset)
46+
onDraw(borderRadius, borderOffset)
4747
} else this
4848

4949
fun Modifier.drawSateCircle(
@@ -58,7 +58,7 @@ fun Modifier.drawSateCircle(
5858
this.drawContent()
5959
drawCircle(Color.White, borderRadius, borderOffset)
6060
drawCircle(color, radius, borderOffset)
61-
}
61+
},
6262
) = drawSate(
6363
percentage = percentage,
6464
isInLine = isInLine,
@@ -68,26 +68,28 @@ fun Modifier.drawSateCircle(
6868
)
6969

7070
@Composable
71-
fun Modifier.enableIf(enable: Boolean = true, effect: @Composable Modifier.() -> Modifier) = if (enable) effect() else this
71+
fun Modifier.enableIf(enable: Boolean = true, effect: @Composable Modifier.() -> Modifier) =
72+
if (enable) effect() else this
7273

7374
/**
7475
* 侧滑返回
7576
*/
7677
fun Modifier.slideBack(
77-
threshold: Float = 40.dp.value
78+
threshold: Float = 40.dp.value,
79+
orientation: Orientation = Orientation.Horizontal,
7880
) = this.composed {
7981
val navigator = LocalNavigator.currentOrThrow
8082
draggable(rememberDraggableState {
8183
if (navigator.canPop && it > threshold) navigator.pop()
82-
}, Orientation.Horizontal)
84+
}, orientation)
8385
}
8486

8587
/**
8688
* 去除点击水波纹效果
8789
*/
8890
@Composable
8991
fun Modifier.simpleClickable(
90-
onClick: () -> Unit
92+
onClick: () -> Unit,
9193
) = this.clickable(
9294
indication = null,
9395
interactionSource = remember { MutableInteractionSource() },

0 commit comments

Comments
 (0)