Compare commits
No commits in common. "0fbbdfa444d3256c9ad3cf2e7b9a14edd7ec77ee" and "e07ccd8b4ebe05e1a6dbfbadd8e7f9e6bdc5b3df" have entirely different histories.
0fbbdfa444
...
e07ccd8b4e
@ -80,25 +80,23 @@ class LunarCache(
|
|||||||
// 农历生日:每年正月二十一(tyme4kt 中正月 indexInYear = 0)
|
// 农历生日:每年正月二十一(tyme4kt 中正月 indexInYear = 0)
|
||||||
val isLunarBirthday = lunarMonth.getIndexInYear() == 0 && lunarDay.day == 21
|
val isLunarBirthday = lunarMonth.getIndexInYear() == 0 && lunarDay.day == 21
|
||||||
val isBirthday = isSolarBirthday || isLunarBirthday
|
val isBirthday = isSolarBirthday || isLunarBirthday
|
||||||
// 玫瑰日:每年 10 月 16 日
|
|
||||||
val isRoseDay = date.month.number == 10 && date.day == 16
|
|
||||||
|
|
||||||
// 农历传统节日(仅当天)
|
// 农历传统节日(仅当天)
|
||||||
val lunarFestival = lunarDay.getFestival()
|
val lunarFestival = lunarDay.getFestival()
|
||||||
if (lunarFestival != null) {
|
if (lunarFestival != null) {
|
||||||
return DayCellInfo(lunarFestival.getName(), true, holidayBadge, lunarMonthName, isBirthday, isRoseDay)
|
return DayCellInfo(lunarFestival.getName(), true, holidayBadge, lunarMonthName, isBirthday)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 节气(当天才显示)
|
// 节气(当天才显示)
|
||||||
val termDay = solarDay.getTermDay()
|
val termDay = solarDay.getTermDay()
|
||||||
if (termDay.getDayIndex() == 0) {
|
if (termDay.getDayIndex() == 0) {
|
||||||
return DayCellInfo(termDay.getSolarTerm().getName(), true, holidayBadge, lunarMonthName, isBirthday, isRoseDay)
|
return DayCellInfo(termDay.getSolarTerm().getName(), true, holidayBadge, lunarMonthName, isBirthday)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 公历节日(仅当天)
|
// 公历节日(仅当天)
|
||||||
val solarFestival = solarDay.getFestival()
|
val solarFestival = solarDay.getFestival()
|
||||||
if (solarFestival != null) {
|
if (solarFestival != null) {
|
||||||
return DayCellInfo(solarFestival.getName(), true, holidayBadge, lunarMonthName, isBirthday, isRoseDay)
|
return DayCellInfo(solarFestival.getName(), true, holidayBadge, lunarMonthName, isBirthday)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 默认:农历日期
|
// 默认:农历日期
|
||||||
@ -108,7 +106,7 @@ class LunarCache(
|
|||||||
} else {
|
} else {
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
return DayCellInfo(text, false, holidayBadge, lunarMonthName, isBirthday, isRoseDay)
|
return DayCellInfo(text, false, holidayBadge, lunarMonthName, isBirthday)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -124,13 +122,11 @@ class LunarCache(
|
|||||||
* @param isAnnotationHighlight 是否为高亮标注(节日/节气)
|
* @param isAnnotationHighlight 是否为高亮标注(节日/节气)
|
||||||
* @param holidayBadge 法定调休角标("班"/"休"/null)
|
* @param holidayBadge 法定调休角标("班"/"休"/null)
|
||||||
* @param isBirthday 是否为生日
|
* @param isBirthday 是否为生日
|
||||||
* @param isRoseDay 是否为玫瑰日(每年 10 月 16 日)
|
|
||||||
*/
|
*/
|
||||||
data class DayCellInfo(
|
data class DayCellInfo(
|
||||||
val annotationText: String,
|
val annotationText: String,
|
||||||
val isAnnotationHighlight: Boolean,
|
val isAnnotationHighlight: Boolean,
|
||||||
val holidayBadge: String?,
|
val holidayBadge: String?,
|
||||||
val lunarMonthName: String? = null,
|
val lunarMonthName: String? = null,
|
||||||
val isBirthday: Boolean = false,
|
val isBirthday: Boolean = false
|
||||||
val isRoseDay: Boolean = false
|
|
||||||
)
|
)
|
||||||
|
|||||||
@ -60,8 +60,7 @@ enum class DayCellState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 单个日期单元格,显示日期数字并支持选中/今天/非当月状态;玫瑰日日期左上角显示玫瑰图标,
|
* 单个日期单元格,显示日期数字并支持选中/今天/非当月状态;生日日期左上角显示金色皇冠。
|
||||||
* 生日日期左上角显示金色皇冠;若两者重合则优先显示玫瑰图标。
|
|
||||||
*
|
*
|
||||||
* @param date 日期
|
* @param date 日期
|
||||||
* @param isCurrentMonth 是否属于当前显示月份
|
* @param isCurrentMonth 是否属于当前显示月份
|
||||||
@ -160,16 +159,6 @@ private fun DayCellImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val isRoseDay = lunarData.isRoseDay
|
|
||||||
var roseClickTick by remember(date) { mutableIntStateOf(0) }
|
|
||||||
val roseScale = remember(date) { Animatable(1f) }
|
|
||||||
LaunchedEffect(roseClickTick) {
|
|
||||||
if (roseClickTick > 0) {
|
|
||||||
roseScale.animateTo(1.4f, spring(dampingRatio = Spring.DampingRatioMediumBouncy))
|
|
||||||
roseScale.animateTo(1f, spring(dampingRatio = Spring.DampingRatioMediumBouncy))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val annotationText = lunarData.annotationText
|
val annotationText = lunarData.annotationText
|
||||||
val isAnnotationHighlight = lunarData.isAnnotationHighlight
|
val isAnnotationHighlight = lunarData.isAnnotationHighlight
|
||||||
val holidayBadge = lunarData.holidayBadge
|
val holidayBadge = lunarData.holidayBadge
|
||||||
@ -332,11 +321,7 @@ private fun DayCellImpl(
|
|||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
indication = null,
|
indication = null,
|
||||||
onClick = {
|
onClick = {
|
||||||
if (isRoseDay) {
|
if (isBirthday) birthdayClickTick += 1
|
||||||
roseClickTick += 1
|
|
||||||
} else if (isBirthday) {
|
|
||||||
birthdayClickTick += 1
|
|
||||||
}
|
|
||||||
onClick()
|
onClick()
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
@ -388,24 +373,7 @@ private fun DayCellImpl(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isRoseDay) {
|
if (isBirthday) {
|
||||||
Icon(
|
|
||||||
painter = painterResource(R.drawable.ic_rose),
|
|
||||||
contentDescription = null,
|
|
||||||
tint = Color.Unspecified,
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.TopStart)
|
|
||||||
.zIndex(1f)
|
|
||||||
.padding(start = 2.dp, top = 2.dp)
|
|
||||||
.size(14.dp)
|
|
||||||
.graphicsLayer {
|
|
||||||
rotationZ = -45f
|
|
||||||
transformOrigin = TransformOrigin.Center
|
|
||||||
scaleX = roseScale.value
|
|
||||||
scaleY = roseScale.value
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else if (isBirthday) {
|
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(R.drawable.ic_birthday_crown),
|
painter = painterResource(R.drawable.ic_birthday_crown),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
|
|||||||
@ -1,50 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="72"
|
|
||||||
android:viewportHeight="72">
|
|
||||||
<path
|
|
||||||
android:pathData="M38.163 16.825s-5.19-.514-5.862 3.044 1.757 6.498 2.93 6.655c0 0-2.555 6.294 1.873 9.694s9.162 1.191 10.976-2.122c0 0 8.084.967 8.44-6.457 0 0-.965-6.424-5.866-6.293 0 0 1.363-6.977-3.898-8.471s-8.593 3.95-8.593 3.95"
|
|
||||||
android:fillColor="#EA5A47" />
|
|
||||||
<path
|
|
||||||
android:pathData="M25.93 32.603c3.991 4.886 2.804 12.462 2.804 12.462s-7.66-.346-11.653-5.232-2.805-12.462-2.805-12.462 7.661.35 11.653 5.232"
|
|
||||||
android:fillColor="#B1CC33" />
|
|
||||||
<path
|
|
||||||
android:pathData="M44.412 38.237c.91 1.976 1.874 3.717 3.497 4.563 1.708.89 3.683.676 5.128-.125 1.526-.847 2.655-2.51 2.697-4.595.036-1.762-1.106-3.524-2.148-5.403l-5.037 2.432zM51.952 20.074s.196-3.275-.304-4.074c0 0 1.142-1.674 3.088-1.317 1.482.272 2.332 3.318 2.653 4.755.112.503.288.723-.05 1.112-.545.627-1.803 1.805-2.96 2.058z"
|
|
||||||
android:fillColor="#D22F27" />
|
|
||||||
<path
|
|
||||||
android:pathData="M48.366 22.704c0 1.631-2.093 2.954-4.675 2.954s-4.675-1.323-4.675-2.954"
|
|
||||||
android:fillColor="#EA5A47" />
|
|
||||||
<path
|
|
||||||
android:pathData="M50.525 31.17c-.501.686-1.424 1.104-2.286 1.355a8.3 8.3 0 0 1-3.543.254 10 10 0 0 1-5.45-2.651 9.3 9.3 0 0 1-2.708-4.85q-.001 0-.014-.089a3.88 3.88 0 0 1 1.391-3.413c.656-.554 1.584-.882 2.476-1.004a8.3 8.3 0 0 1 3.542.269 9.84 9.84 0 0 1 4.96 3.418 9.15 9.15 0 0 1 1.898 5.188"
|
|
||||||
android:fillColor="#D22F27" />
|
|
||||||
<path
|
|
||||||
android:pathData="M39.016 22.839c-.002 1.612 1.917 2.667 4.515 2.778s4.759-1.12 4.828-2.75v-.315c-.02-1.632-2.142-2.974-4.74-2.998s-4.622 1.652-4.603 3.285"
|
|
||||||
android:fillColor="#EA5A47" />
|
|
||||||
<path
|
|
||||||
android:pathData="M43.174 24.818c-.61-1.417.329-3.184 2.098-3.948.918-.396 1.96-.354 2.875-.035 1.417.494 2.41 1.88 2.375 3.382a1.8 1.8 0 0 1-.103.606"
|
|
||||||
android:fillColor="#D22F27" />
|
|
||||||
<path
|
|
||||||
android:pathData="M43.174 23.83c-.61 1.416.329 3.184 2.098 3.947.918.397 1.96.354 2.875.035 1.417-.494 2.41-1.88 2.375-3.382a1.8 1.8 0 0 0-.103-.605"
|
|
||||||
android:fillColor="#D22F27" />
|
|
||||||
<path
|
|
||||||
android:pathData="M49.868 30.398c-.443.635-1.259 1.02-2.021 1.253a7.06 7.06 0 0 1-3.132.235 8.7 8.7 0 0 1-4.818-2.452 8.7 8.7 0 0 1-2.393-4.485M40.003 22.822c0-1.63 1.653-2.953 3.692-2.953s3.692 1.322 3.692 2.953"
|
|
||||||
android:strokeColor="#000000"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:strokeLineCap="round"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeMiterLimit="10" />
|
|
||||||
<path
|
|
||||||
android:pathData="M49.41 24.987c0 1.16-1.176 2.102-2.627 2.102-.753 0-1.461-.34-1.998-.834-.833-.764-1.107-2.018-.635-3.045.069-.15.152-.284.252-.388M53.308 15.903c.98-.624 2.405-.138 3.185 1.086s.617 2.722-.362 3.346M54.195 35.924a4.363 4.363 0 0 1-7.63 4.233M38.163 16.193c-2.253-.855-5.918 1.231-6.512 3.676-.57 2.341.9 6.214 3.153 7.069M50.867 20.834c5.416.628 6.396 6.461 5.802 8.774-.882 3.435-5.39 5.401-8.12 4.69M50.867 20.834a6.762 6.762 0 1 0-12.704-4.64M34.804 26.938a7.796 7.796 0 1 0 13.745 7.36"
|
|
||||||
android:strokeColor="#000000"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:strokeLineCap="round"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeMiterLimit="10" />
|
|
||||||
<path
|
|
||||||
android:pathData="M25.93 32.31c3.991 4.885 2.804 12.461 2.804 12.461s-7.66-.347-11.653-5.233-2.805-12.462-2.805-12.462 7.661.351 11.653 5.233M37.104 40.864c-3.438 3.548-7.676 10.246-5.829 20.735"
|
|
||||||
android:strokeColor="#000000"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:strokeLineCap="round"
|
|
||||||
android:strokeLineJoin="round" />
|
|
||||||
</vector>
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
package plus.rua.project
|
|
||||||
|
|
||||||
import kotlinx.coroutines.test.runTest
|
|
||||||
import kotlinx.datetime.LocalDate
|
|
||||||
import org.junit.Assert.assertFalse
|
|
||||||
import org.junit.Assert.assertTrue
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
class LunarCacheRoseDayTest {
|
|
||||||
private val cache = LunarCache()
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun october16_returnsTrue() = runTest {
|
|
||||||
val info = cache.getOrCompute(LocalDate(2026, 10, 16))
|
|
||||||
assertTrue("10 月 16 日应为玫瑰日", info.isRoseDay)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun september16_returnsFalse() = runTest {
|
|
||||||
val info = cache.getOrCompute(LocalDate(2026, 9, 16))
|
|
||||||
assertFalse("非 10 月的 16 日不应为玫瑰日", info.isRoseDay)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun regularDate_returnsFalse() = runTest {
|
|
||||||
val info = cache.getOrCompute(LocalDate(2026, 6, 15))
|
|
||||||
assertFalse("普通日期不应为玫瑰日", info.isRoseDay)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,94 +0,0 @@
|
|||||||
# 玫瑰日图标设计
|
|
||||||
|
|
||||||
## 背景
|
|
||||||
|
|
||||||
在日历单元格上为每年 **10 月 16 日** 增加一个小玫瑰图标,作为纪念日标识。实现方式参考已有的生日皇冠图标配置。
|
|
||||||
|
|
||||||
## 目标
|
|
||||||
|
|
||||||
- 每年 10 月 16 日的单元格左上角显示玫瑰图标。
|
|
||||||
- 与生日皇冠共用同一套渲染位置、尺寸和点击动画。
|
|
||||||
- 若 10 月 16 日同时命中生日,优先显示玫瑰图标。
|
|
||||||
- 不引入额外的无障碍文本,图标仅作为视觉装饰。
|
|
||||||
|
|
||||||
## 方案选择
|
|
||||||
|
|
||||||
采用**方案一:沿用生日模式做最小扩展**。理由:
|
|
||||||
|
|
||||||
- 与现有生日代码完全同构,改动最小;
|
|
||||||
- 符合用户“参考已有生日的配置”的要求;
|
|
||||||
- 可读性最好,降低维护者理解成本。
|
|
||||||
|
|
||||||
## 详细设计
|
|
||||||
|
|
||||||
### 数据层
|
|
||||||
|
|
||||||
文件:`core/src/main/kotlin/plus/rua/project/LunarCache.kt`
|
|
||||||
|
|
||||||
在 `DayCellInfo` 中新增字段:
|
|
||||||
|
|
||||||
```kotlin
|
|
||||||
data class DayCellInfo(
|
|
||||||
val annotationText: String,
|
|
||||||
val isAnnotationHighlight: Boolean,
|
|
||||||
val holidayBadge: String?,
|
|
||||||
val lunarMonthName: String? = null,
|
|
||||||
val isBirthday: Boolean = false,
|
|
||||||
val isRoseDay: Boolean = false,
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
在 `LunarCache.compute()` 中加入玫瑰日判断:
|
|
||||||
|
|
||||||
```kotlin
|
|
||||||
val isRoseDay = date.month.number == 10 && date.day == 16
|
|
||||||
```
|
|
||||||
|
|
||||||
并将 `isRoseDay` 传入所有 `DayCellInfo(...)` 构造分支。
|
|
||||||
|
|
||||||
### UI 层
|
|
||||||
|
|
||||||
文件:`core/src/main/kotlin/plus/rua/project/ui/DayCell.kt`
|
|
||||||
|
|
||||||
在 `DayCellImpl` 中:
|
|
||||||
|
|
||||||
1. 读取 `val isRoseDay = lunarData.isRoseDay`。
|
|
||||||
2. 新增 `roseClickTick` 与 `roseScale` 状态。
|
|
||||||
3. 在 `clickable` 的 `onClick` 中:若 `isRoseDay` 则触发 `roseClickTick += 1`,随后调用外部 `onClick()`。
|
|
||||||
4. 在 `Box` 的左上角优先渲染玫瑰图标:
|
|
||||||
- 若 `isRoseDay` 为 `true`,显示 `R.drawable.ic_rose`;
|
|
||||||
- 否则若 `isBirthday` 为 `true`,显示 `R.drawable.ic_birthday_crown`。
|
|
||||||
5. 图标尺寸、内边距与皇冠保持一致:
|
|
||||||
- `size(14.dp)`
|
|
||||||
- `padding(start = 2.dp, top = 2.dp)`
|
|
||||||
- `rotationZ = -45f`
|
|
||||||
- 点击时通过 `graphicsLayer` 的 `scaleX / scaleY` 播放弹跳动画。
|
|
||||||
|
|
||||||
### 资源
|
|
||||||
|
|
||||||
- 源文件:`~/Pictures/rose-svgrepo-com.svg`
|
|
||||||
- 目标路径:`core/src/main/res/drawable/ic_rose.xml`
|
|
||||||
- 转换方式:将 SVG 转为 Android VectorDrawable,保留原始填充色,渲染时使用 `Color.Unspecified`。
|
|
||||||
|
|
||||||
### 测试
|
|
||||||
|
|
||||||
新增文件:`core/src/test/kotlin/plus/rua/project/LunarCacheRoseDayTest.kt`
|
|
||||||
|
|
||||||
覆盖以下场景:
|
|
||||||
|
|
||||||
1. `LocalDate(2026, 10, 16)` 的 `isRoseDay` 为 `true`。
|
|
||||||
2. 普通日期(如 `LocalDate(2026, 6, 15)`)的 `isRoseDay` 为 `false`。
|
|
||||||
3. 非 10 月的 16 日(如 `LocalDate(2026, 9, 16)`)的 `isRoseDay` 为 `false`。
|
|
||||||
|
|
||||||
## 改动文件
|
|
||||||
|
|
||||||
- `core/src/main/kotlin/plus/rua/project/LunarCache.kt`
|
|
||||||
- `core/src/main/kotlin/plus/rua/project/ui/DayCell.kt`
|
|
||||||
- `core/src/main/res/drawable/ic_rose.xml`(新增)
|
|
||||||
- `core/src/test/kotlin/plus/rua/project/LunarCacheRoseDayTest.kt`(新增)
|
|
||||||
|
|
||||||
## 风险与注意事项
|
|
||||||
|
|
||||||
- 图标与生日皇冠同位置,需确保 `isRoseDay` 判断在前,避免被生日皇冠覆盖。
|
|
||||||
- SVG 转换后需检查 `viewportWidth` / `viewportHeight` 与 `pathData` 是否正确。
|
|
||||||
- 新增字段后 `DayCellInfo` 的 `copy` / 解构等用法不受影响。
|
|
||||||
Loading…
x
Reference in New Issue
Block a user