@@ -34,7 +34,6 @@ import androidx.compose.runtime.getValue
3434import androidx.compose.runtime.mutableStateOf
3535import androidx.compose.runtime.remember
3636import androidx.compose.runtime.setValue
37- import androidx.compose.runtime.snapshotFlow
3837import androidx.compose.ui.Alignment
3938import androidx.compose.ui.Modifier
4039import androidx.compose.ui.draw.alpha
@@ -53,7 +52,6 @@ import androidx.compose.ui.unit.dp
5352import androidx.compose.ui.unit.sp
5453import androidx.compose.ui.window.Popup
5554import androidx.compose.ui.window.PopupProperties
56- import kotlinx.coroutines.flow.distinctUntilChanged
5755import io.github.kdroidfilter.nucleus.ui.apple.macos.icons.Icon
5856import io.github.kdroidfilter.nucleus.ui.apple.macos.icons.Icons
5957import io.github.kdroidfilter.nucleus.ui.apple.macos.theme.macosGlass
@@ -1243,8 +1241,11 @@ private fun <T> WheelColumn(
12431241
12441242 val useVirtualScroll = items.size > visibleItems
12451243 val paddedCount = if (useVirtualScroll) items.size * 1000 else items.size + halfVisible * 2
1244+ // midOffset is the largest multiple of items.size that is <= paddedCount/2,
1245+ // so that (absoluteIndex - midOffset).mod(items.size) == realIndex.
1246+ val midOffset = if (useVirtualScroll) (paddedCount / 2 ) - ((paddedCount / 2 ) % items.size) else 0
12461247 val initialScroll = if (useVirtualScroll) {
1247- (paddedCount / 2 ) - ((paddedCount / 2 ) % items.size) + initialIndex - halfVisible
1248+ midOffset + initialIndex - halfVisible
12481249 } else {
12491250 initialIndex
12501251 }
@@ -1258,23 +1259,31 @@ private fun <T> WheelColumn(
12581259 val topFade = Brush .verticalGradient(0f to fadeBgColor, 1f to Color .Transparent )
12591260 val bottomFade = Brush .verticalGradient(0f to Color .Transparent , 1f to fadeBgColor)
12601261
1261- val currentCenter by remember {
1262+ // Pixel-accurate: find the item whose center is closest to the viewport midpoint.
1263+ // This is more precise than firstVisibleItemIndex + halfVisible, which only updates
1264+ // when an item fully scrolls off screen rather than when it crosses the center.
1265+ val centerItemIndex by remember {
12621266 derivedStateOf {
1263- listState.firstVisibleItemIndex + halfVisible
1267+ with (listState.layoutInfo) {
1268+ val viewportMid = viewportSize.height / 2
1269+ visibleItemsInfo
1270+ .minByOrNull { kotlin.math.abs(it.offset + it.size / 2 - viewportMid) }
1271+ ?.index ? : (listState.firstVisibleItemIndex + halfVisible)
1272+ }
12641273 }
12651274 }
12661275
1267- LaunchedEffect (Unit ) {
1268- snapshotFlow { currentCenter }
1269- .distinctUntilChanged()
1270- .collect { centerIndex ->
1271- val realIndex = if (useVirtualScroll) {
1272- centerIndex % items.size
1273- } else {
1274- (centerIndex - halfVisible).coerceIn(0 , items.size - 1 )
1275- }
1276- onSelectedChanged(realIndex)
1276+ // Notify the caller only once the scroll/fling has fully settled, not on every
1277+ // frame during a drag, to avoid spurious intermediate selection callbacks.
1278+ LaunchedEffect (listState.isScrollInProgress) {
1279+ if (! listState.isScrollInProgress) {
1280+ val realIndex = if (useVirtualScroll) {
1281+ (centerItemIndex - midOffset).mod(items.size)
1282+ } else {
1283+ (centerItemIndex - halfVisible).coerceIn(0 , items.size - 1 )
12771284 }
1285+ onSelectedChanged(realIndex)
1286+ }
12781287 }
12791288
12801289 Box (
@@ -1296,7 +1305,7 @@ private fun <T> WheelColumn(
12961305 } else {
12971306 (index - halfVisible).coerceIn(0 , items.size - 1 )
12981307 }
1299- val distFromCenter = kotlin.math.abs(index - currentCenter )
1308+ val distFromCenter = kotlin.math.abs(index - centerItemIndex )
13001309 val alpha = when {
13011310 isSpacerItem -> 0f
13021311 distFromCenter == 0 -> 1f
0 commit comments