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

Commit d5bdc87d authored by Jordan Demeulenaere's avatar Jordan Demeulenaere Committed by Android (Google) Code Review
Browse files

Revert^2 "Add support for swipe with multiple fingers"

71f73aee

Change-Id: I34914759ad45b7d5ee87737edda284de38616955
parent 71f73aee
Loading
Loading
Loading
Loading
+2 −3
Original line number Original line Diff line number Diff line
@@ -2,7 +2,6 @@ package com.android.compose.animation.scene


import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import kotlinx.coroutines.CoroutineScope


interface GestureHandler {
interface GestureHandler {
    val draggable: DraggableHandler
    val draggable: DraggableHandler
@@ -10,9 +9,9 @@ interface GestureHandler {
}
}


interface DraggableHandler {
interface DraggableHandler {
    suspend fun onDragStarted(coroutineScope: CoroutineScope, startedPosition: Offset)
    fun onDragStarted(startedPosition: Offset, pointersDown: Int = 1)
    fun onDelta(pixels: Float)
    fun onDelta(pixels: Float)
    suspend fun onDragStopped(coroutineScope: CoroutineScope, velocity: Float)
    fun onDragStopped(velocity: Float)
}
}


interface NestedScrollHandler {
interface NestedScrollHandler {
+191 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2023 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.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.awaitHorizontalTouchSlopOrCancellation
import androidx.compose.foundation.gestures.awaitVerticalTouchSlopOrCancellation
import androidx.compose.foundation.gestures.horizontalDrag
import androidx.compose.foundation.gestures.verticalDrag
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerId
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.PointerInputScope
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.input.pointer.util.VelocityTracker
import androidx.compose.ui.input.pointer.util.addPointerInputChange
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.util.fastForEach

/**
 * Make an element draggable in the given [orientation].
 *
 * The main difference with [multiPointerDraggable] and
 * [androidx.compose.foundation.gestures.draggable] is that [onDragStarted] also receives the number
 * of pointers that are down when the drag is started. If you don't need this information, you
 * should use `draggable` instead.
 *
 * Note that the current implementation is trivial: we wait for the touch slope on the *first* down
 * pointer, then we count the number of distinct pointers that are down right before calling
 * [onDragStarted]. This means that the drag won't start when a first pointer is down (but not
 * dragged) and a second pointer is down and dragged. This is an implementation detail that might
 * change in the future.
 */
// TODO(b/291055080): Migrate to the Modifier.Node API.
@Composable
internal fun Modifier.multiPointerDraggable(
    orientation: Orientation,
    enabled: Boolean,
    startDragImmediately: Boolean,
    onDragStarted: (startedPosition: Offset, pointersDown: Int) -> Unit,
    onDragDelta: (Float) -> Unit,
    onDragStopped: (velocity: Float) -> Unit,
): Modifier {
    val onDragStarted by rememberUpdatedState(onDragStarted)
    val onDragStopped by rememberUpdatedState(onDragStopped)
    val onDragDelta by rememberUpdatedState(onDragDelta)
    val startDragImmediately by rememberUpdatedState(startDragImmediately)

    val velocityTracker = remember { VelocityTracker() }
    val maxFlingVelocity =
        LocalViewConfiguration.current.maximumFlingVelocity.let { max ->
            val maxF = max.toFloat()
            Velocity(maxF, maxF)
        }

    return this.pointerInput(enabled, orientation, maxFlingVelocity) {
        if (!enabled) {
            return@pointerInput
        }

        val onDragStart: (Offset, Int) -> Unit = { startedPosition, pointersDown ->
            velocityTracker.resetTracking()
            onDragStarted(startedPosition, pointersDown)
        }

        val onDragCancel: () -> Unit = { onDragStopped(/* velocity= */ 0f) }

        val onDragEnd: () -> Unit = {
            val velocity = velocityTracker.calculateVelocity(maxFlingVelocity)
            onDragStopped(
                when (orientation) {
                    Orientation.Horizontal -> velocity.x
                    Orientation.Vertical -> velocity.y
                }
            )
        }

        val onDrag: (change: PointerInputChange, dragAmount: Float) -> Unit = { change, amount ->
            velocityTracker.addPointerInputChange(change)
            onDragDelta(amount)
        }

        detectDragGestures(
            orientation = orientation,
            startDragImmediately = { startDragImmediately },
            onDragStart = onDragStart,
            onDragEnd = onDragEnd,
            onDragCancel = onDragCancel,
            onDrag = onDrag,
        )
    }
}

/**
 * Detect drag gestures in the given [orientation].
 *
 * This function is a mix of [androidx.compose.foundation.gestures.awaitDownAndSlop] and
 * [androidx.compose.foundation.gestures.detectVerticalDragGestures] to add support for:
 * 1) starting the gesture immediately without requiring a drag >= touch slope;
 * 2) passing the number of pointers down to [onDragStart].
 */
private suspend fun PointerInputScope.detectDragGestures(
    orientation: Orientation,
    startDragImmediately: () -> Boolean,
    onDragStart: (startedPosition: Offset, pointersDown: Int) -> Unit,
    onDragEnd: () -> Unit,
    onDragCancel: () -> Unit,
    onDrag: (change: PointerInputChange, dragAmount: Float) -> Unit,
) {
    awaitEachGesture {
        val initialDown = awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
        var overSlop = 0f
        val drag =
            if (startDragImmediately()) {
                initialDown.consume()
                initialDown
            } else {
                val down = awaitFirstDown(requireUnconsumed = false)
                val onSlopReached = { change: PointerInputChange, over: Float ->
                    change.consume()
                    overSlop = over
                }

                // TODO(b/291055080): Replace by await[Orientation]PointerSlopOrCancellation once
                // it is public.
                when (orientation) {
                    Orientation.Horizontal ->
                        awaitHorizontalTouchSlopOrCancellation(down.id, onSlopReached)
                    Orientation.Vertical ->
                        awaitVerticalTouchSlopOrCancellation(down.id, onSlopReached)
                }
            }

        if (drag != null) {
            // Count the number of pressed pointers.
            val pressed = mutableSetOf<PointerId>()
            currentEvent.changes.fastForEach { change ->
                if (change.pressed) {
                    pressed.add(change.id)
                }
            }

            onDragStart(drag.position, pressed.size)
            onDrag(drag, overSlop)

            val successful =
                when (orientation) {
                    Orientation.Horizontal ->
                        horizontalDrag(drag.id) {
                            onDrag(it, it.positionChange().x)
                            it.consume()
                        }
                    Orientation.Vertical ->
                        verticalDrag(drag.id) {
                            onDrag(it, it.positionChange().y)
                            it.consume()
                        }
                }

            if (successful) {
                onDragEnd()
            } else {
                onDragCancel()
            }
        }
    }
}
+0 −17
Original line number Original line Diff line number Diff line
@@ -16,7 +16,6 @@


package com.android.compose.animation.scene
package com.android.compose.animation.scene


import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.State
@@ -101,19 +100,3 @@ private class SceneScopeImpl(
        MovableElement(layoutImpl, scene, key, modifier, content)
        MovableElement(layoutImpl, scene, key, modifier, content)
    }
    }
}
}

/** The destination scene when swiping up or left from [upOrLeft]. */
internal fun Scene.upOrLeft(orientation: Orientation): SceneKey? {
    return when (orientation) {
        Orientation.Vertical -> userActions[Swipe.Up]
        Orientation.Horizontal -> userActions[Swipe.Left]
    }
}

/** The destination scene when swiping down or right from [downOrRight]. */
internal fun Scene.downOrRight(orientation: Orientation): SceneKey? {
    return when (orientation) {
        Orientation.Vertical -> userActions[Swipe.Down]
        Orientation.Horizontal -> userActions[Swipe.Right]
    }
}
+6 −5
Original line number Original line Diff line number Diff line
@@ -16,6 +16,7 @@


package com.android.compose.animation.scene
package com.android.compose.animation.scene


import androidx.compose.foundation.gestures.Orientation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.runtime.remember
@@ -191,9 +192,9 @@ data class Swipe(
    }
    }
}
}


enum class SwipeDirection {
enum class SwipeDirection(val orientation: Orientation) {
    Up,
    Up(Orientation.Vertical),
    Down,
    Down(Orientation.Vertical),
    Left,
    Left(Orientation.Horizontal),
    Right,
    Right(Orientation.Horizontal),
}
}
+62 −16
Original line number Original line Diff line number Diff line
@@ -22,8 +22,6 @@ import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.getValue
@@ -55,7 +53,7 @@ internal fun Modifier.swipeToScene(


    /** Whether swipe should be enabled in the given [orientation]. */
    /** Whether swipe should be enabled in the given [orientation]. */
    fun Scene.shouldEnableSwipes(orientation: Orientation): Boolean =
    fun Scene.shouldEnableSwipes(orientation: Orientation): Boolean =
        upOrLeft(orientation) != null || downOrRight(orientation) != null
        userActions.keys.any { it is Swipe && it.direction.orientation == orientation }


    val currentScene = gestureHandler.currentScene
    val currentScene = gestureHandler.currentScene
    val canSwipe = currentScene.shouldEnableSwipes(orientation)
    val canSwipe = currentScene.shouldEnableSwipes(orientation)
@@ -68,8 +66,7 @@ internal fun Modifier.swipeToScene(
        )
        )


    return nestedScroll(connection = gestureHandler.nestedScroll.connection)
    return nestedScroll(connection = gestureHandler.nestedScroll.connection)
        .draggable(
        .multiPointerDraggable(
            state = rememberDraggableState(onDelta = gestureHandler.draggable::onDelta),
            orientation = orientation,
            orientation = orientation,
            enabled = gestureHandler.isDrivingTransition || canSwipe,
            enabled = gestureHandler.isDrivingTransition || canSwipe,
            // Immediately start the drag if this our [transition] is currently animating to a scene
            // Immediately start the drag if this our [transition] is currently animating to a scene
@@ -80,6 +77,7 @@ internal fun Modifier.swipeToScene(
                    gestureHandler.isAnimatingOffset &&
                    gestureHandler.isAnimatingOffset &&
                    !canOppositeSwipe,
                    !canOppositeSwipe,
            onDragStarted = gestureHandler.draggable::onDragStarted,
            onDragStarted = gestureHandler.draggable::onDragStarted,
            onDragDelta = gestureHandler.draggable::onDelta,
            onDragStopped = gestureHandler.draggable::onDragStopped,
            onDragStopped = gestureHandler.draggable::onDragStopped,
        )
        )
}
}
@@ -159,7 +157,7 @@ class SceneGestureHandler(


    internal var gestureWithPriority: Any? = null
    internal var gestureWithPriority: Any? = null


    internal fun onDragStarted() {
    internal fun onDragStarted(pointersDown: Int) {
        if (isDrivingTransition) {
        if (isDrivingTransition) {
            // This [transition] was already driving the animation: simply take over it.
            // This [transition] was already driving the animation: simply take over it.
            // Stop animating and start from where the current offset.
            // Stop animating and start from where the current offset.
@@ -199,6 +197,26 @@ class SceneGestureHandler(
                Orientation.Vertical -> layoutImpl.size.height
                Orientation.Vertical -> layoutImpl.size.height
            }.toFloat()
            }.toFloat()


        swipeTransition.actionUpOrLeft =
            Swipe(
                direction =
                    when (orientation) {
                        Orientation.Horizontal -> SwipeDirection.Left
                        Orientation.Vertical -> SwipeDirection.Up
                    },
                pointerCount = pointersDown,
            )

        swipeTransition.actionDownOrRight =
            Swipe(
                direction =
                    when (orientation) {
                        Orientation.Horizontal -> SwipeDirection.Right
                        Orientation.Vertical -> SwipeDirection.Down
                    },
                pointerCount = pointersDown,
            )

        if (swipeTransition.absoluteDistance > 0f) {
        if (swipeTransition.absoluteDistance > 0f) {
            transitionState = swipeTransition
            transitionState = swipeTransition
        }
        }
@@ -246,11 +264,15 @@ class SceneGestureHandler(
        // to the next screen or go back to the previous one.
        // to the next screen or go back to the previous one.
        val offset = swipeTransition.dragOffset
        val offset = swipeTransition.dragOffset
        val absoluteDistance = swipeTransition.absoluteDistance
        val absoluteDistance = swipeTransition.absoluteDistance
        if (offset <= -absoluteDistance && fromScene.upOrLeft(orientation) == toScene.key) {
        if (
            offset <= -absoluteDistance &&
                fromScene.userActions[swipeTransition.actionUpOrLeft] == toScene.key
        ) {
            swipeTransition.dragOffset += absoluteDistance
            swipeTransition.dragOffset += absoluteDistance
            swipeTransition._fromScene = toScene
            swipeTransition._fromScene = toScene
        } else if (
        } else if (
            offset >= absoluteDistance && fromScene.downOrRight(orientation) == toScene.key
            offset >= absoluteDistance &&
                fromScene.userActions[swipeTransition.actionDownOrRight] == toScene.key
        ) {
        ) {
            swipeTransition.dragOffset -= absoluteDistance
            swipeTransition.dragOffset -= absoluteDistance
            swipeTransition._fromScene = toScene
            swipeTransition._fromScene = toScene
@@ -272,8 +294,8 @@ class SceneGestureHandler(
                Orientation.Vertical -> layoutImpl.size.height
                Orientation.Vertical -> layoutImpl.size.height
            }.toFloat()
            }.toFloat()


        val upOrLeft = upOrLeft(orientation)
        val upOrLeft = userActions[swipeTransition.actionUpOrLeft]
        val downOrRight = downOrRight(orientation)
        val downOrRight = userActions[swipeTransition.actionDownOrRight]


        // Compute the target scene depending on the current offset.
        // Compute the target scene depending on the current offset.
        return when {
        return when {
@@ -516,6 +538,10 @@ class SceneGestureHandler(
        var _distance by mutableFloatStateOf(0f)
        var _distance by mutableFloatStateOf(0f)
        val distance: Float
        val distance: Float
            get() = _distance
            get() = _distance

        /** The [UserAction]s associated to this swipe. */
        var actionUpOrLeft: UserAction = Back
        var actionDownOrRight: UserAction = Back
    }
    }


    companion object {
    companion object {
@@ -526,9 +552,9 @@ class SceneGestureHandler(
private class SceneDraggableHandler(
private class SceneDraggableHandler(
    private val gestureHandler: SceneGestureHandler,
    private val gestureHandler: SceneGestureHandler,
) : DraggableHandler {
) : DraggableHandler {
    override suspend fun onDragStarted(coroutineScope: CoroutineScope, startedPosition: Offset) {
    override fun onDragStarted(startedPosition: Offset, pointersDown: Int) {
        gestureHandler.gestureWithPriority = this
        gestureHandler.gestureWithPriority = this
        gestureHandler.onDragStarted()
        gestureHandler.onDragStarted(pointersDown)
    }
    }


    override fun onDelta(pixels: Float) {
    override fun onDelta(pixels: Float) {
@@ -537,7 +563,7 @@ private class SceneDraggableHandler(
        }
        }
    }
    }


    override suspend fun onDragStopped(coroutineScope: CoroutineScope, velocity: Float) {
    override fun onDragStopped(velocity: Float) {
        if (gestureHandler.gestureWithPriority == this) {
        if (gestureHandler.gestureWithPriority == this) {
            gestureHandler.gestureWithPriority = null
            gestureHandler.gestureWithPriority = null
            gestureHandler.onDragStopped(velocity = velocity, canChangeScene = true)
            gestureHandler.onDragStopped(velocity = velocity, canChangeScene = true)
@@ -580,11 +606,31 @@ class SceneNestedScrollHandler(
        // moving on to the next scene.
        // moving on to the next scene.
        var gestureStartedOnNestedChild = false
        var gestureStartedOnNestedChild = false


        val actionUpOrLeft =
            Swipe(
                direction =
                    when (gestureHandler.orientation) {
                        Orientation.Horizontal -> SwipeDirection.Left
                        Orientation.Vertical -> SwipeDirection.Up
                    },
                pointerCount = 1,
            )

        val actionDownOrRight =
            Swipe(
                direction =
                    when (gestureHandler.orientation) {
                        Orientation.Horizontal -> SwipeDirection.Right
                        Orientation.Vertical -> SwipeDirection.Down
                    },
                pointerCount = 1,
            )

        fun findNextScene(amount: Float): SceneKey? {
        fun findNextScene(amount: Float): SceneKey? {
            val fromScene = gestureHandler.currentScene
            val fromScene = gestureHandler.currentScene
            return when {
            return when {
                amount < 0f -> fromScene.upOrLeft(gestureHandler.orientation)
                amount < 0f -> fromScene.userActions[actionUpOrLeft]
                amount > 0f -> fromScene.downOrRight(gestureHandler.orientation)
                amount > 0f -> fromScene.userActions[actionDownOrRight]
                else -> null
                else -> null
            }
            }
        }
        }
@@ -625,7 +671,7 @@ class SceneNestedScrollHandler(
            onStart = {
            onStart = {
                gestureHandler.gestureWithPriority = this
                gestureHandler.gestureWithPriority = this
                priorityScene = nextScene
                priorityScene = nextScene
                gestureHandler.onDragStarted()
                gestureHandler.onDragStarted(pointersDown = 1)
            },
            },
            onScroll = { offsetAvailable ->
            onScroll = { offsetAvailable ->
                if (gestureHandler.gestureWithPriority != this) {
                if (gestureHandler.gestureWithPriority != this) {
Loading