summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/OffsetOverscrollEffect.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToDismiss.kt220
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToReveal.kt5
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()