Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 26ced0b8 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

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
parents 8eed6111 502a5b56
Loading
Loading
Loading
Loading
+7 −3
Original line number Diff line number Diff line
@@ -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)
                    }
                }
            }
+5 −1
Original line number Diff line number Diff line
@@ -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(
+154 −66
Original line number Diff line number Diff line
@@ -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
    // 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,
        )

    Box(
        modifier =
            modifier
                .layout { measurable, constraints ->
                    val placeable = measurable.measure(constraints)
                    contentBoxWidth.value = placeable.measuredWidth
                    layout(placeable.measuredWidth, placeable.measuredHeight) {
                        placeable.place(0, 0)
                    }
                }

            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
                .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 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
                                override fun shouldConsumeNestedPreScroll(sign: Float): Boolean {
                                    return dragController.shouldConsumePreScrolls(sign)
                                }
                            }
                        },
                    orientation = Orientation.Horizontal,
                )
                    if (offsetAnimatable.value != 0f) {
                        onDismissed()
                .overscroll(overscrollEffect)
                .absoluteOffset { IntOffset(dragController.offset.fastRoundToInt(), y = 0) }
    ) {
        content()
    }
}
                return super.onPostFling(consumed, available)

@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
        }

    Box(
        modifier =
            modifier
                .onSizeChanged { revealedContentBoxWidth.value = it.width }
                .nestedScroll(nestedScrollConnection)
                .offset { IntOffset(x = offsetAnimatable.value.fastRoundToInt(), y = 0) }
    ) {
        content(null)
        if (lastTarget < 0f && sign > 0f) {
            range = -maxBound()..0f
            return true
        }

        return false
    }

    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
    }
}
+2 −3
Original line number Diff line number Diff line
@@ -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()