refactor(ui): optimize year view performance, animations, and layout
This commit is contained in:
parent
d7406ef652
commit
fd7f79c3d3
@ -235,16 +235,11 @@ fun CalendarMonthView(
|
|||||||
targetState = isYearView,
|
targetState = isYearView,
|
||||||
label = "month_year_transition",
|
label = "month_year_transition",
|
||||||
transitionSpec = {
|
transitionSpec = {
|
||||||
val spatialEasing = FastOutSlowInEasing
|
fadeIn(
|
||||||
val enter = scaleIn(
|
animationSpec = spring(stiffness = Spring.StiffnessMediumLow, dampingRatio = Spring.DampingRatioNoBouncy)
|
||||||
initialScale = 0.92f,
|
) togetherWith fadeOut(
|
||||||
animationSpec = tween(380, easing = spatialEasing)
|
animationSpec = spring(stiffness = Spring.StiffnessMediumLow, dampingRatio = Spring.DampingRatioNoBouncy)
|
||||||
) + fadeIn(tween(380, easing = spatialEasing))
|
)
|
||||||
val exit = scaleOut(
|
|
||||||
targetScale = 0.92f,
|
|
||||||
animationSpec = tween(380, easing = spatialEasing)
|
|
||||||
) + fadeOut(tween(380, easing = spatialEasing))
|
|
||||||
enter togetherWith exit
|
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
) { yearViewActive ->
|
) { yearViewActive ->
|
||||||
@ -397,6 +392,9 @@ fun CalendarMonthView(
|
|||||||
BackHandler(enabled = isMenuExpanded) {
|
BackHandler(enabled = isMenuExpanded) {
|
||||||
isMenuExpanded = false
|
isMenuExpanded = false
|
||||||
}
|
}
|
||||||
|
BackHandler(enabled = isYearView && !isMenuExpanded) {
|
||||||
|
viewModel.toggleYearView()
|
||||||
|
}
|
||||||
|
|
||||||
// Scrim:半透明遮罩,在菜单展开时淡入拦截点击关闭菜单,增强渐变视觉与聚焦感
|
// Scrim:半透明遮罩,在菜单展开时淡入拦截点击关闭菜单,增强渐变视觉与聚焦感
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
|
|||||||
@ -1,14 +1,23 @@
|
|||||||
package plus.rua.project.ui
|
package plus.rua.project.ui
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.Spring
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
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.scaleIn
|
||||||
|
import androidx.compose.animation.scaleOut
|
||||||
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
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@ -17,29 +26,32 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.ui.platform.testTag
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
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.clip
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.drawText
|
import androidx.compose.ui.text.drawText
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.rememberTextMeasurer
|
import androidx.compose.ui.text.rememberTextMeasurer
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.semantics.contentDescription
|
|
||||||
import androidx.compose.ui.semantics.semantics
|
|
||||||
import com.tyme.lunar.LunarYear
|
import com.tyme.lunar.LunarYear
|
||||||
import kotlinx.datetime.DatePeriod
|
import kotlinx.datetime.DatePeriod
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
@ -49,6 +61,9 @@ import kotlinx.datetime.number
|
|||||||
import kotlinx.datetime.plus
|
import kotlinx.datetime.plus
|
||||||
import plus.rua.project.composeTraceBeginSection
|
import plus.rua.project.composeTraceBeginSection
|
||||||
import plus.rua.project.composeTraceEndSection
|
import plus.rua.project.composeTraceEndSection
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
import plus.rua.project.composeTraceBeginSection
|
||||||
|
import plus.rua.project.composeTraceEndSection
|
||||||
|
|
||||||
private val WEEKDAY_LABELS = listOf("一", "二", "三", "四", "五", "六", "日")
|
private val WEEKDAY_LABELS = listOf("一", "二", "三", "四", "五", "六", "日")
|
||||||
|
|
||||||
@ -105,8 +120,8 @@ fun YearGridView(
|
|||||||
val textMeasurer = rememberTextMeasurer()
|
val textMeasurer = rememberTextMeasurer()
|
||||||
val dayTextStyle = remember { TextStyle(fontSize = 8.sp, lineHeight = 12.sp) }
|
val dayTextStyle = remember { TextStyle(fontSize = 8.sp, lineHeight = 12.sp) }
|
||||||
|
|
||||||
// 延迟文本测量到下一帧,避免首帧阻塞
|
// P0: 使用 remember 同步预测量所有可能的字符样式,消除 produceState 首帧空白/闪烁
|
||||||
val dayLayouts by produceState<Map<Int, androidx.compose.ui.text.TextLayoutResult>?>(null, textMeasurer, dayTextStyle, colors) {
|
val dayLayouts = remember(textMeasurer, dayTextStyle, colors) {
|
||||||
val days = 1..31
|
val days = 1..31
|
||||||
val colorTypes = listOf(0 to colors.day, 1 to colors.todayText, 2 to colors.otherMonth)
|
val colorTypes = listOf(0 to colors.day, 1 to colors.todayText, 2 to colors.otherMonth)
|
||||||
val map = HashMap<Int, androidx.compose.ui.text.TextLayoutResult>(32 * 3)
|
val map = HashMap<Int, androidx.compose.ui.text.TextLayoutResult>(32 * 3)
|
||||||
@ -115,10 +130,10 @@ fun YearGridView(
|
|||||||
map[d * 3 + type] = textMeasurer.measure(d.toString(), dayTextStyle.copy(color = c))
|
map[d * 3 + type] = textMeasurer.measure(d.toString(), dayTextStyle.copy(color = c))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
value = map
|
map
|
||||||
}
|
}
|
||||||
|
|
||||||
val titleLayouts by produceState<Map<Int, androidx.compose.ui.text.TextLayoutResult>?>(null, textMeasurer, colors) {
|
val titleLayouts = remember(textMeasurer, colors) {
|
||||||
val map = HashMap<Int, androidx.compose.ui.text.TextLayoutResult>(13 * 2)
|
val map = HashMap<Int, androidx.compose.ui.text.TextLayoutResult>(13 * 2)
|
||||||
(1..12).forEach { month ->
|
(1..12).forEach { month ->
|
||||||
val text = "${month}月"
|
val text = "${month}月"
|
||||||
@ -131,15 +146,19 @@ fun YearGridView(
|
|||||||
TextStyle(fontSize = 10.sp, color = colors.titleNormal)
|
TextStyle(fontSize = 10.sp, color = colors.titleNormal)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
value = map
|
map
|
||||||
}
|
}
|
||||||
|
|
||||||
val weekdayLayouts by produceState<Map<String, androidx.compose.ui.text.TextLayoutResult>?>(null, textMeasurer, colors) {
|
val weekdayLayouts = remember(textMeasurer, colors) {
|
||||||
value = WEEKDAY_LABELS.associateWith { label ->
|
WEEKDAY_LABELS.associateWith { label ->
|
||||||
textMeasurer.measure(label, TextStyle(fontSize = 8.sp, color = colors.weekday))
|
textMeasurer.measure(label, TextStyle(fontSize = 8.sp, color = colors.weekday))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||||
|
val gridRows = if (isLandscape) 3 else 4
|
||||||
|
val gridCols = if (isLandscape) 4 else 3
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@ -147,20 +166,22 @@ fun YearGridView(
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
// 4×3 月历网格
|
// 4×3 月历网格
|
||||||
|
// 弹性平分纵向空间,横竖屏自适应
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.padding(horizontal = 4.dp),
|
.padding(horizontal = 4.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
verticalArrangement = Arrangement.SpaceEvenly
|
||||||
) {
|
) {
|
||||||
(0 until 4).forEach { row ->
|
(0 until gridRows).forEach { row ->
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
(0 until 3).forEach { col ->
|
(0 until gridCols).forEach { col ->
|
||||||
val month = row * 3 + col + 1
|
val month = row * gridCols + col + 1
|
||||||
|
if (month <= 12) {
|
||||||
MiniMonth(
|
MiniMonth(
|
||||||
year = year,
|
year = year,
|
||||||
month = month,
|
month = month,
|
||||||
@ -179,6 +200,7 @@ fun YearGridView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
composeTraceEndSection()
|
composeTraceEndSection()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,9 +217,9 @@ private fun MiniMonth(
|
|||||||
today: LocalDate,
|
today: LocalDate,
|
||||||
days: List<MiniDayData>,
|
days: List<MiniDayData>,
|
||||||
colors: MiniMonthColors,
|
colors: MiniMonthColors,
|
||||||
dayLayouts: Map<Int, androidx.compose.ui.text.TextLayoutResult>?,
|
dayLayouts: Map<Int, androidx.compose.ui.text.TextLayoutResult>,
|
||||||
titleLayouts: Map<Int, androidx.compose.ui.text.TextLayoutResult>?,
|
titleLayouts: Map<Int, androidx.compose.ui.text.TextLayoutResult>,
|
||||||
weekdayLayouts: Map<String, androidx.compose.ui.text.TextLayoutResult>?,
|
weekdayLayouts: Map<String, androidx.compose.ui.text.TextLayoutResult>,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
@ -211,44 +233,78 @@ private fun MiniMonth(
|
|||||||
(titleHeightPx + titleToWeekdayGapPx + weekdayHeightPx + dayRowCount * dayCellHeightPx).toDp()
|
(titleHeightPx + titleToWeekdayGapPx + weekdayHeightPx + dayRowCount * dayCellHeightPx).toDp()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isPressed by remember { mutableStateOf(false) }
|
||||||
|
val pressedScale by androidx.compose.animation.core.animateFloatAsState(
|
||||||
|
targetValue = if (isPressed) 0.95f else 1.0f,
|
||||||
|
animationSpec = spring(stiffness = Spring.StiffnessHigh),
|
||||||
|
label = "mini_month_press_scale"
|
||||||
|
)
|
||||||
|
|
||||||
|
val containsToday = remember(days, today) {
|
||||||
|
days.any { it.isCurrentMonth && it.date == today }
|
||||||
|
}
|
||||||
|
val semanticsDesc = remember(year, month, containsToday) {
|
||||||
|
"$year 年 $month 月" + if (containsToday) ",包含今天" else ""
|
||||||
|
}
|
||||||
|
|
||||||
|
val containerShape = RoundedCornerShape(12.dp)
|
||||||
|
val selectedBgColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.15f)
|
||||||
|
val selectedBorderColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f)
|
||||||
|
|
||||||
|
val containerModifier = if (isSelected) {
|
||||||
|
Modifier
|
||||||
|
.background(color = selectedBgColor, shape = containerShape)
|
||||||
|
.border(width = 1.dp, color = selectedBorderColor, shape = containerShape)
|
||||||
|
} else Modifier
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.padding(2.dp)
|
.then(containerModifier)
|
||||||
|
.graphicsLayer {
|
||||||
|
scaleX = pressedScale
|
||||||
|
scaleY = pressedScale
|
||||||
|
}
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectTapGestures(
|
||||||
|
onPress = {
|
||||||
|
isPressed = true
|
||||||
|
tryAwaitRelease()
|
||||||
|
isPressed = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
.clickable(onClick = onClick)
|
.clickable(onClick = onClick)
|
||||||
.padding(vertical = 2.dp)
|
.padding(vertical = 4.dp, horizontal = 2.dp)
|
||||||
.semantics {
|
.semantics {
|
||||||
contentDescription = "$year 年 $month 月"
|
contentDescription = semanticsDesc
|
||||||
},
|
},
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Canvas(modifier = Modifier.fillMaxWidth().height(totalHeight)) {
|
Canvas(modifier = Modifier.fillMaxWidth().height(totalHeight)) {
|
||||||
val dl = dayLayouts ?: return@Canvas
|
|
||||||
val tl = titleLayouts ?: return@Canvas
|
|
||||||
val wl = weekdayLayouts ?: return@Canvas
|
|
||||||
val cellWidth = size.width / 7f
|
val cellWidth = size.width / 7f
|
||||||
|
|
||||||
// 1. 绘制标题
|
// 1. 绘制标题(像素对齐避免抗锯齿模糊)
|
||||||
val titleLayout = tl[month * 2 + (if (isSelected) 1 else 0)]!!
|
val titleLayout = titleLayouts[month * 2 + (if (isSelected) 1 else 0)]
|
||||||
|
if (titleLayout != null) {
|
||||||
|
val titleX = ((size.width - titleLayout.size.width) / 2f).roundToInt().toFloat()
|
||||||
drawText(
|
drawText(
|
||||||
textLayoutResult = titleLayout,
|
textLayoutResult = titleLayout,
|
||||||
topLeft = Offset(
|
topLeft = Offset(titleX, 0f)
|
||||||
(size.width - titleLayout.size.width) / 2f,
|
|
||||||
0f
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// 2. 绘制星期行
|
// 2. 绘制星期行
|
||||||
val weekdayY = titleHeightPx + titleToWeekdayGapPx
|
val weekdayY = titleHeightPx + titleToWeekdayGapPx
|
||||||
WEEKDAY_LABELS.forEachIndexed { i, label ->
|
WEEKDAY_LABELS.forEachIndexed { i, label ->
|
||||||
val layout = wl[label]!!
|
weekdayLayouts[label]?.let { layout ->
|
||||||
|
val x = (i * cellWidth + (cellWidth - layout.size.width) / 2f).roundToInt().toFloat()
|
||||||
|
val y = (weekdayY + (weekdayHeightPx - layout.size.height) / 2f).roundToInt().toFloat()
|
||||||
drawText(
|
drawText(
|
||||||
textLayoutResult = layout,
|
textLayoutResult = layout,
|
||||||
topLeft = Offset(
|
topLeft = Offset(x, y)
|
||||||
i * cellWidth + (cellWidth - layout.size.width) / 2f,
|
|
||||||
weekdayY + (weekdayHeightPx - layout.size.height) / 2f
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 3. 绘制日期网格
|
// 3. 绘制日期网格
|
||||||
val dayGridY = weekdayY + weekdayHeightPx
|
val dayGridY = weekdayY + weekdayHeightPx
|
||||||
@ -256,8 +312,8 @@ private fun MiniMonth(
|
|||||||
days.forEachIndexed { index, dayData ->
|
days.forEachIndexed { index, dayData ->
|
||||||
val row = index / 7
|
val row = index / 7
|
||||||
val col = index % 7
|
val col = index % 7
|
||||||
val centerX = col * cellWidth + cellWidth / 2f
|
val centerX = (col * cellWidth + cellWidth / 2f).roundToInt().toFloat()
|
||||||
val centerY = dayGridY + row * dayCellHeightPx + dayCellHeightPx / 2f
|
val centerY = (dayGridY + row * dayCellHeightPx + dayCellHeightPx / 2f).roundToInt().toFloat()
|
||||||
|
|
||||||
val isToday = dayData.date == today && dayData.isCurrentMonth
|
val isToday = dayData.date == today && dayData.isCurrentMonth
|
||||||
val dayNum = if (dayData.isCurrentMonth) dayData.date.day else 0
|
val dayNum = if (dayData.isCurrentMonth) dayData.date.day else 0
|
||||||
@ -266,14 +322,9 @@ private fun MiniMonth(
|
|||||||
isToday -> 1
|
isToday -> 1
|
||||||
else -> 0
|
else -> 0
|
||||||
}
|
}
|
||||||
val textColor: Color = when (colorType) {
|
|
||||||
2 -> colors.otherMonth
|
|
||||||
1 -> colors.todayText
|
|
||||||
else -> colors.day
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isToday) {
|
if (isToday) {
|
||||||
val radius = cellWidth.coerceAtMost(dayCellHeightPx) / 2f * 0.8f
|
val radius = (cellWidth.coerceAtMost(dayCellHeightPx) / 2f * 0.8f).roundToInt().toFloat()
|
||||||
drawCircle(
|
drawCircle(
|
||||||
color = colors.todayBg,
|
color = colors.todayBg,
|
||||||
radius = radius,
|
radius = radius,
|
||||||
@ -282,12 +333,12 @@ private fun MiniMonth(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (dayNum > 0) {
|
if (dayNum > 0) {
|
||||||
dl[dayNum * 3 + colorType]?.let { layoutResult ->
|
dayLayouts[dayNum * 3 + colorType]?.let { layoutResult ->
|
||||||
drawText(
|
drawText(
|
||||||
textLayoutResult = layoutResult,
|
textLayoutResult = layoutResult,
|
||||||
topLeft = Offset(
|
topLeft = Offset(
|
||||||
x = centerX - layoutResult.size.width / 2f,
|
x = (centerX - layoutResult.size.width / 2f).roundToInt().toFloat(),
|
||||||
y = centerY - layoutResult.size.height / 2f
|
y = (centerY - layoutResult.size.height / 2f).roundToInt().toFloat()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -341,14 +392,12 @@ fun YearHeader(
|
|||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
AnimatedContent(
|
AnimatedContent(
|
||||||
targetState = year,
|
targetState = year,
|
||||||
|
label = "year_header_title",
|
||||||
transitionSpec = {
|
transitionSpec = {
|
||||||
if (targetState > initialState) {
|
val isForward = targetState > initialState
|
||||||
slideInVertically(tween(250)) { -it } togetherWith
|
val slideIn = slideInVertically(tween(260)) { height -> if (isForward) -height else height } + fadeIn(tween(180))
|
||||||
slideOutVertically(tween(250)) { it }
|
val slideOut = slideOutVertically(tween(260)) { height -> if (isForward) height else -height } + fadeOut(tween(180))
|
||||||
} else {
|
slideIn togetherWith slideOut
|
||||||
slideInVertically(tween(250)) { it } togetherWith
|
|
||||||
slideOutVertically(tween(250)) { -it }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
) { y ->
|
) { y ->
|
||||||
Text(
|
Text(
|
||||||
@ -359,20 +408,42 @@ fun YearHeader(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
val showThisYear = year != currentYear
|
AnimatedVisibility(
|
||||||
val thisYearAlpha by animateFloatAsState(
|
visible = year != currentYear,
|
||||||
targetValue = if (showThisYear) 1f else 0f,
|
enter = fadeIn(tween(200)) + scaleIn(spring(stiffness = Spring.StiffnessMedium), initialScale = 0.7f),
|
||||||
animationSpec = tween(200)
|
exit = fadeOut(tween(150)) + scaleOut(targetScale = 0.8f),
|
||||||
)
|
label = "this_year_button_visibility"
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
onClick = { onYearChange(currentYear) },
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.4f)
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "今年",
|
text = "今年",
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
modifier = Modifier
|
fontWeight = FontWeight.Medium,
|
||||||
.graphicsLayer { alpha = thisYearAlpha }
|
modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp)
|
||||||
.clip(RoundedCornerShape(12.dp))
|
)
|
||||||
.clickable(enabled = showThisYear) { onYearChange(currentYear) }
|
}
|
||||||
.padding(horizontal = 10.dp, vertical = 4.dp)
|
}
|
||||||
|
}
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = year,
|
||||||
|
label = "year_header_lunar",
|
||||||
|
transitionSpec = {
|
||||||
|
val isForward = targetState > initialState
|
||||||
|
val slideIn = slideInVertically(tween(260)) { height -> if (isForward) -height else height } + fadeIn(tween(180))
|
||||||
|
val slideOut = slideOutVertically(tween(260)) { height -> if (isForward) height else -height } + fadeOut(tween(180))
|
||||||
|
slideIn togetherWith slideOut
|
||||||
|
},
|
||||||
|
modifier = Modifier.padding(top = 4.dp)
|
||||||
|
) { y ->
|
||||||
|
Text(
|
||||||
|
text = lunarYearLabel(y),
|
||||||
|
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.6f),
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
AnimatedContent(
|
AnimatedContent(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user