Merge branch 'feature-birthday-crown'
This commit is contained in:
commit
5bf8a85f7a
@ -75,8 +75,11 @@ class LunarCache(
|
||||
val lunarDay = solarDay.getLunarDay()
|
||||
val lunarMonth = lunarDay.getLunarMonth()
|
||||
val lunarMonthName = lunarMonth.getName()
|
||||
val isBirthday = (date.month.number == 9 && date.day == 4) ||
|
||||
(lunarDay.getLunarMonth().getIndexInYear() == 0 && lunarDay.day == 21)
|
||||
// 阳历生日:每年 9 月 4 日
|
||||
val isSolarBirthday = date.month.number == 9 && date.day == 4
|
||||
// 农历生日:每年正月二十一(tyme4kt 中正月 indexInYear = 0)
|
||||
val isLunarBirthday = lunarMonth.getIndexInYear() == 0 && lunarDay.day == 21
|
||||
val isBirthday = isSolarBirthday || isLunarBirthday
|
||||
|
||||
// 农历传统节日(仅当天)
|
||||
val lunarFestival = lunarDay.getFestival()
|
||||
@ -118,6 +121,7 @@ class LunarCache(
|
||||
* @param annotationText 底部标注文字(农历/节气/节日)
|
||||
* @param isAnnotationHighlight 是否为高亮标注(节日/节气)
|
||||
* @param holidayBadge 法定调休角标("班"/"休"/null)
|
||||
* @param isBirthday 是否为生日
|
||||
*/
|
||||
data class DayCellInfo(
|
||||
val annotationText: String,
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
package plus.rua.project.ui
|
||||
|
||||
import androidx.compose.animation.animateColor
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.core.updateTransition
|
||||
import androidx.compose.ui.graphics.TransformOrigin
|
||||
@ -16,14 +19,19 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@ -31,6 +39,7 @@ import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@ -44,13 +53,14 @@ import kotlinx.datetime.number
|
||||
import plus.rua.project.DayCellInfo
|
||||
import plus.rua.project.LunarCache
|
||||
import plus.rua.project.ShiftKind
|
||||
import plus.rua.project.shared.R
|
||||
|
||||
enum class DayCellState {
|
||||
NORMAL, OTHER_MONTH, TODAY, SELECTED, SELECTED_TODAY
|
||||
}
|
||||
|
||||
/**
|
||||
* 单个日期单元格,显示日期数字并支持选中/今天/非当月状态。
|
||||
* 单个日期单元格,显示日期数字并支持选中/今天/非当月状态;生日日期左上角显示金色皇冠。
|
||||
*
|
||||
* @param date 日期
|
||||
* @param isCurrentMonth 是否属于当前显示月份
|
||||
@ -63,6 +73,9 @@ enum class DayCellState {
|
||||
* @param holidayEdgeInfo 假日在连续序列中的边缘状态,决定背景圆角。null 表示无假日。
|
||||
* @param cellIndex 单元格在月网格中的线性索引(weekIndex*7+dayIndex),用于法定假日波浪动画延迟。
|
||||
* @param onClick 点击回调
|
||||
* @param interactionSource 点击交互源
|
||||
* @param lunarCache 农历缓存实例
|
||||
* @param lunarData 预计算的日期信息;null 时内部自动获取
|
||||
* @param modifier 外部布局修饰符
|
||||
*/
|
||||
@Composable
|
||||
@ -76,10 +89,10 @@ fun DayCell(
|
||||
holidayEdgeInfo: HolidayEdgeInfo? = null,
|
||||
cellIndex: Int = 0,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
lunarCache: LunarCache = LunarCache.default,
|
||||
lunarData: DayCellInfo? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (lunarData != null) {
|
||||
DayCellImpl(
|
||||
@ -92,9 +105,9 @@ fun DayCell(
|
||||
holidayEdgeInfo = holidayEdgeInfo,
|
||||
cellIndex = cellIndex,
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
interactionSource = interactionSource,
|
||||
lunarData = lunarData,
|
||||
modifier = modifier,
|
||||
)
|
||||
} else {
|
||||
val computed by produceState(
|
||||
@ -114,9 +127,9 @@ fun DayCell(
|
||||
holidayEdgeInfo = holidayEdgeInfo,
|
||||
cellIndex = cellIndex,
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
interactionSource = interactionSource,
|
||||
lunarData = computed,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -132,10 +145,20 @@ private fun DayCellImpl(
|
||||
holidayEdgeInfo: HolidayEdgeInfo?,
|
||||
cellIndex: Int,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier,
|
||||
interactionSource: MutableInteractionSource,
|
||||
lunarData: DayCellInfo,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
val isBirthday = lunarData.isBirthday
|
||||
var birthdayClickTick by remember(date) { mutableIntStateOf(0) }
|
||||
val crownScale = remember(date) { Animatable(1f) }
|
||||
LaunchedEffect(birthdayClickTick) {
|
||||
if (birthdayClickTick > 0) {
|
||||
crownScale.animateTo(1.4f, spring(dampingRatio = Spring.DampingRatioMediumBouncy))
|
||||
crownScale.animateTo(1f, spring(dampingRatio = Spring.DampingRatioMediumBouncy))
|
||||
}
|
||||
}
|
||||
|
||||
val annotationText = lunarData.annotationText
|
||||
val isAnnotationHighlight = lunarData.isAnnotationHighlight
|
||||
val holidayBadge = lunarData.holidayBadge
|
||||
@ -266,10 +289,11 @@ private fun DayCellImpl(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.semantics {
|
||||
val birthdaySuffix = if (isBirthday) ",生日" else ""
|
||||
contentDescription = if (isToday) {
|
||||
"今天 ${date.year}年${date.month.number}月${date.day}日"
|
||||
"今天 ${date.year}年${date.month.number}月${date.day}日$birthdaySuffix"
|
||||
} else {
|
||||
"${date.year}年${date.month.number}月${date.day}日"
|
||||
"${date.year}年${date.month.number}月${date.day}日$birthdaySuffix"
|
||||
}
|
||||
}
|
||||
.clip(CircleShape)
|
||||
@ -296,7 +320,10 @@ private fun DayCellImpl(
|
||||
.clickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = null,
|
||||
onClick = onClick
|
||||
onClick = {
|
||||
if (isBirthday) birthdayClickTick += 1
|
||||
onClick()
|
||||
}
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
@ -346,5 +373,23 @@ private fun DayCellImpl(
|
||||
)
|
||||
}
|
||||
}
|
||||
if (isBirthday) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_birthday_crown),
|
||||
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 = crownScale.value
|
||||
scaleY = crownScale.value
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
30
core/src/main/res/drawable/ic_birthday_crown.xml
Normal file
30
core/src/main/res/drawable/ic_birthday_crown.xml
Normal file
@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="512"
|
||||
android:viewportHeight="512">
|
||||
<path
|
||||
android:fillColor="#FFC033"
|
||||
android:pathData="M461.913,456.348H50.087c-9.223,0-16.696-7.473-16.696-16.696V372.87c0-9.223,7.473-16.696,16.696-16.696h411.826c9.223,0,16.696,7.473,16.696,16.696v66.783C478.609,448.875,471.136,456.348,461.913,456.348z" />
|
||||
<path
|
||||
android:fillColor="#FFE14D"
|
||||
android:pathData="M478.609,389.565H33.391V72.348c0-6.957,4.31-13.185,10.821-15.63c6.527-2.424,13.859-0.598,18.44,4.636l102.511,117.152l76.946-115.418c6.608-9.913,21.175-9.913,27.783,0l76.946,115.418L449.349,61.353c4.587-5.234,11.935-7.06,18.44-4.636c6.51,2.445,10.82,8.674,10.82,15.63V389.565z" />
|
||||
<path
|
||||
android:fillColor="#F37B2A"
|
||||
android:pathData="M256,322.783c-27.619,0-50.087-22.468-50.087-50.087s22.468-50.087,50.087-50.087s50.087,22.468,50.087,50.087S283.619,322.783,256,322.783z" />
|
||||
<path
|
||||
android:fillColor="#F37B2A"
|
||||
android:pathData="M50.087,322.783C22.468,322.783,0,300.315,0,272.696s22.468-50.087,50.087-50.087s50.087,22.468,50.087,50.087S77.706,322.783,50.087,322.783z" />
|
||||
<path
|
||||
android:fillColor="#F9A926"
|
||||
android:pathData="M461.913,356.174H256v100.174h205.913c9.223,0,16.696-7.473,16.696-16.696V372.87C478.609,363.647,471.136,356.174,461.913,356.174z" />
|
||||
<path
|
||||
android:fillColor="#FFCC33"
|
||||
android:pathData="M478.609,389.565V72.348c0-6.957-4.31-13.185-10.821-15.63c-6.506-2.424-13.853-0.598-18.44,4.636L346.837,178.505L269.891,63.087c-3.304-4.957-8.597-7.435-13.891-7.435v333.913H478.609z" />
|
||||
<path
|
||||
android:fillColor="#E56722"
|
||||
android:pathData="M306.087,272.696c0-27.619-22.468-50.087-50.087-50.087v100.174C283.619,322.783,306.087,300.315,306.087,272.696z" />
|
||||
<path
|
||||
android:fillColor="#E56722"
|
||||
android:pathData="M461.913,322.783c-27.619,0-50.087-22.468-50.087-50.087s22.468-50.087,50.087-50.087S512,245.077,512,272.696S489.532,322.783,461.913,322.783z" />
|
||||
</vector>
|
||||
@ -27,16 +27,4 @@ class LunarCacheBirthdayTest {
|
||||
val info = cache.getOrCompute(LocalDate(2026, 6, 15))
|
||||
assertFalse("普通日期不应为生日", info.isBirthday)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun solarBirthdayNotFirstLunarDay21_stillReturnsTrue() = runTest {
|
||||
val info = cache.getOrCompute(LocalDate(2026, 9, 4))
|
||||
assertTrue(info.isBirthday)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun lunarBirthdayNotSeptember4_stillReturnsTrue() = runTest {
|
||||
val info = cache.getOrCompute(LocalDate(2026, 3, 9))
|
||||
assertTrue(info.isBirthday)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user