Replace year view month cells with mini calendar grids

Each month in the 4x3 year grid now shows a compact calendar with
day numbers, matching the iOS Calendar year view style. Today is
highlighted with a filled circle. Selected month title uses primary color.
This commit is contained in:
meyou 2026-05-16 16:35:56 +08:00
parent 731a1bb6a1
commit c996d026cc
2 changed files with 107 additions and 34 deletions

View File

@ -246,6 +246,7 @@ fun CalendarMonthView(
YearGridView( YearGridView(
year = viewModel.yearViewYear, year = viewModel.yearViewYear,
selectedMonth = if (viewModel.yearViewYear == currentYear) currentMonth else 0, selectedMonth = if (viewModel.yearViewYear == currentYear) currentMonth else 0,
today = today,
onMonthClick = { month -> onMonthClick = { month ->
viewModel.selectMonthFromYearView(month) viewModel.selectMonthFromYearView(month)
// 同步 CalendarPager 到目标月份 // 同步 CalendarPager 到目标月份

View File

@ -5,8 +5,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -14,6 +12,7 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
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.draw.clip
@ -23,9 +22,18 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
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 kotlinx.datetime.DatePeriod
import kotlinx.datetime.LocalDate
import kotlinx.datetime.minus
import kotlinx.datetime.number
import kotlinx.datetime.plus
import kotlinx.datetime.todayIn
/** /**
* 年度网格视图显示 4×3 月份网格支持年份切换 * 年度网格视图显示 4×3 精简月历网格支持年份切换
*
* 每格显示一个精简版月历月份标题 + 日期数字网格
* 选中月份高亮点击进入该月
* *
* @param year 显示的年份 * @param year 显示的年份
* @param selectedMonth 当前选中月份1-12 * @param selectedMonth 当前选中月份1-12
@ -37,6 +45,7 @@ import androidx.compose.ui.unit.sp
fun YearGridView( fun YearGridView(
year: Int, year: Int,
selectedMonth: Int, selectedMonth: Int,
today: LocalDate,
onMonthClick: (Int) -> Unit, onMonthClick: (Int) -> Unit,
onYearChange: (Int) -> Unit, onYearChange: (Int) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
@ -62,13 +71,13 @@ fun YearGridView(
.clickable { onYearChange(year - 1) } .clickable { onYearChange(year - 1) }
.padding(horizontal = 16.dp, vertical = 8.dp) .padding(horizontal = 16.dp, vertical = 8.dp)
) )
Spacer(modifier = Modifier.weight(1f)) Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.Center) {
Text( Text(
text = "${year}", text = "${year}",
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
Spacer(modifier = Modifier.weight(1f)) }
Text( Text(
text = "", text = "",
fontSize = 24.sp, fontSize = 24.sp,
@ -81,12 +90,12 @@ fun YearGridView(
) )
} }
// 4×3 月网格 // 4×3 月网格
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.weight(1f) .weight(1f)
.padding(horizontal = 16.dp), .padding(horizontal = 8.dp),
verticalArrangement = Arrangement.SpaceEvenly verticalArrangement = Arrangement.SpaceEvenly
) { ) {
(0 until 4).forEach { row -> (0 until 4).forEach { row ->
@ -96,9 +105,11 @@ fun YearGridView(
) { ) {
(0 until 3).forEach { col -> (0 until 3).forEach { col ->
val month = row * 3 + col + 1 val month = row * 3 + col + 1
MonthCell( MiniMonth(
year = year,
month = month, month = month,
isSelected = month == selectedMonth, isSelected = month == selectedMonth,
today = today,
onClick = { onMonthClick(month) }, onClick = { onMonthClick(month) },
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
@ -110,45 +121,106 @@ fun YearGridView(
} }
/** /**
* 年视图中的月份单元 * 精简版月历月份标题 + 日期数字网
*/ */
@Composable @Composable
private fun MonthCell( private fun MiniMonth(
year: Int,
month: Int, month: Int,
isSelected: Boolean, isSelected: Boolean,
today: LocalDate,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val contentColor = if (isSelected) { val days = remember(year, month) { generateMiniMonthDays(year, month) }
MaterialTheme.colorScheme.onPrimary val titleColor = if (isSelected) {
MaterialTheme.colorScheme.primary
} else { } else {
MaterialTheme.colorScheme.onSurface MaterialTheme.colorScheme.onSurface
} }
val bgColor = MaterialTheme.colorScheme.primary val dayColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
val otherMonthColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.2f)
val todayBgColor = MaterialTheme.colorScheme.primary
Box( Column(
modifier = modifier modifier = modifier
.aspectRatio(1f) .padding(2.dp)
.padding(6.dp)
.clip(CircleShape) .clip(CircleShape)
.clickable(onClick = onClick)
.padding(vertical = 4.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// 月份标题
Text(
text = "${month}",
color = titleColor,
fontSize = 10.sp,
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
textAlign = TextAlign.Center
)
// 日期网格
days.chunked(7).forEach { week ->
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
week.forEach { dayData ->
val isToday = dayData.date == today
val color = when {
!dayData.isCurrentMonth -> otherMonthColor
isToday -> MaterialTheme.colorScheme.primary
else -> dayColor
}
Box(
contentAlignment = Alignment.Center
) {
if (isToday) {
Box(
modifier = Modifier
.drawBehind { .drawBehind {
if (isSelected) {
drawCircle( drawCircle(
color = bgColor, color = todayBgColor,
radius = size.minDimension / 2f, radius = size.minDimension / 2f,
center = Offset(size.width / 2f, size.height / 2f) center = Offset(size.width / 2f, size.height / 2f)
) )
} }
.clip(CircleShape)
)
} }
.clickable(onClick = onClick),
contentAlignment = Alignment.Center
) {
Text( Text(
text = "${month}", text = if (dayData.isCurrentMonth) dayData.date.day.toString() else "",
color = contentColor, color = if (isToday) MaterialTheme.colorScheme.onPrimary else color,
fontSize = 16.sp, fontSize = 7.sp,
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal, textAlign = TextAlign.Center,
textAlign = TextAlign.Center lineHeight = 10.sp
)
}
}
}
}
}
}
private data class MiniDayData(
val date: LocalDate,
val isCurrentMonth: Boolean
)
@Suppress("DEPRECATION") // monthNumber 无替代 API
private fun generateMiniMonthDays(year: Int, month: Int): List<MiniDayData> {
val firstOfMonth = LocalDate(year, month, 1)
val offset = firstOfMonth.dayOfWeek.ordinal
val startDate = firstOfMonth.minus(DatePeriod(days = offset))
val nextMonth = if (month == 12) LocalDate(year + 1, 1, 1) else LocalDate(year, month + 1, 1)
val daysInMonth = nextMonth.minus(DatePeriod(days = 1)).day
val rows = ((offset + daysInMonth - 1) / 7) + 1
val totalDays = rows * 7
return (0 until totalDays).map { i ->
val date = startDate.plus(DatePeriod(days = i))
MiniDayData(
date = date,
isCurrentMonth = date.month.number == month && date.year == year
) )
} }
} }