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

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

Merge changes from topics "stl-edge-swipe", "stl-multiple-pointers" into main

* changes:
  Add support for swipes started from an edge
  Add support for swipe with multiple fingers
parents 7f2312f6 13489cae
Loading
Loading
Loading
Loading
+75 −0
Original line number 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.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp

interface EdgeDetector {
    /**
     * Return the [Edge] associated to [position] inside a layout of size [layoutSize], given
     * [density] and [orientation].
     */
    fun edge(
        layoutSize: IntSize,
        position: IntOffset,
        density: Density,
        orientation: Orientation,
    ): Edge?
}

val DefaultEdgeDetector = FixedSizeEdgeDetector(40.dp)

/** An [EdgeDetector] that detects edges assuming a fixed edge size of [size]. */
class FixedSizeEdgeDetector(val size: Dp) : EdgeDetector {
    override fun edge(
        layoutSize: IntSize,
        position: IntOffset,
        density: Density,
        orientation: Orientation,
    ): Edge? {
        val axisSize: Int
        val axisPosition: Int
        val topOrLeft: Edge
        val bottomOrRight: Edge
        when (orientation) {
            Orientation.Horizontal -> {
                axisSize = layoutSize.width
                axisPosition = position.x
                topOrLeft = Edge.Left
                bottomOrRight = Edge.Right
            }
            Orientation.Vertical -> {
                axisSize = layoutSize.height
                axisPosition = position.y
                topOrLeft = Edge.Top
                bottomOrRight = Edge.Bottom
            }
        }

        val sizePx = with(density) { size.toPx() }
        return when {
            axisPosition <= sizePx -> topOrLeft
            axisPosition >= axisSize - sizePx -> bottomOrRight
            else -> null
        }
    }
}
+2 −3
Original line number 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.input.nestedscroll.NestedScrollConnection
import kotlinx.coroutines.CoroutineScope

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

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

interface NestedScrollHandler {
+191 −0
Original line number 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 Diff line number Diff line
@@ -16,7 +16,6 @@

package com.android.compose.animation.scene

import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
@@ -101,19 +100,3 @@ private class SceneScopeImpl(
        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]
    }
}
+11 −6
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.compose.animation.scene

import androidx.compose.foundation.gestures.Orientation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
@@ -37,6 +38,7 @@ import androidx.compose.ui.platform.LocalDensity
 *   instance by triggering back navigation or by swiping to a new scene.
 * @param transitions the definition of the transitions used to animate a change of scene.
 * @param state the observable state of this layout.
 * @param edgeDetector the edge detector used to detect which edge a swipe is started from, if any.
 * @param scenes the configuration of the different scenes of this layout.
 */
@Composable
@@ -46,6 +48,7 @@ fun SceneTransitionLayout(
    transitions: SceneTransitions,
    modifier: Modifier = Modifier,
    state: SceneTransitionLayoutState = remember { SceneTransitionLayoutState(currentScene) },
    edgeDetector: EdgeDetector = DefaultEdgeDetector,
    scenes: SceneTransitionLayoutScope.() -> Unit,
) {
    val density = LocalDensity.current
@@ -56,15 +59,17 @@ fun SceneTransitionLayout(
            transitions,
            state,
            density,
            edgeDetector,
        )
    }

    layoutImpl.onChangeScene = onChangeScene
    layoutImpl.transitions = transitions
    layoutImpl.density = density
    layoutImpl.edgeDetector = edgeDetector

    layoutImpl.setScenes(scenes)
    layoutImpl.setCurrentScene(currentScene)

    layoutImpl.Content(modifier)
}

@@ -191,9 +196,9 @@ data class Swipe(
    }
}

enum class SwipeDirection {
    Up,
    Down,
    Left,
    Right,
enum class SwipeDirection(val orientation: Orientation) {
    Up(Orientation.Vertical),
    Down(Orientation.Vertical),
    Left(Orientation.Horizontal),
    Right(Orientation.Horizontal),
}
Loading