Add fling velocity threshold to collapse/expand drag gesture

Quick swipe now snaps to the target state regardless of progress position,
matching the behavior of HorizontalPager's fling logic. Uses VelocityTracker
to measure release velocity and a 800 dp/s threshold (FLING_VELOCITY_THRESHOLD_DP).
Slow drags still use the positional COLLAPSE_THRESHOLD (50%) as before.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
xfy 2026-05-15 16:49:32 +08:00
parent d0492d02f0
commit 69e49b5d81
3 changed files with 39 additions and 10 deletions

View File

@ -17,6 +17,7 @@ import kotlinx.datetime.plus
import kotlinx.datetime.todayIn
import kotlin.time.Clock
import plus.rua.project.ui.COLLAPSE_THRESHOLD
import plus.rua.project.ui.FLING_VELOCITY_THRESHOLD_DP
data class CalendarDay(
val date: LocalDate,
@ -61,10 +62,16 @@ class CalendarViewModel(
}
// 拖拽超过阈值时自动折叠到周视图,否则回弹到月视图
fun onDragEnd() {
// velocityDpPerSec: 松手时的 fling 速度 (dp/s),正值=上滑(折叠方向),负值=下滑(展开方向)
fun onDragEnd(velocityDpPerSec: Float = 0f) {
coroutineScope.launch {
val current = _collapseAnimatable.value
if (current > COLLAPSE_THRESHOLD) {
val progress = _collapseAnimatable.value
val shouldCollapse = when {
velocityDpPerSec > FLING_VELOCITY_THRESHOLD_DP -> true // 快速上滑→折叠
velocityDpPerSec < -FLING_VELOCITY_THRESHOLD_DP -> false // 快速下滑→展开
else -> progress > COLLAPSE_THRESHOLD // 慢速按 progress 判断
}
if (shouldCollapse) {
_collapseAnimatable.animateTo(
targetValue = 1f,
animationSpec = spring(dampingRatio = 0.8f, stiffness = 400f)
@ -88,10 +95,16 @@ class CalendarViewModel(
}
// 下拉超过阈值时自动展开到月视图,否则回弹到周视图
fun onExpandDragEnd() {
// velocityDpPerSec: 同上,正值=上滑,负值=下滑
fun onExpandDragEnd(velocityDpPerSec: Float = 0f) {
coroutineScope.launch {
val current = _collapseAnimatable.value
if (current < COLLAPSE_THRESHOLD) {
val progress = _collapseAnimatable.value
val shouldExpand = when {
velocityDpPerSec < -FLING_VELOCITY_THRESHOLD_DP -> true // 快速下滑→展开
velocityDpPerSec > FLING_VELOCITY_THRESHOLD_DP -> false // 快速上滑→保持折叠
else -> progress < COLLAPSE_THRESHOLD // 慢速按 progress 判断
}
if (shouldExpand) {
_collapseAnimatable.animateTo(
targetValue = 0f,
animationSpec = spring(dampingRatio = 0.8f, stiffness = 400f)

View File

@ -11,11 +11,13 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import plus.rua.project.CalendarViewModel
@ -33,20 +35,27 @@ fun BottomCard(
dragRangePx: Float,
modifier: Modifier = Modifier
) {
val density = LocalDensity.current
Surface(
modifier = modifier
.fillMaxWidth()
.pointerInput(viewModel.isCollapsed) {
val velocityTracker = androidx.compose.ui.input.pointer.util.VelocityTracker()
if (viewModel.isCollapsed) {
// 折叠状态:下拉恢复到月视图
detectVerticalDragGestures(
onDragEnd = {
viewModel.onExpandDragEnd()
val velocity = velocityTracker.calculateVelocity()
// 上滑为正(折叠方向),下拉为负(展开方向)
val velocityDpPerSec = with(density) { -velocity.y.toDp().value }
viewModel.onExpandDragEnd(velocityDpPerSec)
},
onDragCancel = {
viewModel.onExpandDragEnd()
}
) { _, dragAmount ->
) { change, dragAmount ->
velocityTracker.addPosition(change.uptimeMillis, change.position)
val delta = -dragAmount / dragRangePx
viewModel.onExpandDrag(delta)
}
@ -54,12 +63,16 @@ fun BottomCard(
// 展开状态:上拉折叠到周视图
detectVerticalDragGestures(
onDragEnd = {
viewModel.onDragEnd()
val velocity = velocityTracker.calculateVelocity()
// 上滑为正(折叠方向),下拉为负(展开方向)
val velocityDpPerSec = with(density) { -velocity.y.toDp().value }
viewModel.onDragEnd(velocityDpPerSec)
},
onDragCancel = {
viewModel.onDragEnd()
}
) { _, dragAmount ->
) { change, dragAmount ->
velocityTracker.addPosition(change.uptimeMillis, change.position)
val delta = -dragAmount / dragRangePx
viewModel.onDrag(delta)
}

View File

@ -24,6 +24,9 @@ const val HORIZONTAL_PADDING_DP = 16
/** BottomCard 拖拽手势范围最小值 (dp),防止行数少时 dragRange 过小 */
const val DRAG_RANGE_MIN_DP = 100
/** fling 速度阈值 (dp/s),超过此速度按方向直接折叠/展开,不受 progress 阈值限制 */
const val FLING_VELOCITY_THRESHOLD_DP = 800
/** 日历与 BottomCard 之间的间距 (dp):展开时 */
const val CARD_GAP_EXPANDED_DP = 24