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

View File

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

View File

@ -6,6 +6,9 @@ import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition import androidx.compose.animation.ExitTransition
import androidx.compose.animation.SharedTransitionLayout import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.core.FastOutSlowInEasing 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.core.tween
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
@ -107,6 +110,13 @@ fun CalendarMonthView(
val collapseProgress = uiState.collapseProgress val collapseProgress = uiState.collapseProgress
val showLegalHoliday = uiState.showLegalHoliday 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 density = LocalDensity.current
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
@ -215,7 +225,7 @@ fun CalendarMonthView(
selectedDate = selectedDate, selectedDate = selectedDate,
today = today, today = today,
isCollapsed = isCollapsed, isCollapsed = isCollapsed,
collapseProgress = collapseProgress, collapseProgress = animatedCollapseProgress,
showLegalHoliday = showLegalHoliday, showLegalHoliday = showLegalHoliday,
rowHeightPx = rowHeightPx, rowHeightPx = rowHeightPx,
screenWidthPx = screenWidthPx, screenWidthPx = screenWidthPx,
@ -562,9 +572,9 @@ private fun BottomCardArea(
today = today, today = today,
shiftKind = shiftKind, shiftKind = shiftKind,
onDrag = { delta -> viewModel.onDrag(delta) }, onDrag = { delta -> viewModel.onDrag(delta) },
onDragEnd = { velocity -> viewModel.onDragEnd(velocity) }, onDragEnd = { viewModel.onDragEnd() },
onExpandDrag = { delta -> viewModel.onExpandDrag(delta) }, onExpandDrag = { delta -> viewModel.onExpandDrag(delta) },
onExpandDragEnd = { velocity -> viewModel.onExpandDragEnd(velocity) }, onExpandDragEnd = { viewModel.onExpandDragEnd() },
dragRangePx = dragRangePx, dragRangePx = dragRangePx,
modifier = modifier modifier = modifier
.offset(y = with(density) { (slideProgress * 200).dp }) .offset(y = with(density) { (slideProgress * 200).dp })

View File

@ -311,7 +311,7 @@ class CalendarViewModelStateTest {
fun onExpandDragEnd_progressBelowThreshold_expands() { fun onExpandDragEnd_progressBelowThreshold_expands() {
val vm = createViewModel() val vm = createViewModel()
vm.onDrag(1f) vm.onDrag(1f)
vm.onExpandDrag(-0.95f) vm.onExpandDrag(-0.15f) // progress = 0.85 < 0.92
vm.onExpandDragEnd() vm.onExpandDragEnd()
assertFalse(vm.isCollapsed.value) assertFalse(vm.isCollapsed.value)
assertEquals(0f, vm.collapseProgress.value, 0.001f) assertEquals(0f, vm.collapseProgress.value, 0.001f)
@ -321,50 +321,12 @@ class CalendarViewModelStateTest {
fun onExpandDragEnd_progressAboveThreshold_staysCollapsed() { fun onExpandDragEnd_progressAboveThreshold_staysCollapsed() {
val vm = createViewModel() val vm = createViewModel()
vm.onDrag(1f) vm.onDrag(1f)
vm.onExpandDrag(-0.05f) vm.onExpandDrag(-0.05f) // progress = 0.95 > 0.92
vm.onExpandDragEnd() vm.onExpandDragEnd()
assertTrue(vm.isCollapsed.value) assertTrue(vm.isCollapsed.value)
assertEquals(1f, vm.collapseProgress.value, 0.001f) 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 配合 ---- // ---- getMonthDays 与 selectedDate 配合 ----
@Test @Test