feat: US-007~010 实现年月视图共享元素转场动画
- 引入 SharedTransitionLayout + AnimatedContent 管理月/年视图切换 - 使用 sharedBounds + rememberSharedContentState 标记共享元素 - 转场动画 400ms,FastOutSlowInEasing - 添加 compose-animation 依赖支持 SharedTransition API Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c19916c2ec
commit
cf9315cfe3
@ -24,6 +24,7 @@ androidx-lifecycle-viewmodelCompose = { module = "androidx.lifecycle:lifecycle-v
|
|||||||
androidx-lifecycle-runtimeCompose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
|
androidx-lifecycle-runtimeCompose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
|
||||||
compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" }
|
compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" }
|
||||||
compose-runtime = { module = "androidx.compose.runtime:runtime" }
|
compose-runtime = { module = "androidx.compose.runtime:runtime" }
|
||||||
|
compose-animation = { module = "androidx.compose.animation:animation" }
|
||||||
compose-foundation = { module = "androidx.compose.foundation:foundation" }
|
compose-foundation = { module = "androidx.compose.foundation:foundation" }
|
||||||
compose-material3 = { module = "androidx.compose.material3:material3" }
|
compose-material3 = { module = "androidx.compose.material3:material3" }
|
||||||
compose-ui = { module = "androidx.compose.ui:ui" }
|
compose-ui = { module = "androidx.compose.ui:ui" }
|
||||||
|
|||||||
@ -45,6 +45,7 @@ dependencies {
|
|||||||
|
|
||||||
implementation(libs.compose.runtime)
|
implementation(libs.compose.runtime)
|
||||||
implementation(libs.compose.foundation)
|
implementation(libs.compose.foundation)
|
||||||
|
implementation(libs.compose.animation)
|
||||||
implementation(libs.compose.material3)
|
implementation(libs.compose.material3)
|
||||||
implementation(libs.compose.ui)
|
implementation(libs.compose.ui)
|
||||||
implementation(libs.compose.uiToolingPreview)
|
implementation(libs.compose.uiToolingPreview)
|
||||||
|
|||||||
@ -1,14 +1,19 @@
|
|||||||
package plus.rua.project.ui
|
package plus.rua.project.ui
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.EnterTransition
|
import androidx.compose.animation.EnterTransition
|
||||||
import androidx.compose.animation.ExitTransition
|
import androidx.compose.animation.ExitTransition
|
||||||
|
import androidx.compose.animation.SharedTransitionLayout
|
||||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||||
import androidx.compose.animation.scaleIn
|
import androidx.compose.animation.scaleIn
|
||||||
import androidx.compose.animation.scaleOut
|
import androidx.compose.animation.scaleOut
|
||||||
|
import androidx.compose.animation.togetherWith
|
||||||
|
import androidx.compose.ui.graphics.TransformOrigin
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
@ -50,7 +55,6 @@ import androidx.compose.ui.draw.alpha
|
|||||||
import androidx.compose.ui.draw.clipToBounds
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.TransformOrigin
|
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.layout.onSizeChanged
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
@ -70,13 +74,14 @@ import kotlin.math.abs
|
|||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 日历主界面,包含月/周视图切换、折叠动画和年视图缩放转场。
|
* 日历主界面,包含月/周视图切换、折叠动画和年视图共享元素转场。
|
||||||
*
|
*
|
||||||
* 折叠时日历从月视图收缩为周视图(1行),BottomCard 同步上移填充空间。
|
* 折叠时日历从月视图收缩为周视图(1行),BottomCard 同步上移填充空间。
|
||||||
* 通过左下角 FAB 菜单切换月/年视图,以当前月为锚点缩放转场。
|
* 通过左下角 FAB 菜单切换月/年视图,使用 SharedTransitionLayout 实现共享元素转场。
|
||||||
*
|
*
|
||||||
* @param modifier 外部布局修饰符
|
* @param modifier 外部布局修饰符
|
||||||
*/
|
*/
|
||||||
|
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun CalendarMonthView(
|
fun CalendarMonthView(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@ -139,10 +144,8 @@ fun CalendarMonthView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 年视图锚点缩放:当前月在 4×3 网格中的归一化位置
|
SharedTransitionLayout {
|
||||||
val anchorPivotX = ((currentMonth - 1) % 3 + 0.5f) / 3f
|
val sharedScope = this
|
||||||
val anchorPivotY = ((currentMonth - 1) / 3 + 0.5f) / 4f
|
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@ -152,21 +155,30 @@ fun CalendarMonthView(
|
|||||||
screenWidthPx = size.width
|
screenWidthPx = size.width
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
// 月视图层:仅在非年视图时渲染,年视图激活时立即移除。
|
AnimatedContent(
|
||||||
if (!viewModel.isYearView) {
|
targetState = viewModel.isYearView,
|
||||||
|
label = "month_year_transition",
|
||||||
|
transitionSpec = {
|
||||||
|
fadeIn(tween(300, easing = FastOutSlowInEasing)) togetherWith
|
||||||
|
fadeOut(tween(300, easing = FastOutSlowInEasing))
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) { isYearView ->
|
||||||
|
with(sharedScope) {
|
||||||
|
if (!isYearView) {
|
||||||
composeTraceBeginSection("MonthView:Compose")
|
composeTraceBeginSection("MonthView:Compose")
|
||||||
val layoutReady = rowHeightPx > 0
|
val layoutReady = rowHeightPx > 0
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.graphicsLayer {
|
.sharedBounds(
|
||||||
val monthProgress = 1f - viewModel.yearViewProgress
|
sharedContentState = rememberSharedContentState(key = "month_content"),
|
||||||
val scale = lerp(0.3f, 1f, monthProgress)
|
animatedVisibilityScope = this@AnimatedContent,
|
||||||
scaleX = scale
|
boundsTransform = { _, _ ->
|
||||||
scaleY = scale
|
tween(400, easing = FastOutSlowInEasing)
|
||||||
alpha = if (layoutReady) monthProgress.coerceIn(0f, 1f) else 0f
|
|
||||||
transformOrigin = TransformOrigin(anchorPivotX, anchorPivotY)
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
.alpha(if (layoutReady) 1f else 0f)
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@ -205,22 +217,18 @@ fun CalendarMonthView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
composeTraceEndSection()
|
composeTraceEndSection()
|
||||||
}
|
} else {
|
||||||
|
|
||||||
// 年视图层:标题固定,HorizontalPager 只包裹网格。
|
|
||||||
if (viewModel.isYearView) {
|
|
||||||
val yearProgress = viewModel.yearViewProgress
|
|
||||||
composeTraceBeginSection("YearView:Compose")
|
composeTraceBeginSection("YearView:Compose")
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.graphicsLayer {
|
.sharedBounds(
|
||||||
val scale = lerp(3.3f, 1f, yearProgress)
|
sharedContentState = rememberSharedContentState(key = "month_content"),
|
||||||
scaleX = scale
|
animatedVisibilityScope = this@AnimatedContent,
|
||||||
scaleY = scale
|
boundsTransform = { _, _ ->
|
||||||
alpha = yearProgress.coerceIn(0f, 1f)
|
tween(400, easing = FastOutSlowInEasing)
|
||||||
transformOrigin = TransformOrigin(anchorPivotX, anchorPivotY)
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
.padding(horizontal = HORIZONTAL_PADDING_DP.dp)
|
.padding(horizontal = HORIZONTAL_PADDING_DP.dp)
|
||||||
) {
|
) {
|
||||||
YearHeader(
|
YearHeader(
|
||||||
@ -271,6 +279,8 @@ fun CalendarMonthView(
|
|||||||
}
|
}
|
||||||
composeTraceEndSection()
|
composeTraceEndSection()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// FAB 浮动按钮
|
// FAB 浮动按钮
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
@ -358,6 +368,7 @@ fun CalendarMonthView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MenuIcon(color: Color, modifier: Modifier = Modifier) {
|
private fun MenuIcon(color: Color, modifier: Modifier = Modifier) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user