refactor: 移除 fling 速度阈值,改用 spring 动画驱动折叠过渡

- ViewModel onDragEnd/onExpandDragEnd 移除 velocity 参数
- BottomCard 移除 VelocityTracker,简化回调签名
- CalendarMonthView 添加 animateFloatAsState spring 动画
- 更新单元测试:移除 fling 测试,调整阈值边界

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
xfy 2026-05-22 13:36:58 +08:00
parent 7f9db1dc1d
commit 0cdac663c9
4 changed files with 31 additions and 92 deletions

View File

@ -18,7 +18,6 @@ import kotlinx.datetime.number
import kotlinx.datetime.plus
import kotlinx.datetime.todayIn
import plus.rua.project.ui.COLLAPSE_THRESHOLD
import plus.rua.project.ui.FLING_VELOCITY_THRESHOLD_DP
import plus.rua.project.ui.getMonthGridInfo
import kotlin.time.Clock
@ -215,25 +214,17 @@ class CalendarViewModel(
* @param delta 拖拽增量已归一化到 [0,1] 区间
*/
fun onDrag(delta: Float) {
val new = (_collapseProgress.value + delta).coerceIn(0f, 1f)
_collapseProgress.value = new
_collapseProgress.value = (_collapseProgress.value + delta).coerceIn(0f, 1f)
}
/**
* 展开状态拖拽结束根据进度和速度决定折叠或回弹
* 展开状态拖拽结束根据进度决定折叠或回弹
*
* 拖拽超过阈值时自动折叠到周视图否则回弹到月视图
*
* @param velocityDpPerSec 松手时的 fling 速度 (dp/s)正值=上滑折叠方向负值=下滑展开方向
*/
fun onDragEnd(velocityDpPerSec: Float = 0f) {
fun onDragEnd() {
val progress = _collapseProgress.value
val shouldCollapse = when {
velocityDpPerSec > FLING_VELOCITY_THRESHOLD_DP -> true
velocityDpPerSec < -FLING_VELOCITY_THRESHOLD_DP -> false
else -> progress > COLLAPSE_THRESHOLD
}
if (shouldCollapse) {
if (progress > COLLAPSE_THRESHOLD) {
_isCollapsed.value = true
_collapseProgress.value = 1f
} else {
@ -248,25 +239,17 @@ class CalendarViewModel(
* @param delta 拖拽增量已归一化到 [0,1] 区间
*/
fun onExpandDrag(delta: Float) {
val new = (_collapseProgress.value + delta).coerceIn(0f, 1f)
_collapseProgress.value = new
_collapseProgress.value = (_collapseProgress.value + delta).coerceIn(0f, 1f)
}
/**
* 折叠状态拖拽结束根据进度和速度决定展开或回弹
* 折叠状态拖拽结束根据进度决定展开或回弹
*
* 下拉超过阈值时自动展开到月视图否则回弹到周视图
*
* @param velocityDpPerSec 松手时的 fling 速度 (dp/s)正值=上滑负值=下滑
*/
fun onExpandDragEnd(velocityDpPerSec: Float = 0f) {
fun onExpandDragEnd() {
val progress = _collapseProgress.value
val shouldExpand = when {
velocityDpPerSec < -FLING_VELOCITY_THRESHOLD_DP -> true
velocityDpPerSec > FLING_VELOCITY_THRESHOLD_DP -> false
else -> progress < COLLAPSE_THRESHOLD
}
if (shouldExpand) {
if (progress < (1 - COLLAPSE_THRESHOLD)) {
_isCollapsed.value = false
_collapseProgress.value = 0f
} else {

View File

@ -60,13 +60,12 @@ fun BottomCard(
today: LocalDate,
shiftKind: ShiftKind?,
onDrag: (Float) -> Unit,
onDragEnd: (Float) -> Unit,
onDragEnd: () -> Unit,
onExpandDrag: (Float) -> Unit,
onExpandDragEnd: (Float) -> Unit,
onExpandDragEnd: () -> Unit,
dragRangePx: Float,
modifier: Modifier = Modifier
) {
val density = LocalDensity.current
val relativeDesc = relativeDayDescription(selectedDate, today)
@Suppress("DEPRECATION") // monthNumber 无替代 APIkotlinx-datetime 尚未提供新接口
@ -87,36 +86,21 @@ fun BottomCard(
modifier = modifier
.fillMaxWidth()
.pointerInput(isCollapsed) {
val velocityTracker = androidx.compose.ui.input.pointer.util.VelocityTracker()
if (isCollapsed) {
// 折叠状态:下拉恢复到月视图
detectVerticalDragGestures(
onDragEnd = {
val velocity = velocityTracker.calculateVelocity()
val velocityDpPerSec = with(density) { -velocity.y.toDp().value }
onExpandDragEnd(velocityDpPerSec)
},
onDragCancel = {
onExpandDragEnd(0f)
}
) { change, dragAmount ->
velocityTracker.addPosition(change.uptimeMillis, change.position)
onDragEnd = { onExpandDragEnd() },
onDragCancel = { onExpandDragEnd() }
) { _, dragAmount ->
val delta = -dragAmount / dragRangePx
onExpandDrag(delta)
}
} else {
// 展开状态:上拉折叠到周视图
detectVerticalDragGestures(
onDragEnd = {
val velocity = velocityTracker.calculateVelocity()
val velocityDpPerSec = with(density) { -velocity.y.toDp().value }
onDragEnd(velocityDpPerSec)
},
onDragCancel = {
onDragEnd(0f)
}
) { change, dragAmount ->
velocityTracker.addPosition(change.uptimeMillis, change.position)
onDragEnd = { onDragEnd() },
onDragCancel = { onDragEnd() }
) { _, dragAmount ->
val delta = -dragAmount / dragRangePx
onDrag(delta)
}

View File

@ -6,6 +6,9 @@ import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
@ -107,6 +110,13 @@ fun CalendarMonthView(
val collapseProgress = uiState.collapseProgress
val showLegalHoliday = uiState.showLegalHoliday
// 松手后 progress 从当前值 spring 动画到目标值0 或 1
val animatedCollapseProgress by animateFloatAsState(
targetValue = collapseProgress,
animationSpec = spring(stiffness = Spring.StiffnessMedium),
label = "collapseProgress"
)
val density = LocalDensity.current
val coroutineScope = rememberCoroutineScope()
@ -215,7 +225,7 @@ fun CalendarMonthView(
selectedDate = selectedDate,
today = today,
isCollapsed = isCollapsed,
collapseProgress = collapseProgress,
collapseProgress = animatedCollapseProgress,
showLegalHoliday = showLegalHoliday,
rowHeightPx = rowHeightPx,
screenWidthPx = screenWidthPx,
@ -562,9 +572,9 @@ private fun BottomCardArea(
today = today,
shiftKind = shiftKind,
onDrag = { delta -> viewModel.onDrag(delta) },
onDragEnd = { velocity -> viewModel.onDragEnd(velocity) },
onDragEnd = { viewModel.onDragEnd() },
onExpandDrag = { delta -> viewModel.onExpandDrag(delta) },
onExpandDragEnd = { velocity -> viewModel.onExpandDragEnd(velocity) },
onExpandDragEnd = { viewModel.onExpandDragEnd() },
dragRangePx = dragRangePx,
modifier = modifier
.offset(y = with(density) { (slideProgress * 200).dp })

View File

@ -311,7 +311,7 @@ class CalendarViewModelStateTest {
fun onExpandDragEnd_progressBelowThreshold_expands() {
val vm = createViewModel()
vm.onDrag(1f)
vm.onExpandDrag(-0.95f)
vm.onExpandDrag(-0.15f) // progress = 0.85 < 0.92
vm.onExpandDragEnd()
assertFalse(vm.isCollapsed.value)
assertEquals(0f, vm.collapseProgress.value, 0.001f)
@ -321,50 +321,12 @@ class CalendarViewModelStateTest {
fun onExpandDragEnd_progressAboveThreshold_staysCollapsed() {
val vm = createViewModel()
vm.onDrag(1f)
vm.onExpandDrag(-0.05f)
vm.onExpandDrag(-0.05f) // progress = 0.95 > 0.92
vm.onExpandDragEnd()
assertTrue(vm.isCollapsed.value)
assertEquals(1f, vm.collapseProgress.value, 0.001f)
}
@Test
fun onDragEnd_fastFlingUp_setsCollapsed() {
val vm = createViewModel()
vm.onDrag(0.1f)
vm.onDragEnd(velocityDpPerSec = 900f)
assertTrue(vm.isCollapsed.value)
assertEquals(1f, vm.collapseProgress.value, 0.001f)
}
@Test
fun onDragEnd_fastFlingDown_keepsExpanded() {
val vm = createViewModel()
vm.onDrag(0.9f)
vm.onDragEnd(velocityDpPerSec = -900f)
assertFalse(vm.isCollapsed.value)
assertEquals(0f, vm.collapseProgress.value, 0.001f)
}
@Test
fun onExpandDragEnd_fastFlingDown_setsExpanded() {
val vm = createViewModel()
vm.onDrag(1f)
vm.onExpandDrag(-0.1f)
vm.onExpandDragEnd(velocityDpPerSec = -900f)
assertFalse(vm.isCollapsed.value)
assertEquals(0f, vm.collapseProgress.value, 0.001f)
}
@Test
fun onExpandDragEnd_fastFlingUp_staysCollapsed() {
val vm = createViewModel()
vm.onDrag(1f)
vm.onExpandDrag(-0.9f)
vm.onExpandDragEnd(velocityDpPerSec = 900f)
assertTrue(vm.isCollapsed.value)
assertEquals(1f, vm.collapseProgress.value, 0.001f)
}
// ---- getMonthDays 与 selectedDate 配合 ----
@Test