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

View File

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

View File

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