Skip to content

Conversation

@HChenX
Copy link
Contributor

@HChenX HChenX commented Dec 1, 2025

No description provided.

@HChenX HChenX changed the title refactor(overscroll): Make springback effect closer to Xiaomi's nativ… refactor(overscroll): Make springback effect closer to Xiaomi's native behavior. Dec 1, 2025
This commit refactors the overscroll implementation with several key improvements:

*   Simplifies the spring equilibrium check by using `sign` to detect when the position crosses zero, removing the previous complex logic.
*   Streamlines the `applyDrag` function to only update the offset without returning an overflow value, simplifying the drag calculation.
*   Refactors `onPreScroll` and `onPostScroll` to use this simplified drag logic, making the code more concise and removing redundant calculations.
*   Optimizes the `onPreFling` behavior by consuming a portion of the fling velocity when the fling is in the opposite direction of the overscroll, improving the feel of the animation.

This comment was marked as outdated.

Passes the consumed offsets from the parent nested scroll connection through the `Overscroll` handler. This ensures that scroll and fling events consumed by parent composables are correctly accounted for, preventing them from being ignored by the overscroll effect.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

}

/**
* Calculate the new velocity using Euler's formula
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says "Calculate the new velocity using Euler's formula" but this is actually Euler's method (also known as Euler integration or forward Euler), not Euler's formula. Euler's formula refers to e^(ix) = cos(x) + i*sin(x).

Suggested correction:

/**
 * Calculate the new velocity using Euler's method (forward Euler integration)
 * */
Suggested change
* Calculate the new velocity using Euler's formula
* Calculate the new velocity using Euler's method (forward Euler integration)

Copilot uses AI. Check for mistakes.
}
}

private class SpringOperator(dampingRatio: Float, naturalPeriod: Float) {
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The naturalPeriod parameter in the SpringOperator is in seconds, but this is not clearly documented. The constants STANDARD_SPRING_PERIOD = 0.4f and SLOWER_SPRING_PERIOD_FOR_HIGH_VELOCITY = 0.55f would benefit from documentation indicating the unit.

Consider adding a KDoc comment:

/**
 * @param dampingRatio The damping ratio (1.0 = critical damping)
 * @param naturalPeriod The natural period of oscillation in seconds
 */
private class SpringOperator(dampingRatio: Float, naturalPeriod: Float) {

Copilot uses AI. Check for mistakes.
currentPosition: Double,
targetPosition: Double
): Double {
val velocityDecayFactor = 1.0 - dampingCoefficient * deltaTime
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The velocityDecayFactor calculation can potentially become negative if dampingCoefficient * deltaTime > 1.0, which would cause the velocity to flip sign incorrectly. While deltaTime is clamped to MAX_FRAME_DELTA_SECONDS, under certain combinations of damping ratio and natural period, this could still occur.

Consider adding safety bounds:

val velocityDecayFactor = (1.0 - dampingCoefficient * deltaTime).coerceAtLeast(0.0)
Suggested change
val velocityDecayFactor = 1.0 - dampingCoefficient * deltaTime
val velocityDecayFactor = (1.0 - dampingCoefficient * deltaTime).coerceAtLeast(0.0)

Copilot uses AI. Check for mistakes.
Comment on lines +196 to +198
val scrollRange = with(LocalDensity.current) {
(if (isVertical) windowSize.height.toDp() else windowSize.width.toDp()).toPx()
}
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The scrollRange calculation unnecessarily converts physical pixels to Dp and back to pixels. The getWindowSize() function already returns physical pixel dimensions (as seen in the Android implementation using windowMetrics.bounds.width()), so converting to Dp and back is redundant and may introduce precision loss.

Simplify to:

val scrollRange = if (isVertical) windowSize.height.toFloat() else windowSize.width.toFloat()
Suggested change
val scrollRange = with(LocalDensity.current) {
(if (isVertical) windowSize.height.toDp() else windowSize.width.toDp()).toPx()
}
val scrollRange = if (isVertical) windowSize.height.toFloat() else windowSize.width.toFloat()

Copilot uses AI. Check for mistakes.
Comment on lines +295 to +299
if (abs(offset) <= offsetThreshold || sign(delta) == sign(rawTouchAccumulation)) {
return parentConsumed
}
// If a fling animation is running, request it to stop to blend with user input.
if (flingAnimatable.isRunning) {
coroutineScope.launch {
// stop() is suspend; we call it in coroutine context to stop smoothly.
flingAnimatable.stop()

if (sign(delta) != sign(rawTouchAccumulation)) { // opposite direction
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition on line 295 returns early if sign(delta) == sign(rawTouchAccumulation), but then line 299 checks if (sign(delta) != sign(rawTouchAccumulation)) which will always be true at that point. The second condition is redundant and should be removed.

Additionally, after the condition on line 295, the only possible case is when the signs are opposite, so the check on line 299 is unnecessary. Consider restructuring:

if (abs(offset) <= offsetThreshold) {
    return parentConsumed
}

if (sign(delta) == sign(rawTouchAccumulation)) {
    return parentConsumed
}

// At this point, signs are opposite
val actualConsumed = if (abs(rawTouchAccumulation) <= abs(delta)) {
    -rawTouchAccumulation
} else {
    delta
}
// ... rest of logic

Copilot uses AI. Check for mistakes.
if (absPixelOffset >= absMaxOffset) return absMaxOffset
if (absPixelOffset <= 0f) return 0f

val part2 = range.toDouble().pow(2.0 / 3.0) * (range - (3.0 * absPixelOffset)).pow(1.0 / 3.0)
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The formula (range - (3.0 * absPixelOffset)).pow(1.0 / 3.0) can produce a negative value when absPixelOffset > range / 3, which would then attempt to take the cube root of a negative number. While Kotlin's pow function with fractional exponents on negative numbers returns NaN, this might not be the intended behavior.

Consider adding validation or adjusting the formula:

val innerValue = range - (3.0 * absPixelOffset)
if (innerValue < 0.0) return range.toFloat() // or handle appropriately
val part2 = range.toDouble().pow(2.0 / 3.0) * innerValue.pow(1.0 / 3.0)
Suggested change
val part2 = range.toDouble().pow(2.0 / 3.0) * (range - (3.0 * absPixelOffset)).pow(1.0 / 3.0)
val innerValue = range - (3.0 * absPixelOffset)
if (innerValue < 0.0) return range.toFloat()
val part2 = range.toDouble().pow(2.0 / 3.0) * innerValue.pow(1.0 / 3.0)

Copilot uses AI. Check for mistakes.
@YuKongA YuKongA merged commit 0ee751f into compose-miuix-ui:main Dec 1, 2025
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants