diff options
author | 2025-03-19 23:55:28 -0700 | |
---|---|---|
committer | 2025-03-19 23:55:28 -0700 | |
commit | 26ced0b88489247f151dbe8b23c019f8edc97551 (patch) | |
tree | c293dab2eacb66c1efba65f69984de0a262a59d0 | |
parent | 8eed6111d35fc29b26a4db3492d0d891b24d880a (diff) | |
parent | 502a5b56b5eb3de345e8f0b8f4c7161fab2a465f (diff) |
Merge changes Ie276704c,Ibd3839d5,Ib5e2f94c into main
* changes:
[Media] Removes placeRelatively parameter from OffsetOverscrollEffect
[Media] Fixes swipe to dismiss in RTL.
[Media] Fixes swipe to reveal in RTL
4 files changed, 168 insertions, 73 deletions
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/OffsetOverscrollEffect.kt b/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/OffsetOverscrollEffect.kt index 07a571b94ce4..c411d272cb22 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/OffsetOverscrollEffect.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/OffsetOverscrollEffect.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.unit.dp import kotlin.math.roundToInt import kotlinx.coroutines.CoroutineScope +/** Returns a [remember]ed [OffsetOverscrollEffect]. */ @Composable @OptIn(ExperimentalMaterial3ExpressiveApi::class) fun rememberOffsetOverscrollEffect( @@ -63,7 +64,10 @@ data class OffsetOverscrollEffectFactory( private val animationSpec: AnimationSpec<Float>, ) : OverscrollFactory { override fun createOverscrollEffect(): OverscrollEffect { - return OffsetOverscrollEffect(animationScope, animationSpec) + return OffsetOverscrollEffect( + animationScope = animationScope, + animationSpec = animationSpec, + ) } } @@ -80,11 +84,11 @@ class OffsetOverscrollEffect(animationScope: CoroutineScope, animationSpec: Anim return layout(placeable.width, placeable.height) { val offsetPx = computeOffset(density = this@measure, overscrollDistance) if (offsetPx != 0) { - placeable.placeRelativeWithLayer( + placeable.placeWithLayer( with(requireConverter()) { offsetPx.toIntOffset() } ) } else { - placeable.placeRelative(0, 0) + placeable.place(0, 0) } } } diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt index d6d185195c51..063ff15054e8 100644 --- a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt @@ -267,7 +267,11 @@ private fun CardCarouselContent( } if (behavior.isCarouselDismissible) { - SwipeToDismiss(content = { PagerContent() }, onDismissed = onDismissed) + SwipeToDismiss( + content = { PagerContent() }, + isSwipingEnabled = isSwipingEnabled, + onDismissed = onDismissed, + ) } else { val overscrollEffect = rememberOffsetOverscrollEffect() SwipeToReveal( diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToDismiss.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToDismiss.kt index b80bf4143252..f044257bb343 100644 --- a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToDismiss.kt +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToDismiss.kt @@ -17,99 +17,187 @@ package com.android.systemui.media.remedia.ui.compose import androidx.compose.animation.core.Animatable -import androidx.compose.foundation.OverscrollEffect +import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.absoluteOffset +import androidx.compose.foundation.overscroll import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import androidx.compose.ui.input.nestedscroll.NestedScrollSource -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.input.pointer.PointerType +import androidx.compose.ui.layout.layout import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.Velocity +import androidx.compose.ui.util.fastCoerceIn import androidx.compose.ui.util.fastRoundToInt +import com.android.compose.gesture.NestedDraggable +import com.android.compose.gesture.effect.rememberOffsetOverscrollEffect +import com.android.compose.gesture.nestedDraggable +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch /** Swipe to dismiss that supports nested scrolling. */ @Composable fun SwipeToDismiss( - content: @Composable (overscrollEffect: OverscrollEffect?) -> Unit, + content: @Composable () -> Unit, + isSwipingEnabled: Boolean, onDismissed: () -> Unit, modifier: Modifier = Modifier, ) { - val scope = rememberCoroutineScope() - val offsetAnimatable = remember { Animatable(0f) } + val overscrollEffect = rememberOffsetOverscrollEffect() - // This is the width of the revealed content UI box. It's not a state because it's not - // observed in any composition and is an object with a value to avoid the extra cost - // associated with boxing and unboxing an int. - val revealedContentBoxWidth = remember { + // This is the width of the content UI box. It's not a state because it's not observed in any + // composition and is an object with a value to avoid the extra cost associated with boxing and + // unboxing an int. + val contentBoxWidth = remember { object { var value = 0 } } - val nestedScrollConnection = remember { - object : NestedScrollConnection { - override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { - return if (offsetAnimatable.value > 0f && available.x < 0f) { - scope.launch { offsetAnimatable.snapTo(offsetAnimatable.value + available.x) } - Offset(available.x, 0f) - } else if (offsetAnimatable.value < 0f && available.x > 0f) { - scope.launch { offsetAnimatable.snapTo(offsetAnimatable.value + available.x) } - Offset(available.x, 0f) - } else { - Offset.Zero - } - } - - override fun onPostScroll( - consumed: Offset, - available: Offset, - source: NestedScrollSource, - ): Offset { - return if (available.x > 0f) { - scope.launch { offsetAnimatable.snapTo(offsetAnimatable.value + available.x) } - Offset(available.x, 0f) - } else if (available.x < 0f) { - scope.launch { offsetAnimatable.snapTo(offsetAnimatable.value + available.x) } - Offset(available.x, 0f) - } else { - Offset.Zero - } - } + // In order to support the drag to dismiss, infrastructure has to be put in place where a + // NestedDraggable helps by consuming the unconsumed drags and flings and applying the offset. + // + // This is the NestedDraggalbe controller. + val dragController = + rememberDismissibleContentDragController( + maxBound = { contentBoxWidth.value.toFloat() }, + onDismissed = onDismissed, + ) - override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { - scope.launch { - offsetAnimatable.animateTo( - if (offsetAnimatable.value >= revealedContentBoxWidth.value / 2f) { - revealedContentBoxWidth.value * 2f - } else if (offsetAnimatable.value <= -revealedContentBoxWidth.value / 2f) { - -revealedContentBoxWidth.value * 2f - } else { - 0f - } - ) - if (offsetAnimatable.value != 0f) { - onDismissed() + Box( + modifier = + modifier + .layout { measurable, constraints -> + val placeable = measurable.measure(constraints) + contentBoxWidth.value = placeable.measuredWidth + layout(placeable.measuredWidth, placeable.measuredHeight) { + placeable.place(0, 0) } } - return super.onPostFling(consumed, available) - } + .nestedDraggable( + enabled = isSwipingEnabled, + draggable = + remember { + object : NestedDraggable { + override fun onDragStarted( + position: Offset, + sign: Float, + pointersDown: Int, + pointerType: PointerType?, + ): NestedDraggable.Controller { + return dragController + } + + override fun shouldConsumeNestedPostScroll(sign: Float): Boolean { + return dragController.shouldConsumePostScrolls(sign) + } + + override fun shouldConsumeNestedPreScroll(sign: Float): Boolean { + return dragController.shouldConsumePreScrolls(sign) + } + } + }, + orientation = Orientation.Horizontal, + ) + .overscroll(overscrollEffect) + .absoluteOffset { IntOffset(dragController.offset.fastRoundToInt(), y = 0) } + ) { + content() + } +} + +@Composable +private fun rememberDismissibleContentDragController( + maxBound: () -> Float, + onDismissed: () -> Unit, +): DismissibleContentDragController { + val scope = rememberCoroutineScope() + return remember { + DismissibleContentDragController( + scope = scope, + maxBound = maxBound, + onDismissed = onDismissed, + ) + } +} + +private class DismissibleContentDragController( + private val scope: CoroutineScope, + private val maxBound: () -> Float, + private val onDismissed: () -> Unit, +) : NestedDraggable.Controller { + private val offsetAnimatable = Animatable(0f) + private var lastTarget = 0f + private var range = 0f..1f + private var shouldConsumePreScrolls by mutableStateOf(false) + + override val autoStopNestedDrags: Boolean + get() = true + + val offset: Float + get() = offsetAnimatable.value + + fun shouldConsumePreScrolls(sign: Float): Boolean { + if (!shouldConsumePreScrolls) return false + + if (lastTarget > 0f && sign < 0f) { + range = 0f..maxBound() + return true } + + if (lastTarget < 0f && sign > 0f) { + range = -maxBound()..0f + return true + } + + return false } - Box( - modifier = - modifier - .onSizeChanged { revealedContentBoxWidth.value = it.width } - .nestedScroll(nestedScrollConnection) - .offset { IntOffset(x = offsetAnimatable.value.fastRoundToInt(), y = 0) } - ) { - content(null) + fun shouldConsumePostScrolls(sign: Float): Boolean { + val max = maxBound() + if (sign > 0f && lastTarget < max) { + range = 0f..maxBound() + return true + } + + if (sign < 0f && lastTarget > -max) { + range = -maxBound()..0f + return true + } + + return false + } + + override fun onDrag(delta: Float): Float { + val previousTarget = lastTarget + lastTarget = (lastTarget + delta).fastCoerceIn(range.start, range.endInclusive) + val newTarget = lastTarget + scope.launch { offsetAnimatable.snapTo(newTarget) } + return lastTarget - previousTarget + } + + override suspend fun onDragStopped(velocity: Float, awaitFling: suspend () -> Unit): Float { + val rangeMiddle = range.start + (range.endInclusive - range.start) / 2f + lastTarget = + when { + lastTarget >= rangeMiddle -> range.endInclusive + else -> range.start + } + + shouldConsumePreScrolls = lastTarget != 0f + val newTarget = lastTarget + + scope.launch { + offsetAnimatable.animateTo(newTarget) + if (newTarget != 0f) { + onDismissed() + } + } + return velocity } } diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToReveal.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToReveal.kt index 770762c7a29f..1d7b79d9a07a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToReveal.kt +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToReveal.kt @@ -22,7 +22,6 @@ import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.absoluteOffset -import androidx.compose.foundation.layout.offset import androidx.compose.foundation.overscroll import androidx.compose.foundation.withoutVisualEffect import androidx.compose.runtime.Composable @@ -82,7 +81,7 @@ fun SwipeToReveal( // overscroll visual effect. // // This is the NestedDraggalbe controller. - val revealedContentDragController = rememberRevealedContentDragController { + val revealedContentDragController = rememberDismissibleContentDragController { revealedContentBoxWidth.value.toFloat() } @@ -186,7 +185,7 @@ fun SwipeToReveal( } @Composable -private fun rememberRevealedContentDragController( +private fun rememberDismissibleContentDragController( maxBound: () -> Float ): RevealedContentDragController { val scope = rememberCoroutineScope() |