diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/MouseWheelScrollable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/MouseWheelScrollable.kt index f7c7abcc086eb..c3c0f11807fe5 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/MouseWheelScrollable.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/MouseWheelScrollable.kt @@ -113,7 +113,10 @@ internal class MouseWheelScrollNode( ) { operator fun plus(other: MouseWheelScrollDelta) = MouseWheelScrollDelta( value = value + other.value, - shouldApplyImmediately = shouldApplyImmediately || other.shouldApplyImmediately + + // Ignore [other.shouldApplyImmediately] to avoid false-positive [isPreciseWheelScroll] + // detection during animation + shouldApplyImmediately = shouldApplyImmediately ) } private val channel = Channel(capacity = Channel.UNLIMITED) @@ -216,12 +219,18 @@ internal class MouseWheelScrollNode( * Ideally it should be resolved by catching real touches from input device instead of * waiting the next event with timeout before resetting progress flag. */ - suspend fun waitNextScrollDelta(timeoutMillis: Long, forceApplyImmediately: Boolean = false): Boolean { + suspend fun waitNextScrollDelta(timeoutMillis: Long): Boolean { if (timeoutMillis < 0) return false return withTimeoutOrNull(timeoutMillis) { channel.busyReceive() }?.let { - targetScrollDelta = if (forceApplyImmediately) it.copy(shouldApplyImmediately = true) else it + // Keep this value unchanged during animation + // Currently, [isPreciseWheelScroll] might be unstable in case if + // a precise value is almost equal regular one. + val previousDeltaShouldApplyImmediately = targetScrollDelta.shouldApplyImmediately + targetScrollDelta = it.copy( + shouldApplyImmediately = previousDeltaShouldApplyImmediately + ) targetValue = targetScrollDelta.value.reverseIfNeeded().toFloat() animationState = AnimationState(0f) // Reset previous animation leftover @@ -236,14 +245,7 @@ internal class MouseWheelScrollNode( val targetValueLeftover = targetValue - animationState.value if (targetScrollDelta.shouldApplyImmediately || abs(targetValueLeftover) < threshold) { dispatchMouseWheelScroll(targetValueLeftover) - requiredAnimation = waitNextScrollDelta( - timeoutMillis = ScrollProgressTimeout, - - // Apply the next event without `ProgressTimeout` immediately too. - // Currently, `isPreciseWheelScroll` might be false-negative in case if - // precise value is almost equal regular one. - forceApplyImmediately = targetScrollDelta.shouldApplyImmediately - ) + requiredAnimation = waitNextScrollDelta(ScrollProgressTimeout) } else { // Animation will start only on the next frame, // so apply threshold immediately to avoid delays. diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/gestures/DesktopScrollable.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/gestures/DesktopScrollable.desktop.kt index 93d49d9aef1ff..9f408d86b200d 100644 --- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/gestures/DesktopScrollable.desktop.kt +++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/gestures/DesktopScrollable.desktop.kt @@ -118,4 +118,11 @@ private val PointerEvent.isPreciseWheelRotation get() = (awtEventOrNull as? MouseWheelEvent)?.isPreciseWheelRotation ?: false private val MouseWheelEvent.isPreciseWheelRotation - get() = abs(preciseWheelRotation - wheelRotation.toDouble()) > 0.001 + get() = when (DesktopPlatform.Current) { + // On Windows, even free scrolling wheels should trigger animation + DesktopPlatform.Windows -> false + + // For other platforms, apply this heuristic to determine if it's + // high-precision wheel/trackpad or regular stepping mouse wheel. + else -> abs(preciseWheelRotation - wheelRotation.toDouble()) > 0.001 + }