年视图标题固定 + 交叉淡入淡出 + 移除 fadeIn/fadeOut
年视图标题行从 HorizontalPager 内移到外部,左右滑动时标题不随 pager 滚动。 年份切换时标题文字用垂直滑动动画(与 MonthHeader 一致,移除 fadeIn/fadeOut)。 月/年视图左右滑动改为交叉淡入淡出,修复原实现中间全白的问题。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
302e6556dd
commit
7250d08fb7
@ -41,6 +41,7 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
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
|
||||||
@ -331,12 +332,9 @@ fun CalendarMonthView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 年视图层:仅在年视图激活时渲染;HorizontalPager 支持左右滑动切年。
|
// 年视图层:标题固定,HorizontalPager 只包裹网格。
|
||||||
if (viewModel.isYearView) {
|
if (viewModel.isYearView) {
|
||||||
HorizontalPager(
|
Column(
|
||||||
state = yearPagerState,
|
|
||||||
beyondViewportPageCount = 1,
|
|
||||||
flingBehavior = PagerDefaults.flingBehavior(state = yearPagerState),
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
@ -346,31 +344,51 @@ fun CalendarMonthView(
|
|||||||
transformOrigin = TransformOrigin(anchorPivotX, anchorPivotY)
|
transformOrigin = TransformOrigin(anchorPivotX, anchorPivotY)
|
||||||
}
|
}
|
||||||
.padding(horizontal = HORIZONTAL_PADDING_DP.dp)
|
.padding(horizontal = HORIZONTAL_PADDING_DP.dp)
|
||||||
) { page ->
|
) {
|
||||||
val pageYear = viewModel.selectedDate.year + (page - START_PAGE)
|
YearHeader(
|
||||||
YearGridView(
|
year = viewModel.yearViewYear,
|
||||||
year = pageYear,
|
|
||||||
selectedMonth = if (pageYear == currentYear) currentMonth else 0,
|
|
||||||
today = today,
|
|
||||||
onMonthClick = { month ->
|
|
||||||
viewModel.selectMonthFromYearView(month)
|
|
||||||
@Suppress("DEPRECATION") // monthNumber 无替代 API
|
|
||||||
val targetPage = yearMonthToPage(
|
|
||||||
viewModel.yearViewYear, month,
|
|
||||||
today.year, today.month.number
|
|
||||||
)
|
|
||||||
if (targetPage != pagerState.currentPage) {
|
|
||||||
coroutineScope.launch { pagerState.scrollToPage(targetPage) }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onYearChange = { newYear ->
|
onYearChange = { newYear ->
|
||||||
val offset = newYear - pageYear
|
val offset = newYear - viewModel.yearViewYear
|
||||||
val targetPage = yearPagerState.currentPage + offset
|
val targetPage = yearPagerState.currentPage + offset
|
||||||
if (targetPage != yearPagerState.currentPage) {
|
if (targetPage != yearPagerState.currentPage) {
|
||||||
coroutineScope.launch { yearPagerState.animateScrollToPage(targetPage) }
|
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,
|
||||||
|
selectedMonth = if (pageYear == currentYear) currentMonth else 0,
|
||||||
|
today = today,
|
||||||
|
onMonthClick = { month ->
|
||||||
|
viewModel.selectMonthFromYearView(month)
|
||||||
|
@Suppress("DEPRECATION") // monthNumber 无替代 API
|
||||||
|
val targetPage = yearMonthToPage(
|
||||||
|
viewModel.yearViewYear, month,
|
||||||
|
today.year, today.month.number
|
||||||
|
)
|
||||||
|
if (targetPage != pagerState.currentPage) {
|
||||||
|
coroutineScope.launch { pagerState.scrollToPage(targetPage) }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.alpha(crossFadeAlpha)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -72,7 +72,12 @@ fun CalendarPager(
|
|||||||
modifier = modifier
|
modifier = modifier
|
||||||
) { page ->
|
) { page ->
|
||||||
val pageOffset = abs(pagerState.currentPageOffsetFraction)
|
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)
|
val (year, month) = pageToYearMonth(page, initialYear, initialMonth)
|
||||||
CalendarMonthPage(
|
CalendarMonthPage(
|
||||||
year = year,
|
year = year,
|
||||||
|
|||||||
@ -2,8 +2,6 @@ package plus.rua.project.ui
|
|||||||
|
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.core.tween
|
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.slideInVertically
|
||||||
import androidx.compose.animation.slideOutVertically
|
import androidx.compose.animation.slideOutVertically
|
||||||
import androidx.compose.animation.togetherWith
|
import androidx.compose.animation.togetherWith
|
||||||
@ -52,11 +50,11 @@ fun MonthHeader(
|
|||||||
targetState = Pair(year, month),
|
targetState = Pair(year, month),
|
||||||
transitionSpec = {
|
transitionSpec = {
|
||||||
if (targetState.second > initialState.second) {
|
if (targetState.second > initialState.second) {
|
||||||
slideInVertically(tween(250)) { -it } + fadeIn(tween(250)) togetherWith
|
slideInVertically(tween(250)) { -it } togetherWith
|
||||||
slideOutVertically(tween(250)) { it } + fadeOut(tween(250))
|
slideOutVertically(tween(250)) { it }
|
||||||
} else {
|
} else {
|
||||||
slideInVertically(tween(250)) { it } + fadeIn(tween(250)) togetherWith
|
slideInVertically(tween(250)) { it } togetherWith
|
||||||
slideOutVertically(tween(250)) { -it } + fadeOut(tween(250))
|
slideOutVertically(tween(250)) { -it }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) { (y, m) ->
|
) { (y, m) ->
|
||||||
@ -70,11 +68,11 @@ fun MonthHeader(
|
|||||||
targetState = weekNumber,
|
targetState = weekNumber,
|
||||||
transitionSpec = {
|
transitionSpec = {
|
||||||
if (targetState > initialState) {
|
if (targetState > initialState) {
|
||||||
slideInVertically(tween(250)) { -it } + fadeIn(tween(250)) togetherWith
|
slideInVertically(tween(250)) { -it } togetherWith
|
||||||
slideOutVertically(tween(250)) { it } + fadeOut(tween(250))
|
slideOutVertically(tween(250)) { it }
|
||||||
} else {
|
} else {
|
||||||
slideInVertically(tween(250)) { it } + fadeIn(tween(250)) togetherWith
|
slideInVertically(tween(250)) { it } togetherWith
|
||||||
slideOutVertically(tween(250)) { -it } + fadeOut(tween(250))
|
slideOutVertically(tween(250)) { -it }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
package plus.rua.project.ui
|
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.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@ -31,16 +36,12 @@ import kotlinx.datetime.plus
|
|||||||
private val WEEKDAY_LABELS = listOf("一", "二", "三", "四", "五", "六", "日")
|
private val WEEKDAY_LABELS = listOf("一", "二", "三", "四", "五", "六", "日")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 年度网格视图,显示 4×3 精简月历网格,支持年份切换。
|
* 年视图 4×3 月历网格。
|
||||||
*
|
|
||||||
* 每格显示一个精简版月历(月份标题 + 星期行 + 日期数字网格),
|
|
||||||
* 选中月份高亮,点击进入该月。
|
|
||||||
*
|
*
|
||||||
* @param year 显示的年份
|
* @param year 显示的年份
|
||||||
* @param selectedMonth 当前选中月份(1-12)
|
* @param selectedMonth 当前选中月份(1-12)
|
||||||
* @param today 今天的日期
|
* @param today 今天的日期
|
||||||
* @param onMonthClick 月份点击回调
|
* @param onMonthClick 月份点击回调
|
||||||
* @param onYearChange 年份切换回调
|
|
||||||
* @param modifier 外部布局修饰符
|
* @param modifier 外部布局修饰符
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
@ -49,49 +50,12 @@ fun YearGridView(
|
|||||||
selectedMonth: Int,
|
selectedMonth: Int,
|
||||||
today: LocalDate,
|
today: LocalDate,
|
||||||
onMonthClick: (Int) -> Unit,
|
onMonthClick: (Int) -> Unit,
|
||||||
onYearChange: (Int) -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize(),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
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 月历网格
|
// 4×3 月历网格
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user