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

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

Merge changes If247da11,I493386c8 into main

* changes:
  Allow specifying the output range of the debugger visualization
  Allow specifying semantics on a MotionSpec level
parents 3a1d8861 927f1ec7
Loading
Loading
Loading
Loading
+36 −5
Original line number Diff line number Diff line
@@ -53,12 +53,19 @@ import com.android.mechanics.spec.InputDirection
import com.android.mechanics.spec.Mapping
import com.android.mechanics.spec.MotionSpec
import com.android.mechanics.spec.SegmentKey
import com.android.mechanics.spec.SemanticKey
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.min
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch

/** Computes the output range for a debug visualization given a spec and an input range. */
typealias OutputRangeFn =
    (spec: MotionSpec, inputRange: ClosedFloatingPointRange<Float>) -> ClosedFloatingPointRange<
            Float
        >

/**
 * A debug visualization of the [motionValue].
 *
@@ -75,13 +82,13 @@ fun DebugMotionValueVisualization(
    motionValue: MotionValue,
    inputRange: ClosedFloatingPointRange<Float>,
    modifier: Modifier = Modifier,
    outputRange: OutputRangeFn = DebugMotionValueVisualization.default,
    maxAgeMillis: Long = 1000L,
) {
    val spec = motionValue.spec
    val outputRange = remember(spec, inputRange) { spec.computeOutputValueRange(inputRange) }

    val inspector = remember(motionValue) { motionValue.debugInspector() }

    val computedOutputRange =
        remember(motionValue.spec, inputRange) { outputRange(motionValue.spec, inputRange) }
    DisposableEffect(inspector) { onDispose { inspector.dispose() } }

    val colorScheme = MaterialTheme.colorScheme
@@ -98,7 +105,7 @@ fun DebugMotionValueVisualization(
                .debugMotionSpecGraph(
                    primarySpec,
                    inputRange,
                    outputRange,
                    computedOutputRange,
                    axisColor,
                    specColor,
                    activeSegment,
@@ -107,12 +114,36 @@ fun DebugMotionValueVisualization(
                    motionValue,
                    valueColor,
                    inputRange,
                    outputRange,
                    computedOutputRange,
                    maxAgeMillis,
                )
    )
}

object DebugMotionValueVisualization {

    /**
     * Returns the output range as annotated in the spec using [OutputRangeKey], or
     * [minMaxOutputRange] is not specified.
     */
    val default: OutputRangeFn = { spec, inputRange ->
        spec.semanticState(OutputRangeKey) ?: spec.computeOutputValueRange(inputRange)
    }
    /**
     * Returns an output range containing the min and max output values at each breakpoint within
     * the input range
     */
    val minMaxOutputRange: OutputRangeFn = { spec, inputRange ->
        spec.computeOutputValueRange(inputRange)
    }

    /** Returns an output range that is identical to the input range */
    val inputRange: OutputRangeFn = { _, inputRange -> inputRange }

    /** Defines the output range for the visualization. */
    val OutputRangeKey = SemanticKey<ClosedFloatingPointRange<Float>>("visualizationOutputRange")
}

/**
 * Draws a full-sized debug visualization of [spec].
 *
+15 −3
Original line number Diff line number Diff line
@@ -31,12 +31,14 @@ import com.android.mechanics.spring.SpringParameters
 *   caused by setting this new spec.
 * @param segmentHandlers allow for custom segment-change logic, when the `MotionValue` runtime
 *   would leave the [SegmentKey].
 * @param semantics semantics applied to the complete [MotionSpec]
 */
data class MotionSpec(
    val maxDirection: DirectionalMotionSpec,
    val minDirection: DirectionalMotionSpec = maxDirection,
    val resetSpring: SpringParameters = DefaultResetSpring,
    val segmentHandlers: Map<SegmentKey, OnChangeSegmentHandler> = emptyMap(),
    val semantics: List<SemanticValue<*>> = emptyList(),
) {

    /** The [DirectionalMotionSpec] for the specified [direction]. */
@@ -52,6 +54,16 @@ data class MotionSpec(
        return get(segmentKey.direction).findSegmentIndex(segmentKey) != -1
    }

    /**
     * The semantic state for [key], as defined for the [MotionSpec].
     *
     * Returns `null` if no semantic value with [key] is defined.
     */
    fun <T> semanticState(key: SemanticKey<T>): T? {
        @Suppress("UNCHECKED_CAST")
        return semantics.fastFirstOrNull { it.key == key }?.value as T?
    }

    /**
     * The semantic state for [key] at segment with [segmentKey].
     *
@@ -60,7 +72,8 @@ data class MotionSpec(
     */
    fun <T> semanticState(key: SemanticKey<T>, segmentKey: SegmentKey): T? {
        with(get(segmentKey.direction)) {
            val semanticValues = semantics.fastFirstOrNull { it.key == key } ?: return null
            val semanticValues =
                semantics.fastFirstOrNull { it.key == key } ?: return semanticState(key)
            val segmentIndex = findSegmentIndex(segmentKey)
            if (segmentIndex < 0) throw NoSuchElementException()

@@ -154,8 +167,7 @@ data class MotionSpec(
 *   element, and [Breakpoint.maxLimit] as the last element.
 * @param mappings All mappings in between the breakpoints, thus must always contain
 *   `breakpoints.size - 1` elements.
 * @param semantics semantics provided by this spec, must only reference to breakpoint keys included
 *   in [breakpoints].
 * @param semantics Semantics that apply to the [MotionSpec].
 */
data class DirectionalMotionSpec(
    val breakpoints: List<Breakpoint>,
+9 −8
Original line number Diff line number Diff line
@@ -33,9 +33,9 @@ fun MotionBuilderContext.spatialMotionSpec(
    baseMapping: Mapping = Mapping.Identity,
    defaultSpring: SpringParameters = this.spatial.default,
    resetSpring: SpringParameters = defaultSpring,
    baseSemantics: List<SemanticValue<*>> = emptyList(),
    semantics: List<SemanticValue<*>> = emptyList(),
    init: MotionSpecBuilderScope.() -> Unit,
) = motionSpec(baseMapping, defaultSpring, resetSpring, baseSemantics, init)
) = motionSpec(baseMapping, defaultSpring, resetSpring, semantics, init)

/**
 * Creates a [MotionSpec] for an effects value.
@@ -49,9 +49,9 @@ fun MotionBuilderContext.effectsMotionSpec(
    baseMapping: Mapping = Mapping.Zero,
    defaultSpring: SpringParameters = this.effects.default,
    resetSpring: SpringParameters = defaultSpring,
    baseSemantics: List<SemanticValue<*>> = emptyList(),
    semantics: List<SemanticValue<*>> = emptyList(),
    init: MotionSpecBuilderScope.() -> Unit,
) = motionSpec(baseMapping, defaultSpring, resetSpring, baseSemantics, init)
) = motionSpec(baseMapping, defaultSpring, resetSpring, semantics, init)

/**
 * Creates a [MotionSpec], based on reusable effects.
@@ -61,21 +61,21 @@ fun MotionBuilderContext.effectsMotionSpec(
 *   unless otherwise specified.
 * @param resetSpring spring parameters to animate a difference in output, if the difference is
 *   caused by setting this new spec.
 * @param baseSemantics initial semantics that apply before of effects override them.
 * @param semantics initial semantics that apply before of effects override them.
 * @param init
 */
fun MotionBuilderContext.motionSpec(
    baseMapping: Mapping,
    defaultSpring: SpringParameters,
    resetSpring: SpringParameters = defaultSpring,
    baseSemantics: List<SemanticValue<*>> = emptyList(),
    semantics: List<SemanticValue<*>> = emptyList(),
    init: MotionSpecBuilderScope.() -> Unit,
): MotionSpec {
    return MotionSpecBuilderImpl(
            baseMapping,
            defaultSpring,
            resetSpring,
            baseSemantics,
            semantics,
            motionBuilderContext = this,
        )
        .apply(init)
@@ -121,8 +121,9 @@ fun MotionBuilderContext.fixedValueSpec(
    semantics: List<SemanticValue<*>> = emptyList(),
): MotionSpec {
    return MotionSpec(
        directionalMotionSpec(Mapping.Fixed(value), semantics),
        directionalMotionSpec(Mapping.Fixed(value)),
        resetSpring = resetSpring,
        semantics = semantics,
    )
}

+11 −7
Original line number Diff line number Diff line
@@ -56,13 +56,17 @@ internal class MotionSpecBuilderImpl(

    fun build(): MotionSpec {
        if (placedEffects.isEmpty()) {
            return MotionSpec(directionalMotionSpec(baseMapping), resetSpring = resetSpring)
            return MotionSpec(
                directionalMotionSpec(baseMapping),
                resetSpring = resetSpring,
                semantics = baseSemantics,
            )
        }

        builders =
            mutableObjectListOf(
                DirectionalEffectBuilderScopeImpl(defaultSpring, baseSemantics),
                DirectionalEffectBuilderScopeImpl(defaultSpring, baseSemantics),
                DirectionalEffectBuilderScopeImpl(defaultSpring),
                DirectionalEffectBuilderScopeImpl(defaultSpring),
            )
        segmentHandlers = mutableMapOf()

@@ -104,6 +108,7 @@ internal class MotionSpecBuilderImpl(
            builders[1].build(),
            resetSpring,
            segmentHandlers.toMap(),
            semantics = baseSemantics,
        )
    }

@@ -452,10 +457,9 @@ internal class MotionSpecBuilderImpl(
    }
}

private class DirectionalEffectBuilderScopeImpl(
    defaultSpring: SpringParameters,
    baseSemantics: List<SemanticValue<*>>,
) : DirectionalBuilderImpl(defaultSpring, baseSemantics), DirectionalEffectBuilderScope {
private class DirectionalEffectBuilderScopeImpl(defaultSpring: SpringParameters) :
    DirectionalBuilderImpl(defaultSpring, baseSemantics = emptyList()),
    DirectionalEffectBuilderScope {

    var beforeGuarantee: Guarantee? = null
    var beforeSpring: SpringParameters? = null
+30 −0
Original line number Diff line number Diff line
@@ -307,6 +307,36 @@ class MotionSpecTest {
        assertFailsWith<NoSuchElementException> { underTest.semantics(unknownSegment) }
    }

    @Test
    fun semantics_atSpecLevel_canBeAssociatedWithSpec() {
        val underTest = MotionSpec(DirectionalMotionSpec.Empty, semantics = listOf(S1 with "One"))

        assertThat(underTest.semanticState(S1)).isEqualTo("One")
    }

    @Test
    fun semantics_atSpecLevel_canBeQueriedViaSegment() {
        val underTest = MotionSpec(DirectionalMotionSpec.Empty, semantics = listOf(S1 with "One"))

        val maxDirectionSegment = SegmentKey(BMin, BMax, InputDirection.Max)
        assertThat(underTest.semanticState(S1, maxDirectionSegment)).isEqualTo("One")
    }

    @Test
    fun semantics_atSpecLevel_segmentLevelTakesPrecedence() {
        val underTest =
            MotionSpec(
                maxDirection = directionalMotionSpec(semantics = listOf(S1 with "Two")),
                minDirection = DirectionalMotionSpec.Empty,
                semantics = listOf(S1 with "One"),
            )

        assertThat(underTest.semanticState(S1, SegmentKey(BMin, BMax, InputDirection.Max)))
            .isEqualTo("Two")
        assertThat(underTest.semanticState(S1, SegmentKey(BMin, BMax, InputDirection.Min)))
            .isEqualTo("One")
    }

    companion object {
        val BMin = Breakpoint.minLimit.key
        val B1 = BreakpointKey("one")
Loading