年视图标题固定 + 交叉淡入淡出 + 移除 fadeIn/fadeOut

年视图标题行从 HorizontalPager 内移到外部,左右滑动时标题不随 pager 滚动。
年份切换时标题文字用垂直滑动动画(与 MonthHeader 一致,移除 fadeIn/fadeOut)。
月/年视图左右滑动改为交叉淡入淡出,修复原实现中间全白的问题。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
xfy 2026-05-18 14:14:46 +08:00
parent 302e6556dd
commit 7250d08fb7
4 changed files with 125 additions and 76 deletions

View File

@ -41,6 +41,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
@ -331,12 +332,9 @@ fun CalendarMonthView(
}
}
// 年视图层:仅在年视图激活时渲染HorizontalPager 支持左右滑动切年
// 年视图层:标题固定HorizontalPager 只包裹网格
if (viewModel.isYearView) {
HorizontalPager(
state = yearPagerState,
beyondViewportPageCount = 1,
flingBehavior = PagerDefaults.flingBehavior(state = yearPagerState),
Column(
modifier = Modifier
.fillMaxSize()
.graphicsLayer {
@ -346,7 +344,32 @@ fun CalendarMonthView(
transformOrigin = TransformOrigin(anchorPivotX, anchorPivotY)
}
.padding(horizontal = HORIZONTAL_PADDING_DP.dp)
) {
YearHeader(
year = viewModel.yearViewYear,
onYearChange = { newYear ->
val offset = newYear - viewModel.yearViewYear
val targetPage = yearPagerState.currentPage + offset
if (targetPage != yearPagerState.currentPage) {
coroutineScope.launch { yearPagerState.animateScrollToPage(targetPage) }
}
}
)
HorizontalPager(
state = yearPagerState,
beyondViewportPageCount = 1,
flingBehavior = PagerDefaults.flingBehavior(state = yearPagerState),
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) { page ->
val pageOffset = abs(yearPagerState.currentPageOffsetFraction)
val isCurrentPage = page == yearPagerState.currentPage
val crossFadeAlpha = if (isCurrentPage) {
1f - pageOffset
} else {
pageOffset
}
val pageYear = viewModel.selectedDate.year + (page - START_PAGE)
YearGridView(
year = pageYear,
@ -363,16 +386,11 @@ fun CalendarMonthView(
coroutineScope.launch { pagerState.scrollToPage(targetPage) }
}
},
onYearChange = { newYear ->
val offset = newYear - pageYear
val targetPage = yearPagerState.currentPage + offset
if (targetPage != yearPagerState.currentPage) {
coroutineScope.launch { yearPagerState.animateScrollToPage(targetPage) }
}
}
modifier = Modifier.alpha(crossFadeAlpha)
)
}
}
}
// FAB 浮动按钮
FloatingActionButton(

View File

@ -72,7 +72,12 @@ fun CalendarPager(
modifier = modifier
) { page ->
val pageOffset = abs(pagerState.currentPageOffsetFraction)
val alpha = 1f - pageOffset.coerceIn(0f, 0.3f) / 0.3f
val isCurrentPage = page == pagerState.currentPage
val alpha = if (isCurrentPage) {
1f - pageOffset
} else {
pageOffset
}
val (year, month) = pageToYearMonth(page, initialYear, initialMonth)
CalendarMonthPage(
year = year,

View File

@ -2,8 +2,6 @@ package plus.rua.project.ui
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
@ -52,11 +50,11 @@ fun MonthHeader(
targetState = Pair(year, month),
transitionSpec = {
if (targetState.second > initialState.second) {
slideInVertically(tween(250)) { -it } + fadeIn(tween(250)) togetherWith
slideOutVertically(tween(250)) { it } + fadeOut(tween(250))
slideInVertically(tween(250)) { -it } togetherWith
slideOutVertically(tween(250)) { it }
} else {
slideInVertically(tween(250)) { it } + fadeIn(tween(250)) togetherWith
slideOutVertically(tween(250)) { -it } + fadeOut(tween(250))
slideInVertically(tween(250)) { it } togetherWith
slideOutVertically(tween(250)) { -it }
}
}
) { (y, m) ->
@ -70,11 +68,11 @@ fun MonthHeader(
targetState = weekNumber,
transitionSpec = {
if (targetState > initialState) {
slideInVertically(tween(250)) { -it } + fadeIn(tween(250)) togetherWith
slideOutVertically(tween(250)) { it } + fadeOut(tween(250))
slideInVertically(tween(250)) { -it } togetherWith
slideOutVertically(tween(250)) { it }
} else {
slideInVertically(tween(250)) { it } + fadeIn(tween(250)) togetherWith
slideOutVertically(tween(250)) { -it } + fadeOut(tween(250))
slideInVertically(tween(250)) { it } togetherWith
slideOutVertically(tween(250)) { -it }
}
},
modifier = Modifier

View File

@ -1,5 +1,10 @@
package plus.rua.project.ui
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -31,16 +36,12 @@ import kotlinx.datetime.plus
private val WEEKDAY_LABELS = listOf("", "", "", "", "", "", "")
/**
* 年度网格视图显示 4×3 精简月历网格支持年份切换
*
* 每格显示一个精简版月历月份标题 + 星期行 + 日期数字网格
* 选中月份高亮点击进入该月
* 年视图 4×3 月历网格
*
* @param year 显示的年份
* @param selectedMonth 当前选中月份1-12
* @param today 今天的日期
* @param onMonthClick 月份点击回调
* @param onYearChange 年份切换回调
* @param modifier 外部布局修饰符
*/
@Composable
@ -49,49 +50,12 @@ fun YearGridView(
selectedMonth: Int,
today: LocalDate,
onMonthClick: (Int) -> Unit,
onYearChange: (Int) -> Unit,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
// 年份导航行
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "",
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier
.clip(CircleShape)
.clickable { onYearChange(year - 1) }
.padding(horizontal = 16.dp, vertical = 8.dp)
)
Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.Center) {
Text(
text = "${year}",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
}
Text(
text = "",
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier
.clip(CircleShape)
.clickable { onYearChange(year + 1) }
.padding(horizontal = 16.dp, vertical = 8.dp)
)
}
// 4×3 月历网格
Column(
modifier = Modifier
@ -244,3 +208,67 @@ private fun generateMiniMonthDays(year: Int, month: Int): List<MiniDayData> {
)
}
}
/**
* 年视图标题栏显示年份文字和左右导航箭头
*
* 年份切换时文字有垂直滑动过渡动画方向由新旧年份大小决定
*
* @param year 当前年份
* @param onYearChange 年份切换回调
* @param modifier 外部布局修饰符
*/
@Composable
fun YearHeader(
year: Int,
onYearChange: (Int) -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "",
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier
.clip(CircleShape)
.clickable { onYearChange(year - 1) }
.padding(horizontal = 16.dp, vertical = 8.dp)
)
Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.Center) {
AnimatedContent(
targetState = year,
transitionSpec = {
if (targetState > initialState) {
slideInVertically(tween(250)) { -it } togetherWith
slideOutVertically(tween(250)) { it }
} else {
slideInVertically(tween(250)) { it } togetherWith
slideOutVertically(tween(250)) { -it }
}
}
) { y ->
Text(
text = "${y}",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
}
}
Text(
text = "",
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier
.clip(CircleShape)
.clickable { onYearChange(year + 1) }
.padding(horizontal = 16.dp, vertical = 8.dp)
)
}
}