From 0b6d9ea87a892cfe98515406f338b4b39adc194e Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Mon, 25 May 2026 23:38:59 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=BB=9A=E8=BD=AE=E9=80=89=E6=8B=A9?= =?UTF-8?q?=E5=99=A8=E4=BB=85=E5=9C=A8=E6=BB=9A=E5=8A=A8=E5=81=9C=E6=AD=A2?= =?UTF-8?q?=E5=90=8E=E8=A7=A6=E5=8F=91=E9=80=89=E4=B8=AD=E5=8F=98=E6=9B=B4?= =?UTF-8?q?=E5=92=8C=E8=A7=A6=E8=A7=89=E5=8F=8D=E9=A6=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 快速滑动时不再每帧更新 selectedIndex,改为等滚动停止后 计算最终中心项再触发回调,消除来回抽搐问题。 --- .../kotlin/plus/rua/project/ui/WheelPicker.kt | 95 +++++++------------ 1 file changed, 35 insertions(+), 60 deletions(-) diff --git a/core/src/main/kotlin/plus/rua/project/ui/WheelPicker.kt b/core/src/main/kotlin/plus/rua/project/ui/WheelPicker.kt index 6e4dae8..e46312f 100644 --- a/core/src/main/kotlin/plus/rua/project/ui/WheelPicker.kt +++ b/core/src/main/kotlin/plus/rua/project/ui/WheelPicker.kt @@ -1,6 +1,7 @@ package plus.rua.project.ui -import android.os.Build +import android.view.HapticFeedbackConstants +import android.view.View import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior import androidx.compose.foundation.layout.Box @@ -16,32 +17,31 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter import kotlin.math.abs -import kotlin.math.roundToInt private val ItemHeight = 48.dp private const val VisibleItemCount = 5 private val WheelHeight = ItemHeight * VisibleItemCount +private const val PaddingItems = VisibleItemCount / 2 /** * 通用滚轮选择器,支持惯性吸附和触觉反馈。 * + * 滚动停止后才触发选中变更和触觉反馈,避免快速滑动时抖动。 + * * @param items 显示的项目列表 * @param selectedIndex 当前选中项索引 - * @param onSelectedChange 选中项变化回调 + * @param onSelectedChange 选中项变化回调(仅在滚动停止后触发) * @param modifier 外部布局修饰符 - * @param itemContent 单个项目渲染,[isSelected] 为 true 表示中心选中项 */ @Composable fun WheelPicker( @@ -49,65 +49,41 @@ fun WheelPicker( selectedIndex: Int, onSelectedChange: (Int) -> Unit, modifier: Modifier = Modifier, - itemContent: @Composable (index: Int, item: String, isSelected: Boolean) -> Unit = { _, item, isSelected -> - Text( - text = item, - color = if (isSelected) MaterialTheme.colorScheme.onSurface - else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f), - fontSize = if (isSelected) 20.sp else 16.sp, - fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal, - style = LocalTextStyle.current - ) - } ) { - val paddingItems = VisibleItemCount / 2 - val totalItems = items.size + paddingItems * 2 val listState = rememberLazyListState( - initialFirstVisibleItemIndex = (selectedIndex - paddingItems).coerceAtLeast(0) + initialFirstVisibleItemIndex = (selectedIndex - PaddingItems).coerceAtLeast(0) ) - val coroutineScope = rememberCoroutineScope() val view = LocalView.current - fun centerForLayoutIndex(layoutIndex: Int): Int = layoutIndex - paddingItems - - fun layoutIndexForCenter(center: Int): Int = center + paddingItems - - // 检测中心选中项变化 → 触觉反馈 - val currentCenter by remember { + // 视觉中心项(实时,仅用于渲染高亮) + val visualCenter by remember { derivedStateOf { val viewportCenter = listState.layoutInfo.viewportSize.height / 2f listState.layoutInfo.visibleItemsInfo.minByOrNull { abs(it.offset + it.size / 2f - viewportCenter) - }?.index?.let { centerForLayoutIndex(it) } ?: -1 - } - } - - LaunchedEffect(currentCenter) { - if (currentCenter in items.indices && currentCenter != selectedIndex) { - onSelectedChange(currentCenter) - performHapticFeedback(view) + }?.index?.let { it - PaddingItems } ?: selectedIndex } } // 初始滚动到选中项 LaunchedEffect(selectedIndex) { - val target = layoutIndexForCenter(selectedIndex) - if (centerForLayoutIndex(listState.firstVisibleItemIndex) != selectedIndex) { - listState.scrollToItem((target - paddingItems).coerceAtLeast(0)) + val target = (selectedIndex - PaddingItems).coerceAtLeast(0) + if (listState.firstVisibleItemIndex != target) { + listState.scrollToItem(target) } } - // 滚动停止后吸附到最近项 + // 滚动停止后:计算最终中心项 → 触发选中变更 + 触觉反馈 LaunchedEffect(listState) { - snapshotFlow { listState.isScrollInProgress } - .collect { scrolling -> + var lastSettled = selectedIndex + snapshotFlow { listState.isScrollInProgress to listState.layoutInfo} + .collect { (scrolling, _) -> if (!scrolling) { - val target = layoutIndexForCenter(currentCenter.coerceIn(0, items.lastIndex)) - val current = listState.firstVisibleItemIndex + paddingItems - if (target != current) { - coroutineScope.launch { - listState.animateScrollToItem((target - paddingItems).coerceAtLeast(0)) - } + val center = visualCenter.coerceIn(0, items.lastIndex) + if (center != lastSettled) { + lastSettled = center + onSelectedChange(center) + view.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK) } } } @@ -124,8 +100,8 @@ fun WheelPicker( horizontalAlignment = Alignment.CenterHorizontally, userScrollEnabled = true ) { - items(totalItems) { layoutIndex -> - val centerIndex = centerForLayoutIndex(layoutIndex) + items(items.size + PaddingItems * 2) { layoutIndex -> + val centerIndex = layoutIndex - PaddingItems val isValid = centerIndex in items.indices Box( modifier = Modifier @@ -134,18 +110,17 @@ fun WheelPicker( contentAlignment = Alignment.Center ) { if (isValid) { - itemContent(centerIndex, items[centerIndex], centerIndex == currentCenter) + val isSelected = centerIndex == visualCenter + Text( + text = items[centerIndex], + color = if (isSelected) MaterialTheme.colorScheme.onSurface + else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f), + fontSize = if (isSelected) 20.sp else 16.sp, + fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal, + style = LocalTextStyle.current + ) } } } } } - -private fun performHapticFeedback(view: android.view.View) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { - view.performHapticFeedback(android.view.HapticFeedbackConstants.CLOCK_TICK) - } else { - @Suppress("DEPRECATION") - view.performHapticFeedback(android.view.HapticFeedbackConstants.CLOCK_TICK) - } -}