feat: 优化年月视图共享元素转场动画

- 月→年切换时自动展开折叠状态
- 将 sharedElement 精确绑定到日历网格区域
- YearGridView 为每个 MiniMonth 添加共享元素转场

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
xfy 2026-05-20 16:17:16 +08:00
parent f5cc4ef2e5
commit 9ad619c105
3 changed files with 68 additions and 42 deletions

View File

@ -129,7 +129,15 @@ class CalendarViewModel(
animJob.join()
composeTraceEndSection()
} else {
// 月 → 年:先启动动画(月视图开始缩小),等一帧后翻转 isYearView年视图开始组合
// 月 → 年:如果折叠,先展开
if (isCollapsed) {
_collapseAnimatable.animateTo(
targetValue = 0f,
animationSpec = spring(dampingRatio = 0.8f, stiffness = 400f)
)
isCollapsed = false
}
// 先启动动画(月视图开始缩小),等一帧后翻转 isYearView年视图开始组合
composeTraceBeginSection("MonthView→YearView")
yearViewYear = selectedDate.year
_yearViewAnimatable.snapTo(0f)

View File

@ -159,25 +159,16 @@ fun CalendarMonthView(
targetState = viewModel.isYearView,
label = "month_year_transition",
transitionSpec = {
fadeIn(tween(300, easing = FastOutSlowInEasing)) togetherWith
fadeOut(tween(300, easing = FastOutSlowInEasing))
fadeIn(tween(0)) togetherWith fadeOut(tween(0))
},
modifier = Modifier.fillMaxSize()
) { isYearView ->
with(sharedScope) {
if (!isYearView) {
composeTraceBeginSection("MonthView:Compose")
val layoutReady = rowHeightPx > 0
Box(
modifier = Modifier
.fillMaxSize()
.sharedBounds(
sharedContentState = rememberSharedContentState(key = "month_content"),
animatedVisibilityScope = this@AnimatedContent,
boundsTransform = { _, _ ->
tween(400, easing = FastOutSlowInEasing)
}
)
.alpha(if (layoutReady) 1f else 0f)
) {
Column(
@ -197,17 +188,29 @@ fun CalendarMonthView(
WeekdayHeader(
modifier = Modifier.fillMaxWidth().padding(bottom = ROW_PADDING_DP.dp)
)
CalendarPagerArea(
viewModel = viewModel,
today = today,
rowHeightPx = rowHeightPx,
screenWidthPx = screenWidthPx,
onRowHeightMeasured = { h ->
if (h > 0) rowHeightPx = h
},
pagerState = pagerState,
modifier = Modifier.clipToBounds()
)
with(sharedScope) {
CalendarPagerArea(
viewModel = viewModel,
today = today,
rowHeightPx = rowHeightPx,
screenWidthPx = screenWidthPx,
onRowHeightMeasured = { h ->
if (h > 0) rowHeightPx = h
},
pagerState = pagerState,
modifier = Modifier
.sharedElement(
sharedContentState = rememberSharedContentState(
key = "month_grid_${currentYear}_${currentMonth}"
),
animatedVisibilityScope = this@AnimatedContent,
boundsTransform = { _, _ ->
tween(400, easing = FastOutSlowInEasing)
}
)
.clipToBounds()
)
}
BottomCardArea(
viewModel = viewModel,
today = today,
@ -222,13 +225,6 @@ fun CalendarMonthView(
Column(
modifier = Modifier
.fillMaxSize()
.sharedBounds(
sharedContentState = rememberSharedContentState(key = "month_content"),
animatedVisibilityScope = this@AnimatedContent,
boundsTransform = { _, _ ->
tween(400, easing = FastOutSlowInEasing)
}
)
.padding(horizontal = HORIZONTAL_PADDING_DP.dp)
) {
YearHeader(
@ -273,13 +269,14 @@ fun CalendarMonthView(
coroutineScope.launch { pagerState.scrollToPage(targetPage) }
}
},
sharedTransitionScope = sharedScope,
animatedVisibilityScope = this@AnimatedContent,
modifier = Modifier.alpha(crossFadeAlpha)
)
}
}
composeTraceEndSection()
}
}
}
// FAB 浮动按钮

View File

@ -1,6 +1,10 @@
package plus.rua.project.ui
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInVertically
@ -63,14 +67,19 @@ private data class MiniMonthColors(
* @param selectedMonth 当前选中月份1-12
* @param today 今天的日期
* @param onMonthClick 月份点击回调
* @param sharedTransitionScope 共享元素转场作用域
* @param animatedVisibilityScope 动画可见性作用域
* @param modifier 外部布局修饰符
*/
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun YearGridView(
year: Int,
selectedMonth: Int,
today: LocalDate,
onMonthClick: (Int) -> Unit,
sharedTransitionScope: SharedTransitionScope,
animatedVisibilityScope: AnimatedVisibilityScope,
modifier: Modifier = Modifier
) {
composeTraceBeginSection("YearGridView:$year")
@ -156,18 +165,30 @@ fun YearGridView(
) {
(0 until 3).forEach { col ->
val month = row * 3 + col + 1
MiniMonth(
month = month,
isSelected = month == selectedMonth,
today = today,
days = monthDays[month - 1],
colors = colors,
dayLayouts = dayLayouts,
titleLayouts = titleLayouts,
weekdayLayouts = weekdayLayouts,
onClick = { onMonthClick(month) },
modifier = Modifier.weight(1f)
)
with(sharedTransitionScope) {
MiniMonth(
month = month,
isSelected = month == selectedMonth,
today = today,
days = monthDays[month - 1],
colors = colors,
dayLayouts = dayLayouts,
titleLayouts = titleLayouts,
weekdayLayouts = weekdayLayouts,
onClick = { onMonthClick(month) },
modifier = Modifier
.weight(1f)
.sharedElement(
sharedContentState = rememberSharedContentState(
key = "month_grid_${year}_$month"
),
animatedVisibilityScope = animatedVisibilityScope,
boundsTransform = { _, _ ->
tween(400, easing = FastOutSlowInEasing)
}
)
)
}
}
}
}