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

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

Merge "Add OverscrollFactory parameter to STL.{scene,content} builders (1/2)" into main

parents 64d00c2f 113a70ca
Loading
Loading
Loading
Loading
+4 −2
Original line number Diff line number Diff line
@@ -464,13 +464,15 @@ private class NestedDraggableNode(
        velocity: Velocity,
        performFling: suspend (Velocity) -> Velocity,
    ): Velocity {
        // Make sure we only use the velocity in this draggable orientation.
        val orientationVelocity = velocity.toFloat().toVelocity()
        return if (overscrollEffect != null) {
            overscrollEffect.applyToFling(velocity) { performFling(it) }
            overscrollEffect.applyToFling(orientationVelocity) { performFling(it) }

            // Effects always consume the whole velocity.
            velocity
        } else {
            performFling(velocity)
            performFling(orientationVelocity)
        }
    }

+52 −3
Original line number Diff line number Diff line
@@ -23,7 +23,9 @@ import androidx.compose.foundation.gestures.Orientation
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.unit.Velocity
import com.android.compose.ui.util.HorizontalSpaceVectorConverter
import com.android.compose.ui.util.SpaceVectorConverter
import com.android.compose.ui.util.VerticalSpaceVectorConverter
import kotlin.math.abs
import kotlin.math.sign
import kotlinx.coroutines.CoroutineScope
@@ -40,13 +42,12 @@ interface ContentOverscrollEffect : OverscrollEffect {
}

open class BaseContentOverscrollEffect(
    orientation: Orientation,
    private val animationScope: CoroutineScope,
    private val animationSpec: AnimationSpec<Float>,
) : ContentOverscrollEffect, SpaceVectorConverter by SpaceVectorConverter(orientation) {

) : ContentOverscrollEffect {
    /** The [Animatable] that holds the current overscroll value. */
    private val animatable = Animatable(initialValue = 0f, visibilityThreshold = 0.5f)
    private var lastConverter: SpaceVectorConverter? = null

    override val overscrollDistance: Float
        get() = animatable.value
@@ -58,6 +59,15 @@ open class BaseContentOverscrollEffect(
        delta: Offset,
        source: NestedScrollSource,
        performScroll: (Offset) -> Offset,
    ): Offset {
        val converter = converterOrNull(delta.x, delta.y) ?: return performScroll(delta)
        return converter.applyToScroll(delta, source, performScroll)
    }

    private fun SpaceVectorConverter.applyToScroll(
        delta: Offset,
        source: NestedScrollSource,
        performScroll: (Offset) -> Offset,
    ): Offset {
        val deltaForAxis = delta.toFloat()

@@ -105,6 +115,14 @@ open class BaseContentOverscrollEffect(
    override suspend fun applyToFling(
        velocity: Velocity,
        performFling: suspend (Velocity) -> Velocity,
    ) {
        val converter = converterOrNull(velocity.x, velocity.y) ?: return
        converter.applyToFling(velocity, performFling)
    }

    private suspend fun SpaceVectorConverter.applyToFling(
        velocity: Velocity,
        performFling: suspend (Velocity) -> Velocity,
    ) {
        // We launch a coroutine to ensure the fling animation starts after any pending [snapTo]
        // animations have finished.
@@ -117,4 +135,35 @@ open class BaseContentOverscrollEffect(
            }
        }
    }

    protected fun requireConverter(): SpaceVectorConverter {
        return checkNotNull(lastConverter) {
            "lastConverter is null, make sure to call requireConverter() only when " +
                "overscrollDistance != 0f"
        }
    }

    private fun converterOrNull(x: Float, y: Float): SpaceVectorConverter? {
        val converter: SpaceVectorConverter =
            when {
                x != 0f && y != 0f ->
                    error(
                        "BaseContentOverscrollEffect only supports single orientation scrolls " +
                            "and velocities"
                    )
                x == 0f && y == 0f -> lastConverter ?: return null
                x != 0f -> HorizontalSpaceVectorConverter
                else -> VerticalSpaceVectorConverter
            }

        if (lastConverter != null) {
            check(lastConverter == converter) {
                "BaseContentOverscrollEffect should always be used in the same orientation"
            }
        } else {
            lastConverter = converter
        }

        return converter
    }
}
+42 −31
Original line number Diff line number Diff line
@@ -19,7 +19,7 @@ package com.android.compose.gesture.effect
import androidx.annotation.VisibleForTesting
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.foundation.OverscrollEffect
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.OverscrollFactory
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
@@ -37,24 +37,42 @@ import androidx.compose.ui.unit.dp
import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineScope

/** An [OverscrollEffect] that offsets the content by the overscroll value. */
class OffsetOverscrollEffect(
    orientation: Orientation,
    animationScope: CoroutineScope,
    animationSpec: AnimationSpec<Float>,
) : BaseContentOverscrollEffect(orientation, animationScope, animationSpec) {
    private var _node: DelegatableNode = newNode()
    override val node: DelegatableNode
        get() = _node
@Composable
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
fun rememberOffsetOverscrollEffect(
    animationSpec: AnimationSpec<Float> = MaterialTheme.motionScheme.defaultSpatialSpec()
): OffsetOverscrollEffect {
    val animationScope = rememberCoroutineScope()
    return remember(animationScope, animationSpec) {
        OffsetOverscrollEffect(animationScope, animationSpec)
    }
}

@Composable
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
fun rememberOffsetOverscrollEffectFactory(
    animationSpec: AnimationSpec<Float> = MaterialTheme.motionScheme.defaultSpatialSpec()
): OverscrollFactory {
    val animationScope = rememberCoroutineScope()
    return remember(animationScope, animationSpec) {
        OffsetOverscrollEffectFactory(animationScope, animationSpec)
    }
}

    fun newNode(): DelegatableNode {
        return object : Modifier.Node(), LayoutModifierNode {
            override fun onDetach() {
                super.onDetach()
                // TODO(b/379086317) Remove this workaround: avoid to reuse the same node.
                _node = newNode()
data class OffsetOverscrollEffectFactory(
    private val animationScope: CoroutineScope,
    private val animationSpec: AnimationSpec<Float>,
) : OverscrollFactory {
    override fun createOverscrollEffect(): OverscrollEffect {
        return OffsetOverscrollEffect(animationScope, animationSpec)
    }
}

/** An [OverscrollEffect] that offsets the content by the overscroll value. */
class OffsetOverscrollEffect(animationScope: CoroutineScope, animationSpec: AnimationSpec<Float>) :
    BaseContentOverscrollEffect(animationScope, animationSpec) {
    override val node: DelegatableNode =
        object : Modifier.Node(), LayoutModifierNode {
            override fun MeasureScope.measure(
                measurable: Measurable,
                constraints: Constraints,
@@ -62,7 +80,12 @@ class OffsetOverscrollEffect(
                val placeable = measurable.measure(constraints)
                return layout(placeable.width, placeable.height) {
                    val offsetPx = computeOffset(density = this@measure, overscrollDistance)
                    placeable.placeRelativeWithLayer(position = offsetPx.toIntOffset())
                    if (offsetPx != 0) {
                        placeable.placeRelativeWithLayer(
                            with(requireConverter()) { offsetPx.toIntOffset() }
                        )
                    } else {
                        placeable.placeRelative(0, 0)
                    }
                }
            }
@@ -80,18 +103,6 @@ class OffsetOverscrollEffect(
    }
}

@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun rememberOffsetOverscrollEffect(
    orientation: Orientation,
    animationSpec: AnimationSpec<Float> = MaterialTheme.motionScheme.defaultSpatialSpec(),
): OffsetOverscrollEffect {
    val animationScope = rememberCoroutineScope()
    return remember(orientation, animationScope, animationSpec) {
        OffsetOverscrollEffect(orientation, animationScope, animationSpec)
    }
}

/** This converter lets you change a linear progress into a function of your choice. */
fun interface ProgressConverter {
    fun convert(progress: Float): Float
+4 −4
Original line number Diff line number Diff line
@@ -37,11 +37,11 @@ interface SpaceVectorConverter {

fun SpaceVectorConverter(orientation: Orientation) =
    when (orientation) {
        Orientation.Horizontal -> HorizontalConverter
        Orientation.Vertical -> VerticalConverter
        Orientation.Horizontal -> HorizontalSpaceVectorConverter
        Orientation.Vertical -> VerticalSpaceVectorConverter
    }

private data object HorizontalConverter : SpaceVectorConverter {
data object HorizontalSpaceVectorConverter : SpaceVectorConverter {
    override fun Offset.toFloat() = x

    override fun Velocity.toFloat() = x
@@ -55,7 +55,7 @@ private data object HorizontalConverter : SpaceVectorConverter {
    override fun Int.toIntOffset() = IntOffset(this, 0)
}

private data object VerticalConverter : SpaceVectorConverter {
data object VerticalSpaceVectorConverter : SpaceVectorConverter {
    override fun Offset.toFloat() = y

    override fun Velocity.toFloat() = y
+3 −10
Original line number Diff line number Diff line
@@ -55,10 +55,7 @@ class OffsetOverscrollEffectTest {
        }
    }

    private fun setupOverscrollableBox(
        scrollableOrientation: Orientation,
        overscrollEffectOrientation: Orientation = scrollableOrientation,
    ): LayoutInfo {
    private fun setupOverscrollableBox(scrollableOrientation: Orientation): LayoutInfo {
        val layoutSize: Dp = 200.dp
        var touchSlop: Float by Delegates.notNull()
        // The draggable touch slop, i.e. the min px distance a touch pointer must move before it is
@@ -67,7 +64,7 @@ class OffsetOverscrollEffectTest {
        rule.setContent {
            density = LocalDensity.current
            touchSlop = LocalViewConfiguration.current.touchSlop
            val overscrollEffect = rememberOffsetOverscrollEffect(overscrollEffectOrientation)
            val overscrollEffect = rememberOffsetOverscrollEffect()

            Box(
                Modifier.overscroll(overscrollEffect)
@@ -102,11 +99,7 @@ class OffsetOverscrollEffectTest {

    @Test
    fun applyNoOffset_duringHorizontalOverscroll() {
        val info =
            setupOverscrollableBox(
                scrollableOrientation = Orientation.Vertical,
                overscrollEffectOrientation = Orientation.Horizontal,
            )
        val info = setupOverscrollableBox(scrollableOrientation = Orientation.Vertical)

        rule.onNodeWithTag(BOX_TAG).assertTopPositionInRootIsEqualTo(0.dp)

Loading