Merge branch 'feature-birthday-crown'
This commit is contained in:
commit
5bf8a85f7a
@ -75,8 +75,11 @@ class LunarCache(
|
|||||||
val lunarDay = solarDay.getLunarDay()
|
val lunarDay = solarDay.getLunarDay()
|
||||||
val lunarMonth = lunarDay.getLunarMonth()
|
val lunarMonth = lunarDay.getLunarMonth()
|
||||||
val lunarMonthName = lunarMonth.getName()
|
val lunarMonthName = lunarMonth.getName()
|
||||||
val isBirthday = (date.month.number == 9 && date.day == 4) ||
|
// 阳历生日:每年 9 月 4 日
|
||||||
(lunarDay.getLunarMonth().getIndexInYear() == 0 && lunarDay.day == 21)
|
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()
|
val lunarFestival = lunarDay.getFestival()
|
||||||
@ -118,6 +121,7 @@ class LunarCache(
|
|||||||
* @param annotationText 底部标注文字(农历/节气/节日)
|
* @param annotationText 底部标注文字(农历/节气/节日)
|
||||||
* @param isAnnotationHighlight 是否为高亮标注(节日/节气)
|
* @param isAnnotationHighlight 是否为高亮标注(节日/节气)
|
||||||
* @param holidayBadge 法定调休角标("班"/"休"/null)
|
* @param holidayBadge 法定调休角标("班"/"休"/null)
|
||||||
|
* @param isBirthday 是否为生日
|
||||||
*/
|
*/
|
||||||
data class DayCellInfo(
|
data class DayCellInfo(
|
||||||
val annotationText: String,
|
val annotationText: String,
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
package plus.rua.project.ui
|
package plus.rua.project.ui
|
||||||
|
|
||||||
import androidx.compose.animation.animateColor
|
import androidx.compose.animation.animateColor
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||||
|
import androidx.compose.animation.core.Spring
|
||||||
import androidx.compose.animation.core.animateFloat
|
import androidx.compose.animation.core.animateFloat
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.core.updateTransition
|
import androidx.compose.animation.core.updateTransition
|
||||||
import androidx.compose.ui.graphics.TransformOrigin
|
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.aspectRatio
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
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.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
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.draw.clip
|
||||||
@ -31,6 +39,7 @@ import androidx.compose.ui.draw.drawBehind
|
|||||||
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.drawscope.Stroke
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.semantics.contentDescription
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
@ -44,13 +53,14 @@ import kotlinx.datetime.number
|
|||||||
import plus.rua.project.DayCellInfo
|
import plus.rua.project.DayCellInfo
|
||||||
import plus.rua.project.LunarCache
|
import plus.rua.project.LunarCache
|
||||||
import plus.rua.project.ShiftKind
|
import plus.rua.project.ShiftKind
|
||||||
|
import plus.rua.project.shared.R
|
||||||
|
|
||||||
enum class DayCellState {
|
enum class DayCellState {
|
||||||
NORMAL, OTHER_MONTH, TODAY, SELECTED, SELECTED_TODAY
|
NORMAL, OTHER_MONTH, TODAY, SELECTED, SELECTED_TODAY
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 单个日期单元格,显示日期数字并支持选中/今天/非当月状态。
|
* 单个日期单元格,显示日期数字并支持选中/今天/非当月状态;生日日期左上角显示金色皇冠。
|
||||||
*
|
*
|
||||||
* @param date 日期
|
* @param date 日期
|
||||||
* @param isCurrentMonth 是否属于当前显示月份
|
* @param isCurrentMonth 是否属于当前显示月份
|
||||||
@ -63,6 +73,9 @@ enum class DayCellState {
|
|||||||
* @param holidayEdgeInfo 假日在连续序列中的边缘状态,决定背景圆角。null 表示无假日。
|
* @param holidayEdgeInfo 假日在连续序列中的边缘状态,决定背景圆角。null 表示无假日。
|
||||||
* @param cellIndex 单元格在月网格中的线性索引(weekIndex*7+dayIndex),用于法定假日波浪动画延迟。
|
* @param cellIndex 单元格在月网格中的线性索引(weekIndex*7+dayIndex),用于法定假日波浪动画延迟。
|
||||||
* @param onClick 点击回调
|
* @param onClick 点击回调
|
||||||
|
* @param interactionSource 点击交互源
|
||||||
|
* @param lunarCache 农历缓存实例
|
||||||
|
* @param lunarData 预计算的日期信息;null 时内部自动获取
|
||||||
* @param modifier 外部布局修饰符
|
* @param modifier 外部布局修饰符
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
@ -76,10 +89,10 @@ fun DayCell(
|
|||||||
holidayEdgeInfo: HolidayEdgeInfo? = null,
|
holidayEdgeInfo: HolidayEdgeInfo? = null,
|
||||||
cellIndex: Int = 0,
|
cellIndex: Int = 0,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
lunarCache: LunarCache = LunarCache.default,
|
lunarCache: LunarCache = LunarCache.default,
|
||||||
lunarData: DayCellInfo? = null,
|
lunarData: DayCellInfo? = null,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
if (lunarData != null) {
|
if (lunarData != null) {
|
||||||
DayCellImpl(
|
DayCellImpl(
|
||||||
@ -92,9 +105,9 @@ fun DayCell(
|
|||||||
holidayEdgeInfo = holidayEdgeInfo,
|
holidayEdgeInfo = holidayEdgeInfo,
|
||||||
cellIndex = cellIndex,
|
cellIndex = cellIndex,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = modifier,
|
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
lunarData = lunarData,
|
lunarData = lunarData,
|
||||||
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
val computed by produceState(
|
val computed by produceState(
|
||||||
@ -114,9 +127,9 @@ fun DayCell(
|
|||||||
holidayEdgeInfo = holidayEdgeInfo,
|
holidayEdgeInfo = holidayEdgeInfo,
|
||||||
cellIndex = cellIndex,
|
cellIndex = cellIndex,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = modifier,
|
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
lunarData = computed,
|
lunarData = computed,
|
||||||
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -132,10 +145,20 @@ private fun DayCellImpl(
|
|||||||
holidayEdgeInfo: HolidayEdgeInfo?,
|
holidayEdgeInfo: HolidayEdgeInfo?,
|
||||||
cellIndex: Int,
|
cellIndex: Int,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier,
|
|
||||||
interactionSource: MutableInteractionSource,
|
interactionSource: MutableInteractionSource,
|
||||||
lunarData: DayCellInfo,
|
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 annotationText = lunarData.annotationText
|
||||||
val isAnnotationHighlight = lunarData.isAnnotationHighlight
|
val isAnnotationHighlight = lunarData.isAnnotationHighlight
|
||||||
val holidayBadge = lunarData.holidayBadge
|
val holidayBadge = lunarData.holidayBadge
|
||||||
@ -266,10 +289,11 @@ private fun DayCellImpl(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.semantics {
|
.semantics {
|
||||||
|
val birthdaySuffix = if (isBirthday) ",生日" else ""
|
||||||
contentDescription = if (isToday) {
|
contentDescription = if (isToday) {
|
||||||
"今天 ${date.year}年${date.month.number}月${date.day}日"
|
"今天 ${date.year}年${date.month.number}月${date.day}日$birthdaySuffix"
|
||||||
} else {
|
} else {
|
||||||
"${date.year}年${date.month.number}月${date.day}日"
|
"${date.year}年${date.month.number}月${date.day}日$birthdaySuffix"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
@ -296,7 +320,10 @@ private fun DayCellImpl(
|
|||||||
.clickable(
|
.clickable(
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
indication = null,
|
indication = null,
|
||||||
onClick = onClick
|
onClick = {
|
||||||
|
if (isBirthday) birthdayClickTick += 1
|
||||||
|
onClick()
|
||||||
|
}
|
||||||
),
|
),
|
||||||
contentAlignment = Alignment.Center
|
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))
|
val info = cache.getOrCompute(LocalDate(2026, 6, 15))
|
||||||
assertFalse("普通日期不应为生日", info.isBirthday)
|
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