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:
parent
7f9db1dc1d
commit
0cdac663c9
@ -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 {
|
||||||
|
|||||||
@ -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 无替代 API,kotlinx-datetime 尚未提供新接口
|
@Suppress("DEPRECATION") // monthNumber 无替代 API,kotlinx-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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 })
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user