年月视图切换时立即移除源视图,仅对目标视图播放缩放动画
之前月↔年切换使用交叉淡入:两层同时合成,源视图渐隐、目标视图渐显。 现改为单向过渡:先翻转 isYearView 让源视图立刻从合成中移除, withFrameNanos 等一帧后再启动目标视图的 scale/alpha 动画,避免抖动。
This commit is contained in:
parent
c28eb8d0e5
commit
aa223db519
@ -88,22 +88,26 @@ class CalendarViewModel(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 切换年视图。仅在展开态可用。
|
* 切换年视图。仅在展开态可用。
|
||||||
|
*
|
||||||
|
* 切换瞬间立即翻转 isYearView,让对应方向的目标视图立刻接管渲染,
|
||||||
|
* 当前视图被直接移除;动画只作用在目标视图的 scale/alpha 上。
|
||||||
*/
|
*/
|
||||||
fun toggleYearView() {
|
fun toggleYearView() {
|
||||||
if (isCollapsed) return
|
if (isCollapsed) return
|
||||||
yearViewJob?.cancel()
|
yearViewJob?.cancel()
|
||||||
yearViewJob = coroutineScope.launch {
|
yearViewJob = coroutineScope.launch {
|
||||||
if (isYearView) {
|
if (isYearView) {
|
||||||
|
// 年 → 月:先切换状态让月视图开始合成,再等一帧避免首帧抖动
|
||||||
|
isYearView = false
|
||||||
|
withFrameNanos { }
|
||||||
_yearViewAnimatable.animateTo(
|
_yearViewAnimatable.animateTo(
|
||||||
0f, tween(400, easing = FastOutSlowInEasing)
|
0f, tween(400, easing = FastOutSlowInEasing)
|
||||||
)
|
)
|
||||||
isYearView = false
|
|
||||||
} else {
|
} else {
|
||||||
|
// 月 → 年:先切换状态让年视图开始合成
|
||||||
yearViewYear = selectedDate.year
|
yearViewYear = selectedDate.year
|
||||||
isYearView = true
|
isYearView = true
|
||||||
_yearViewAnimatable.snapTo(0f)
|
_yearViewAnimatable.snapTo(0f)
|
||||||
// 等待一帧让年视图先完成首次合成与布局,
|
|
||||||
// 避免首次进入年视图时动画时间被合成开销吞掉。
|
|
||||||
withFrameNanos { }
|
withFrameNanos { }
|
||||||
_yearViewAnimatable.animateTo(
|
_yearViewAnimatable.animateTo(
|
||||||
1f, tween(400, easing = FastOutSlowInEasing)
|
1f, tween(400, easing = FastOutSlowInEasing)
|
||||||
@ -120,12 +124,13 @@ class CalendarViewModel(
|
|||||||
val date = if (yearViewYear == today.year && today.month.number == month) today
|
val date = if (yearViewYear == today.year && today.month.number == month) today
|
||||||
else LocalDate(yearViewYear, month, 1)
|
else LocalDate(yearViewYear, month, 1)
|
||||||
selectedDate = date
|
selectedDate = date
|
||||||
|
isYearView = false
|
||||||
yearViewJob?.cancel()
|
yearViewJob?.cancel()
|
||||||
yearViewJob = coroutineScope.launch {
|
yearViewJob = coroutineScope.launch {
|
||||||
|
withFrameNanos { }
|
||||||
_yearViewAnimatable.animateTo(
|
_yearViewAnimatable.animateTo(
|
||||||
0f, tween(400, easing = FastOutSlowInEasing)
|
0f, tween(400, easing = FastOutSlowInEasing)
|
||||||
)
|
)
|
||||||
isYearView = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -168,13 +168,15 @@ fun CalendarMonthView(
|
|||||||
val anchorPivotX = ((currentMonth - 1) % 3 + 0.5f) / 3f
|
val anchorPivotX = ((currentMonth - 1) % 3 + 0.5f) / 3f
|
||||||
val anchorPivotY = ((currentMonth - 1) / 3 + 0.5f) / 4f
|
val anchorPivotY = ((currentMonth - 1) / 3 + 0.5f) / 4f
|
||||||
|
|
||||||
// 月视图层缩放:从 1f 缩小到 ~0.3f(年网格单格 vs 月视图大小比)
|
// 过渡进度:0=目标视图刚出现,1=目标视图完全到位。
|
||||||
val monthScale = 1f - yearProgress * 0.7f
|
// 月→年时 yearProgress 从 0→1,年→月时从 1→0,因此用 isYearView 同步翻转方向。
|
||||||
val monthAlpha = (1f - yearProgress * 1.4f).coerceIn(0f, 1f)
|
val transitionProgress = if (viewModel.isYearView) yearProgress else 1f - yearProgress
|
||||||
|
val targetAlpha = transitionProgress.coerceIn(0f, 1f)
|
||||||
|
|
||||||
// 年视图层缩放:从 ~3.3f 放大到 1f
|
// 月视图层缩放:从 0.3f(年网格单格大小)放大到 1f
|
||||||
val yearScale = lerp(3.3f, 1f, yearProgress)
|
val monthScale = lerp(0.3f, 1f, transitionProgress)
|
||||||
val yearAlpha = ((yearProgress - 0.2f) / 0.8f).coerceIn(0f, 1f)
|
// 年视图层缩放:从 3.3f(月视图被放大到一格那么大的反向比例)缩小到 1f
|
||||||
|
val yearScale = lerp(3.3f, 1f, transitionProgress)
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@ -185,93 +187,118 @@ fun CalendarMonthView(
|
|||||||
screenHeightPx = size.height
|
screenHeightPx = size.height
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
// 月视图层
|
// 月视图层:仅在非年视图时渲染,年视图激活时立即移除。
|
||||||
Column(
|
if (!viewModel.isYearView) {
|
||||||
modifier = Modifier
|
val dragRangeMinPx = with(density) { DRAG_RANGE_MIN_DP.dp.toPx() }
|
||||||
.fillMaxSize()
|
val dragRangePx = if (effectiveRowHeightPx > 0) {
|
||||||
.graphicsLayer {
|
maxOf((effectiveWeeks - 1) * effectiveRowHeightPx.toFloat(), dragRangeMinPx)
|
||||||
scaleX = monthScale
|
|
||||||
scaleY = monthScale
|
|
||||||
alpha = monthAlpha
|
|
||||||
transformOrigin = TransformOrigin(anchorPivotX, anchorPivotY)
|
|
||||||
}
|
|
||||||
.padding(horizontal = HORIZONTAL_PADDING_DP.dp)
|
|
||||||
) {
|
|
||||||
MonthHeader(
|
|
||||||
year = currentYear,
|
|
||||||
month = currentMonth,
|
|
||||||
weekNumber = viewModel.getIsoWeekNumber(viewModel.selectedDate),
|
|
||||||
showToday = viewModel.selectedDate != today,
|
|
||||||
onToggleYearView = { viewModel.toggleYearView() },
|
|
||||||
onToday = {
|
|
||||||
viewModel.selectDate(today)
|
|
||||||
@Suppress("DEPRECATION") // monthNumber 无替代 API
|
|
||||||
val targetPage = yearMonthToPage(
|
|
||||||
today.year, today.month.number,
|
|
||||||
today.year, today.month.number
|
|
||||||
)
|
|
||||||
if (targetPage != pagerState.currentPage) {
|
|
||||||
coroutineScope.launch { pagerState.animateScrollToPage(targetPage) }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier.onSizeChanged { size ->
|
|
||||||
monthHeaderHeightPx = size.height
|
|
||||||
}
|
|
||||||
)
|
|
||||||
WeekdayHeader(
|
|
||||||
modifier = Modifier.fillMaxWidth().padding(bottom = ROW_PADDING_DP.dp)
|
|
||||||
.onSizeChanged { size ->
|
|
||||||
weekdayHeaderHeightPx = size.height
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if (viewModel.isCollapsed && viewModel.collapseProgress >= 1f) {
|
|
||||||
WeekPager(
|
|
||||||
selectedDate = viewModel.selectedDate,
|
|
||||||
today = today,
|
|
||||||
onDateClick = { date -> viewModel.selectDate(date) },
|
|
||||||
onWeekChanged = { weekMonday ->
|
|
||||||
val weekSunday = weekMonday.plus(DatePeriod(days = 6))
|
|
||||||
val date = when {
|
|
||||||
today in weekMonday..weekSunday -> today
|
|
||||||
weekMonday.month != weekSunday.month -> {
|
|
||||||
if (weekMonday < viewModel.selectedDate) {
|
|
||||||
@Suppress("DEPRECATION") // monthNumber 无替代 API
|
|
||||||
LocalDate(weekSunday.year, weekSunday.month.number, 1)
|
|
||||||
} else {
|
|
||||||
weekMonday
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> weekMonday
|
|
||||||
}
|
|
||||||
viewModel.selectDate(date)
|
|
||||||
},
|
|
||||||
modifier = pagerModifier
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
CalendarPager(
|
dragRangeMinPx
|
||||||
selectedDate = viewModel.selectedDate,
|
}
|
||||||
today = today,
|
|
||||||
onDateClick = { date -> viewModel.selectDate(date) },
|
Box(
|
||||||
onMonthChanged = { year, month ->
|
modifier = Modifier
|
||||||
@Suppress("DEPRECATION") // monthNumber 无替代 API
|
.fillMaxSize()
|
||||||
val date = if (year == today.year && today.month.number == month) today
|
.graphicsLayer {
|
||||||
else LocalDate(year, month, 1)
|
scaleX = monthScale
|
||||||
viewModel.selectDate(date)
|
scaleY = monthScale
|
||||||
},
|
alpha = targetAlpha
|
||||||
collapseProgress = viewModel.collapseProgress,
|
transformOrigin = TransformOrigin(anchorPivotX, anchorPivotY)
|
||||||
rowHeightPx = rowHeightPx,
|
}
|
||||||
effectiveWeeks = effectiveWeeks,
|
) {
|
||||||
onRowHeightMeasured = { h ->
|
Column(
|
||||||
if (h > 0) rowHeightPx = h
|
modifier = Modifier
|
||||||
},
|
.fillMaxSize()
|
||||||
pagerState = pagerState,
|
.padding(horizontal = HORIZONTAL_PADDING_DP.dp)
|
||||||
modifier = pagerModifier
|
) {
|
||||||
)
|
MonthHeader(
|
||||||
|
year = currentYear,
|
||||||
|
month = currentMonth,
|
||||||
|
weekNumber = viewModel.getIsoWeekNumber(viewModel.selectedDate),
|
||||||
|
showToday = viewModel.selectedDate != today,
|
||||||
|
onToggleYearView = { viewModel.toggleYearView() },
|
||||||
|
onToday = {
|
||||||
|
viewModel.selectDate(today)
|
||||||
|
@Suppress("DEPRECATION") // monthNumber 无替代 API
|
||||||
|
val targetPage = yearMonthToPage(
|
||||||
|
today.year, today.month.number,
|
||||||
|
today.year, today.month.number
|
||||||
|
)
|
||||||
|
if (targetPage != pagerState.currentPage) {
|
||||||
|
coroutineScope.launch { pagerState.animateScrollToPage(targetPage) }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.onSizeChanged { size ->
|
||||||
|
monthHeaderHeightPx = size.height
|
||||||
|
}
|
||||||
|
)
|
||||||
|
WeekdayHeader(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(bottom = ROW_PADDING_DP.dp)
|
||||||
|
.onSizeChanged { size ->
|
||||||
|
weekdayHeaderHeightPx = size.height
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (viewModel.isCollapsed && viewModel.collapseProgress >= 1f) {
|
||||||
|
WeekPager(
|
||||||
|
selectedDate = viewModel.selectedDate,
|
||||||
|
today = today,
|
||||||
|
onDateClick = { date -> viewModel.selectDate(date) },
|
||||||
|
onWeekChanged = { weekMonday ->
|
||||||
|
val weekSunday = weekMonday.plus(DatePeriod(days = 6))
|
||||||
|
val date = when {
|
||||||
|
today in weekMonday..weekSunday -> today
|
||||||
|
weekMonday.month != weekSunday.month -> {
|
||||||
|
if (weekMonday < viewModel.selectedDate) {
|
||||||
|
@Suppress("DEPRECATION") // monthNumber 无替代 API
|
||||||
|
LocalDate(weekSunday.year, weekSunday.month.number, 1)
|
||||||
|
} else {
|
||||||
|
weekMonday
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> weekMonday
|
||||||
|
}
|
||||||
|
viewModel.selectDate(date)
|
||||||
|
},
|
||||||
|
modifier = pagerModifier
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
CalendarPager(
|
||||||
|
selectedDate = viewModel.selectedDate,
|
||||||
|
today = today,
|
||||||
|
onDateClick = { date -> viewModel.selectDate(date) },
|
||||||
|
onMonthChanged = { year, month ->
|
||||||
|
@Suppress("DEPRECATION") // monthNumber 无替代 API
|
||||||
|
val date = if (year == today.year && today.month.number == month) today
|
||||||
|
else LocalDate(year, month, 1)
|
||||||
|
viewModel.selectDate(date)
|
||||||
|
},
|
||||||
|
collapseProgress = viewModel.collapseProgress,
|
||||||
|
rowHeightPx = rowHeightPx,
|
||||||
|
effectiveWeeks = effectiveWeeks,
|
||||||
|
onRowHeightMeasured = { h ->
|
||||||
|
if (h > 0) rowHeightPx = h
|
||||||
|
},
|
||||||
|
pagerState = pagerState,
|
||||||
|
modifier = pagerModifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cardHeightPx > 0) {
|
||||||
|
BottomCard(
|
||||||
|
viewModel = viewModel,
|
||||||
|
dragRangePx = dragRangePx,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(with(density) { cardHeightPx.toDp() })
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 年视图层:HorizontalPager 支持左右滑动切年
|
// 年视图层:仅在年视图激活时渲染;HorizontalPager 支持左右滑动切年。
|
||||||
if (viewModel.isYearView || yearProgress > 0.01f) {
|
if (viewModel.isYearView) {
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
state = yearPagerState,
|
state = yearPagerState,
|
||||||
beyondViewportPageCount = 1,
|
beyondViewportPageCount = 1,
|
||||||
@ -281,7 +308,7 @@ fun CalendarMonthView(
|
|||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
scaleX = yearScale
|
scaleX = yearScale
|
||||||
scaleY = yearScale
|
scaleY = yearScale
|
||||||
alpha = yearAlpha
|
alpha = targetAlpha
|
||||||
transformOrigin = TransformOrigin(anchorPivotX, anchorPivotY)
|
transformOrigin = TransformOrigin(anchorPivotX, anchorPivotY)
|
||||||
}
|
}
|
||||||
.padding(horizontal = HORIZONTAL_PADDING_DP.dp)
|
.padding(horizontal = HORIZONTAL_PADDING_DP.dp)
|
||||||
@ -312,26 +339,5 @@ fun CalendarMonthView(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BottomCard:年视图时隐藏
|
|
||||||
if (yearProgress < 0.01f) {
|
|
||||||
val dragRangeMinPx = with(density) { DRAG_RANGE_MIN_DP.dp.toPx() }
|
|
||||||
val dragRangePx = if (effectiveRowHeightPx > 0) {
|
|
||||||
maxOf((effectiveWeeks - 1) * effectiveRowHeightPx.toFloat(), dragRangeMinPx)
|
|
||||||
} else {
|
|
||||||
dragRangeMinPx
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cardHeightPx > 0) {
|
|
||||||
BottomCard(
|
|
||||||
viewModel = viewModel,
|
|
||||||
dragRangePx = dragRangePx,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(with(density) { cardHeightPx.toDp() })
|
|
||||||
.align(Alignment.BottomCenter)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user