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

Commit 9c0fbb1a authored by Jordan Demeulenaere's avatar Jordan Demeulenaere
Browse files

Make it possible to compute the swipe distance lazily

This CL makes it possible to compute the swipe distance of swipes
lazily, so that the distance can depend on the size or position of
elements from the scene we are transitioning to.

The way it works is pretty simple: UserActionDistance.absoluteDistance
can return 0f until the swipe distance can be computed. This CL also
exposes a UserActionDistanceScope to get the target size and offset of
an element inside a scene, which are usually useful to compute swipe
distances that depend on an element target state.

Note that some of the code in this CL is going to be moved in
ag/26316632.

Bug: 308961608
Test: SwipeToSceneTest
Flag: N/A
Change-Id: If70c48de0aba7a793942badcb5a24993277302b1
parent 7b5f3f42
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -17,7 +17,6 @@
package com.android.systemui.scene.ui.composable

import androidx.compose.foundation.gestures.Orientation
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import com.android.compose.animation.scene.Back
import com.android.compose.animation.scene.Edge as ComposeAwareEdge
@@ -27,6 +26,7 @@ import com.android.compose.animation.scene.SwipeDirection
import com.android.compose.animation.scene.TransitionKey as ComposeAwareTransitionKey
import com.android.compose.animation.scene.UserAction as ComposeAwareUserAction
import com.android.compose.animation.scene.UserActionDistance as ComposeAwareUserActionDistance
import com.android.compose.animation.scene.UserActionDistanceScope
import com.android.compose.animation.scene.UserActionResult as ComposeAwareUserActionResult
import com.android.systemui.scene.shared.model.Direction
import com.android.systemui.scene.shared.model.Edge
@@ -89,7 +89,7 @@ fun UserActionResult.asComposeAware(): ComposeAwareUserActionResult {
fun UserActionDistance.asComposeAware(): ComposeAwareUserActionDistance {
    val composeUnware = this
    return object : ComposeAwareUserActionDistance {
        override fun Density.absoluteDistance(
        override fun UserActionDistanceScope.absoluteDistance(
            fromSceneSize: IntSize,
            orientation: Orientation,
        ): Float {
+76 −24
Original line number Diff line number Diff line
@@ -27,7 +27,6 @@ import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.round
@@ -285,16 +284,21 @@ internal class SceneGestureHandler(
    ): Pair<Scene, Float> {
        val toScene = swipeTransition._toScene
        val fromScene = swipeTransition._fromScene
        val absoluteDistance = swipeTransition.distance.absoluteValue
        val distance = swipeTransition.distance()

        // If the swipe was not committed, don't do anything.
        if (swipeTransition._currentScene != toScene) {
        // If the swipe was not committed or if the swipe distance is not computed yet, don't do
        // anything.
        if (
            swipeTransition._currentScene != toScene ||
                distance == SwipeTransition.DistanceUnspecified
        ) {
            return fromScene to 0f
        }

        // If the offset is past the distance then let's change fromScene so that the user can swipe
        // to the next screen or go back to the previous one.
        val offset = swipeTransition.dragOffset
        val absoluteDistance = distance.absoluteValue
        return if (offset <= -absoluteDistance && swipes!!.upOrLeftResult?.toScene == toScene.key) {
            toScene to absoluteDistance
        } else if (
@@ -347,10 +351,11 @@ internal class SceneGestureHandler(

            // Compute the destination scene (and therefore offset) to settle in.
            val offset = swipeTransition.dragOffset
            val distance = swipeTransition.distance
            val distance = swipeTransition.distance()
            var targetScene: Scene
            var targetOffset: Float
            if (
                distance != SwipeTransition.DistanceUnspecified &&
                    shouldCommitSwipe(
                        offset,
                        distance,
@@ -372,7 +377,15 @@ internal class SceneGestureHandler(
                // We wanted to change to a new scene but we are not allowed to, so we animate back
                // to the current scene.
                targetScene = swipeTransition._currentScene
                targetOffset = if (targetScene == fromScene) 0f else distance
                targetOffset =
                    if (targetScene == fromScene) {
                        0f
                    } else {
                        check(distance != SwipeTransition.DistanceUnspecified) {
                            "distance is equal to ${SwipeTransition.DistanceUnspecified}"
                        }
                        distance
                    }
            }

            animateTo(targetScene = targetScene, targetOffset = targetOffset)
@@ -460,21 +473,42 @@ private fun SwipeTransition(
    val upOrLeftResult = swipes.upOrLeftResult
    val downOrRightResult = swipes.downOrRightResult
    val userActionDistance = result.distance ?: DefaultSwipeDistance
    val absoluteDistance =

    // The absolute distance of the gesture. Note that the UserActionDistance might return 0f or a
    // negative value at first if it needs the size or offset of an element that is not composed yet
    // when computing the distance. We call UserActionDistance.absoluteDistance() until it returns a
    // value different than 0.
    var lastAbsoluteDistance = 0f
    val absoluteDistance: () -> Float = {
        if (lastAbsoluteDistance > 0f) {
            lastAbsoluteDistance
        } else {
            with(userActionDistance) {
            layoutImpl.density.absoluteDistance(fromScene.targetSize, orientation)
                    layoutImpl.userActionDistanceScope.absoluteDistance(
                        fromScene.targetSize,
                        orientation,
                    )
                }
                .also { lastAbsoluteDistance = it }
        }
    }

    // The signed distance of the gesture.
    val distance: () -> Float = {
        val absoluteDistance = absoluteDistance()
        when {
            absoluteDistance <= 0f -> SwipeTransition.DistanceUnspecified
            result == upOrLeftResult -> -absoluteDistance
            result == downOrRightResult -> absoluteDistance
            else -> error("Unknown result $result ($upOrLeftResult $downOrRightResult)")
        }
    }

    return SwipeTransition(
        key = result.transitionKey,
        _fromScene = fromScene,
        _toScene = layoutImpl.scene(result.toScene),
        distance =
            when (result) {
                upOrLeftResult -> -absoluteDistance
                downOrRightResult -> absoluteDistance
                else -> error("Unknown result $result ($upOrLeftResult $downOrRightResult)")
            },
        distance = distance,
    )
}

@@ -482,11 +516,16 @@ private class SwipeTransition(
    val key: TransitionKey?,
    val _fromScene: Scene,
    val _toScene: Scene,

    /**
     * The signed distance between [fromScene] and [toScene]. It is negative if [fromScene] is above
     * or to the left of [toScene]
     * or to the left of [toScene].
     *
     * Note that this distance can be equal to [DistanceUnspecified] during the first frame of a
     * transition when the distance depends on the size or position of an element that is composed
     * in the scene we are going to.
     */
    val distance: Float,
    val distance: () -> Float,
) : TransitionState.Transition(_fromScene.key, _toScene.key) {
    var _currentScene by mutableStateOf(_fromScene)
    override val currentScene: SceneKey
@@ -494,7 +533,16 @@ private class SwipeTransition(

    override val progress: Float
        get() {
            // Important: If we are going to return early because distance is equal to 0, we should
            // still make sure we read the offset before returning so that the calling code still
            // subscribes to the offset value.
            val offset = if (isAnimatingOffset) offsetAnimatable.value else dragOffset

            val distance = distance()
            if (distance == DistanceUnspecified) {
                return 0f
            }

            return offset / distance
        }

@@ -571,10 +619,14 @@ private class SwipeTransition(

        finishOffsetAnimation()
    }

    companion object {
        const val DistanceUnspecified = 0f
    }
}

private object DefaultSwipeDistance : UserActionDistance {
    override fun Density.absoluteDistance(
    override fun UserActionDistanceScope.absoluteDistance(
        fromSceneSize: IntSize,
        orientation: Orientation,
    ): Float {
+34 −4
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
@@ -415,15 +416,44 @@ interface UserActionDistance {
    /**
     * Return the **absolute** distance of the user action given the size of the scene we are
     * animating from and the [orientation].
     *
     * Note: This function will be called for each drag event until it returns a value > 0f. This
     * for instance allows you to return 0f or a negative value until the first layout pass of a
     * scene, so that you can use the size and position of elements in the scene we are
     * transitioning to when computing this absolute distance.
     */
    fun Density.absoluteDistance(fromSceneSize: IntSize, orientation: Orientation): Float
    fun UserActionDistanceScope.absoluteDistance(
        fromSceneSize: IntSize,
        orientation: Orientation
    ): Float
}

interface UserActionDistanceScope : Density {
    /**
     * Return the *target* size of [this] element in the given [scene], i.e. the size of the element
     * when idle, or `null` if the element is not composed and measured in that scene (yet).
     */
    fun ElementKey.targetSize(scene: SceneKey): IntSize?

    /**
     * Return the *target* offset of [this] element in the given [scene], i.e. the size of the
     * element when idle, or `null` if the element is not composed and placed in that scene (yet).
     */
    fun ElementKey.targetOffset(scene: SceneKey): Offset?

    /**
     * Return the *target* size of [this] scene, i.e. the size of the scene when idle, or `null` if
     * the scene was never composed.
     */
    fun SceneKey.targetSize(): IntSize?
}

/** The user action has a fixed [absoluteDistance]. */
private class FixedDistance(private val distance: Dp) : UserActionDistance {
    override fun Density.absoluteDistance(fromSceneSize: IntSize, orientation: Orientation): Float {
        return distance.toPx()
    }
    override fun UserActionDistanceScope.absoluteDistance(
        fromSceneSize: IntSize,
        orientation: Orientation,
    ): Float = distance.toPx()
}

/**
+9 −0
Original line number Diff line number Diff line
@@ -96,9 +96,18 @@ internal class SceneTransitionLayoutImpl(
                ?: mutableMapOf<ValueKey, MutableMap<ElementKey?, SnapshotStateMap<SceneKey, *>>>()
                    .also { _sharedValues = it }

    // TODO(b/317958526): Lazily allocate scene gesture handlers the first time they are needed.
    private val horizontalGestureHandler: SceneGestureHandler
    private val verticalGestureHandler: SceneGestureHandler

    private var _userActionDistanceScope: UserActionDistanceScope? = null
    internal val userActionDistanceScope: UserActionDistanceScope
        get() =
            _userActionDistanceScope
                ?: UserActionDistanceScopeImpl(layoutImpl = this).also {
                    _userActionDistanceScope = it
                }

    init {
        updateScenes(builder)

+46 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.compose.animation.scene

import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.IntSize

internal class UserActionDistanceScopeImpl(
    private val layoutImpl: SceneTransitionLayoutImpl,
) : UserActionDistanceScope {
    override val density: Float
        get() = layoutImpl.density.density

    override val fontScale: Float
        get() = layoutImpl.density.fontScale

    override fun ElementKey.targetSize(scene: SceneKey): IntSize? {
        return layoutImpl.elements[this]?.sceneStates?.get(scene)?.targetSize.takeIf {
            it != Element.SizeUnspecified
        }
    }

    override fun ElementKey.targetOffset(scene: SceneKey): Offset? {
        return layoutImpl.elements[this]?.sceneStates?.get(scene)?.targetOffset.takeIf {
            it != Offset.Unspecified
        }
    }

    override fun SceneKey.targetSize(): IntSize? {
        return layoutImpl.scenes[this]?.targetSize.takeIf { it != IntSize.Zero }
    }
}
Loading