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(
year = viewModel.yearViewYear,
selectedMonth = if (viewModel.yearViewYear == currentYear) currentMonth else 0,
today = today,
onMonthClick = { month ->
viewModel.selectMonthFromYearView(month)
// 同步 CalendarPager 到目标月份

View File

@ -5,8 +5,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
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.fillMaxWidth
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.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.unit.dp
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 selectedMonth 当前选中月份1-12
@ -37,6 +45,7 @@ import androidx.compose.ui.unit.sp
fun YearGridView(
year: Int,
selectedMonth: Int,
today: LocalDate,
onMonthClick: (Int) -> Unit,
onYearChange: (Int) -> Unit,
modifier: Modifier = Modifier
@ -62,13 +71,13 @@ fun YearGridView(
.clickable { onYearChange(year - 1) }
.padding(horizontal = 16.dp, vertical = 8.dp)
)
Spacer(modifier = Modifier.weight(1f))
Text(
text = "${year}",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.weight(1f))
Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.Center) {
Text(
text = "${year}",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
}
Text(
text = "",
fontSize = 24.sp,
@ -81,12 +90,12 @@ fun YearGridView(
)
}
// 4×3 月网格
// 4×3 月网格
Column(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.padding(horizontal = 16.dp),
.padding(horizontal = 8.dp),
verticalArrangement = Arrangement.SpaceEvenly
) {
(0 until 4).forEach { row ->
@ -96,9 +105,11 @@ fun YearGridView(
) {
(0 until 3).forEach { col ->
val month = row * 3 + col + 1
MonthCell(
MiniMonth(
year = year,
month = month,
isSelected = month == selectedMonth,
today = today,
onClick = { onMonthClick(month) },
modifier = Modifier.weight(1f)
)
@ -110,45 +121,106 @@ fun YearGridView(
}
/**
* 年视图中的月份单元
* 精简版月历月份标题 + 日期数字网
*/
@Composable
private fun MonthCell(
private fun MiniMonth(
year: Int,
month: Int,
isSelected: Boolean,
today: LocalDate,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val contentColor = if (isSelected) {
MaterialTheme.colorScheme.onPrimary
val days = remember(year, month) { generateMiniMonthDays(year, month) }
val titleColor = if (isSelected) {
MaterialTheme.colorScheme.primary
} else {
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
.aspectRatio(1f)
.padding(6.dp)
.padding(2.dp)
.clip(CircleShape)
.drawBehind {
if (isSelected) {
drawCircle(
color = bgColor,
radius = size.minDimension / 2f,
center = Offset(size.width / 2f, size.height / 2f)
)
}
}
.clickable(onClick = onClick),
contentAlignment = Alignment.Center
.clickable(onClick = onClick)
.padding(vertical = 4.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// 月份标题
Text(
text = "${month}",
color = contentColor,
fontSize = 16.sp,
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 {
drawCircle(
color = todayBgColor,
radius = size.minDimension / 2f,
center = Offset(size.width / 2f, size.height / 2f)
)
}
.clip(CircleShape)
)
}
Text(
text = if (dayData.isCurrentMonth) dayData.date.day.toString() else "",
color = if (isToday) MaterialTheme.colorScheme.onPrimary else color,
fontSize = 7.sp,
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
)
}
}