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

Commit 65d1a4a6 authored by Mike Schneider's avatar Mike Schneider
Browse files

Semantic states for motion values

Test: Unit tests
Flag: com.android.systemui.scene_container
Bug: 402119418
Change-Id: I3e37caf4d30cee8073256dd24aeb6a3e62d2b93a
parent e8d088d9
Loading
Loading
Loading
Loading
+10 −0
Original line number Diff line number Diff line
@@ -37,6 +37,7 @@ import com.android.mechanics.spec.InputDirection
import com.android.mechanics.spec.Mapping
import com.android.mechanics.spec.MotionSpec
import com.android.mechanics.spec.SegmentData
import com.android.mechanics.spec.SemanticKey
import com.android.mechanics.spring.SpringState
import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.CoroutineName
@@ -140,6 +141,15 @@ class MotionValue(
    /** Whether an animation is currently running. */
    val isStable: Boolean by impl::isStable

    /**
     * The current value for the [SemanticKey].
     *
     * `null` if not defined in the spec.
     */
    operator fun <T> get(key: SemanticKey<T>): T? {
        return impl.semanticState(key)
    }

    /**
     * Keeps the [MotionValue]'s animated output running.
     *
+9 −0
Original line number Diff line number Diff line
@@ -24,6 +24,8 @@ import com.android.mechanics.impl.DiscontinuityAnimation
import com.android.mechanics.spec.InputDirection
import com.android.mechanics.spec.SegmentData
import com.android.mechanics.spec.SegmentKey
import com.android.mechanics.spec.SemanticKey
import com.android.mechanics.spec.SemanticValue
import com.android.mechanics.spring.SpringParameters
import com.android.mechanics.spring.SpringState
import kotlinx.coroutines.DisposableHandle
@@ -79,6 +81,13 @@ internal constructor(
    val outputTarget: Float
        get() = currentDirectMapped + animation.targetValue

    fun <T> semantic(semanticKey: SemanticKey<T>): T? {
        return segment.semantic(semanticKey)
    }

    val semantics: List<SemanticValue<*>>
        get() = with(segment) { spec.semantics(key) }

    private val currentDirectMapped: Float
        get() = segment.mapping.map(input) - animation.targetValue
}
+6 −0
Original line number Diff line number Diff line
@@ -23,7 +23,9 @@ import androidx.compose.ui.util.fastIsFinite
import androidx.compose.ui.util.lerp
import com.android.mechanics.MotionValue.Companion.TAG
import com.android.mechanics.spec.Guarantee
import com.android.mechanics.spec.MotionSpec
import com.android.mechanics.spec.SegmentData
import com.android.mechanics.spec.SemanticKey
import com.android.mechanics.spring.SpringState
import com.android.mechanics.spring.calculateUpdatedState

@@ -435,4 +437,8 @@ internal interface Computations : ComputeSpringState {

    val isStable: Boolean
        get() = currentSpringState == SpringState.AtRest

    fun <T> semanticState(semanticKey: SemanticKey<T>): T? {
        return with(currentSegment) { spec.semanticState(semanticKey, key) }
    }
}
+92 −5
Original line number Diff line number Diff line
@@ -50,15 +50,19 @@ import com.android.mechanics.spring.SpringParameters
 *   [Mapping.Identity]).
 * @param init A lambda function that configures the [DirectionalMotionSpecBuilder]. The lambda
 *   should return a [CanBeLastSegment] to indicate the end of the spec.
 * @param semantics Semantics specified in this spec, including the initial value applied for
 *   [initialMapping].
 *     @return The constructed [DirectionalMotionSpec].
 */
fun buildDirectionalMotionSpec(
    defaultSpring: SpringParameters,
    initialMapping: Mapping = Mapping.Identity,
    semantics: List<SemanticValue<*>> = emptyList(),
    init: DirectionalMotionSpecBuilder.() -> CanBeLastSegment,
): DirectionalMotionSpec {
    return DirectionalMotionSpecBuilderImpl(defaultSpring)
        .also { it.mappings += initialMapping }
        .also { it.semantics += semantics.map { SegmentSemanticValuesBuilder(it) } }
        .also { it.init() }
        .build()
}
@@ -67,10 +71,21 @@ fun buildDirectionalMotionSpec(
 * Builds a simple [DirectionalMotionSpec] with a single segment.
 *
 * @param mapping The [Mapping] to apply to the segment. Defaults to [Mapping.Identity].
 * @param semantics Semantics values for this spec.
 * @return A new [DirectionalMotionSpec] instance configured with the provided parameters.
 */
fun buildDirectionalMotionSpec(mapping: Mapping = Mapping.Identity): DirectionalMotionSpec {
    return DirectionalMotionSpec(listOf(Breakpoint.minLimit, Breakpoint.maxLimit), listOf(mapping))
fun buildDirectionalMotionSpec(
    mapping: Mapping = Mapping.Identity,
    semantics: List<SemanticValue<*>> = emptyList(),
): DirectionalMotionSpec {
    fun <T> toSegmentSemanticValues(semanticValue: SemanticValue<T>) =
        SegmentSemanticValues(semanticValue.key, listOf(semanticValue.value))

    return DirectionalMotionSpec(
        listOf(Breakpoint.minLimit, Breakpoint.maxLimit),
        listOf(mapping),
        semantics.map { toSegmentSemanticValues(it) },
    )
}

/**
@@ -98,6 +113,8 @@ interface DirectionalMotionSpecBuilder {
     *   [defaultSpring].
     * @param guarantee The animation guarantee for this transition. Defaults to [Guarantee.None].
     * @param key A unique [BreakpointKey] for this breakpoint. Defaults to a newly generated key.
     * @param semantics Updated semantics values to be applied. Must be a subset of the
     *   [SemanticKey]s used when first creating this builder.
     */
    fun target(
        breakpoint: Float,
@@ -106,6 +123,7 @@ interface DirectionalMotionSpecBuilder {
        spring: SpringParameters = defaultSpring,
        guarantee: Guarantee = Guarantee.None,
        key: BreakpointKey = BreakpointKey(),
        semantics: List<SemanticValue<*>> = emptyList(),
    )

    /**
@@ -124,6 +142,8 @@ interface DirectionalMotionSpecBuilder {
     *   [defaultSpring].
     * @param guarantee The animation guarantee for this transition. Defaults to [Guarantee.None].
     * @param key A unique [BreakpointKey] for this breakpoint. Defaults to a newly generated key.
     * @param semantics Updated semantics values to be applied. Must be a subset of the
     *   [SemanticKey]s used when first creating this builder.
     */
    fun targetFromCurrent(
        breakpoint: Float,
@@ -132,6 +152,7 @@ interface DirectionalMotionSpecBuilder {
        spring: SpringParameters = defaultSpring,
        guarantee: Guarantee = Guarantee.None,
        key: BreakpointKey = BreakpointKey(),
        semantics: List<SemanticValue<*>> = emptyList(),
    )

    /**
@@ -151,6 +172,8 @@ interface DirectionalMotionSpecBuilder {
     *   [defaultSpring].
     * @param guarantee The animation guarantee for this transition. Defaults to [Guarantee.None].
     * @param key A unique [BreakpointKey] for this breakpoint. Defaults to a newly generated key.
     * @param semantics Updated semantics values to be applied. Must be a subset of the
     *   [SemanticKey]s used when first creating this builder.
     */
    fun fractionalInput(
        breakpoint: Float,
@@ -159,6 +182,7 @@ interface DirectionalMotionSpecBuilder {
        spring: SpringParameters = defaultSpring,
        guarantee: Guarantee = Guarantee.None,
        key: BreakpointKey = BreakpointKey(),
        semantics: List<SemanticValue<*>> = emptyList(),
    ): CanBeLastSegment

    /**
@@ -177,6 +201,8 @@ interface DirectionalMotionSpecBuilder {
     *   [defaultSpring].
     * @param guarantee The animation guarantee for this transition. Defaults to [Guarantee.None].
     * @param key A unique [BreakpointKey] for this breakpoint. Defaults to a newly generated key.
     * @param semantics Updated semantics values to be applied. Must be a subset of the
     *   [SemanticKey]s used when first creating this builder.
     */
    fun fractionalInputFromCurrent(
        breakpoint: Float,
@@ -185,6 +211,7 @@ interface DirectionalMotionSpecBuilder {
        spring: SpringParameters = defaultSpring,
        guarantee: Guarantee = Guarantee.None,
        key: BreakpointKey = BreakpointKey(),
        semantics: List<SemanticValue<*>> = emptyList(),
    ): CanBeLastSegment

    /**
@@ -200,6 +227,8 @@ interface DirectionalMotionSpecBuilder {
     *   [defaultSpring].
     * @param guarantee The animation guarantee for this transition. Defaults to [Guarantee.None].
     * @param key A unique [BreakpointKey] for this breakpoint. Defaults to a newly generated key.
     * @param semantics Updated semantics values to be applied. Must be a subset of the
     *   [SemanticKey]s used when first creating this builder.
     */
    fun constantValue(
        breakpoint: Float,
@@ -207,6 +236,7 @@ interface DirectionalMotionSpecBuilder {
        spring: SpringParameters = defaultSpring,
        guarantee: Guarantee = Guarantee.None,
        key: BreakpointKey = BreakpointKey(),
        semantics: List<SemanticValue<*>> = emptyList(),
    ): CanBeLastSegment

    /**
@@ -224,6 +254,8 @@ interface DirectionalMotionSpecBuilder {
     *   [defaultSpring].
     * @param guarantee The animation guarantee for this transition. Defaults to [Guarantee.None].
     * @param key A unique [BreakpointKey] for this breakpoint. Defaults to a newly generated key.
     * @param semantics Updated semantics values to be applied. Must be a subset of the
     *   [SemanticKey]s used when first creating this builder.
     */
    fun constantValueFromCurrent(
        breakpoint: Float,
@@ -231,6 +263,7 @@ interface DirectionalMotionSpecBuilder {
        spring: SpringParameters = defaultSpring,
        guarantee: Guarantee = Guarantee.None,
        key: BreakpointKey = BreakpointKey(),
        semantics: List<SemanticValue<*>> = emptyList(),
    ): CanBeLastSegment

    /**
@@ -245,6 +278,8 @@ interface DirectionalMotionSpecBuilder {
     *   [defaultSpring].
     * @param guarantee The animation guarantee for this transition. Defaults to [Guarantee.None].
     * @param key A unique [BreakpointKey] for this breakpoint. Defaults to a newly generated key.
     * @param semantics Updated semantics values to be applied. Must be a subset of the
     *   [SemanticKey]s used when first creating this builder.
     * @param mapping The custom [Mapping] to use.
     */
    fun mapping(
@@ -252,6 +287,7 @@ interface DirectionalMotionSpecBuilder {
        spring: SpringParameters = defaultSpring,
        guarantee: Guarantee = Guarantee.None,
        key: BreakpointKey = BreakpointKey(),
        semantics: List<SemanticValue<*>> = emptyList(),
        mapping: Mapping,
    ): CanBeLastSegment
}
@@ -261,17 +297,46 @@ sealed interface CanBeLastSegment

private data object CanBeLastSegmentImpl : CanBeLastSegment

private class SegmentSemanticValuesBuilder<T>(seed: SemanticValue<T>) {
    val key = seed.key
    private val values = mutableListOf(seed.value)

    fun backfill(segmentCount: Int) {
        val lastValue = values.last()
        repeat(segmentCount - values.size) { values.add(lastValue) }
    }

    @Suppress("UNCHECKED_CAST")
    fun <V> append(value: V) {
        values.add(value as T)
    }

    fun build() = SegmentSemanticValues(key, values.toList())
}

private class DirectionalMotionSpecBuilderImpl(override val defaultSpring: SpringParameters) :
    DirectionalMotionSpecBuilder {
    private val breakpoints = mutableListOf(Breakpoint.minLimit)
    val semantics = mutableListOf<SegmentSemanticValuesBuilder<*>>()
    val mappings = mutableListOf<Mapping>()

    private var sourceValue: Float = Float.NaN
    private var targetValue: Float = Float.NaN
    private var fractionalMapping: Float = Float.NaN
    private var breakpointPosition: Float = Float.NaN
    private var breakpointKey: BreakpointKey? = null

    private fun applySemantics(toApply: List<SemanticValue<*>>) {
        toApply.forEach { (key, value) ->
            val semanticValuesBuilder =
                checkNotNull(semantics.first { it.key == key }) {
                    "semantic key $key not initially registered"
                }

            semanticValuesBuilder.backfill(mappings.size)
            semanticValuesBuilder.append(value)
        }
    }

    override fun target(
        breakpoint: Float,
        from: Float,
@@ -279,7 +344,9 @@ private class DirectionalMotionSpecBuilderImpl(override val defaultSpring: Sprin
        spring: SpringParameters,
        guarantee: Guarantee,
        key: BreakpointKey,
        semantics: List<SemanticValue<*>>,
    ) {
        applySemantics(semantics)
        toBreakpointImpl(breakpoint, key)
        jumpToImpl(from, spring, guarantee)
        continueWithTargetValueImpl(to)
@@ -292,7 +359,9 @@ private class DirectionalMotionSpecBuilderImpl(override val defaultSpring: Sprin
        spring: SpringParameters,
        guarantee: Guarantee,
        key: BreakpointKey,
        semantics: List<SemanticValue<*>>,
    ) {
        applySemantics(semantics)
        toBreakpointImpl(breakpoint, key)
        jumpByImpl(delta, spring, guarantee)
        continueWithTargetValueImpl(to)
@@ -305,7 +374,9 @@ private class DirectionalMotionSpecBuilderImpl(override val defaultSpring: Sprin
        spring: SpringParameters,
        guarantee: Guarantee,
        key: BreakpointKey,
        semantics: List<SemanticValue<*>>,
    ): CanBeLastSegment {
        applySemantics(semantics)
        toBreakpointImpl(breakpoint, key)
        jumpToImpl(from, spring, guarantee)
        continueWithFractionalInputImpl(fraction)
@@ -319,7 +390,9 @@ private class DirectionalMotionSpecBuilderImpl(override val defaultSpring: Sprin
        spring: SpringParameters,
        guarantee: Guarantee,
        key: BreakpointKey,
        semantics: List<SemanticValue<*>>,
    ): CanBeLastSegment {
        applySemantics(semantics)
        toBreakpointImpl(breakpoint, key)
        jumpByImpl(delta, spring, guarantee)
        continueWithFractionalInputImpl(fraction)
@@ -332,7 +405,9 @@ private class DirectionalMotionSpecBuilderImpl(override val defaultSpring: Sprin
        spring: SpringParameters,
        guarantee: Guarantee,
        key: BreakpointKey,
        semantics: List<SemanticValue<*>>,
    ): CanBeLastSegment {
        applySemantics(semantics)
        toBreakpointImpl(breakpoint, key)
        jumpToImpl(value, spring, guarantee)
        continueWithConstantValueImpl()
@@ -345,7 +420,9 @@ private class DirectionalMotionSpecBuilderImpl(override val defaultSpring: Sprin
        spring: SpringParameters,
        guarantee: Guarantee,
        key: BreakpointKey,
        semantics: List<SemanticValue<*>>,
    ): CanBeLastSegment {
        applySemantics(semantics)
        toBreakpointImpl(breakpoint, key)
        jumpByImpl(delta, spring, guarantee)
        continueWithConstantValueImpl()
@@ -357,8 +434,10 @@ private class DirectionalMotionSpecBuilderImpl(override val defaultSpring: Sprin
        spring: SpringParameters,
        guarantee: Guarantee,
        key: BreakpointKey,
        semantics: List<SemanticValue<*>>,
        mapping: Mapping,
    ): CanBeLastSegment {
        applySemantics(semantics)
        toBreakpointImpl(breakpoint, key)
        continueWithImpl(mapping, spring, guarantee)
        return CanBeLastSegmentImpl
@@ -366,7 +445,15 @@ private class DirectionalMotionSpecBuilderImpl(override val defaultSpring: Sprin

    fun build(): DirectionalMotionSpec {
        completeImpl()
        return DirectionalMotionSpec(breakpoints.toList(), mappings.toList())
        val semantics =
            semantics.map { builder ->
                with(builder) {
                    backfill(mappings.size)
                    build()
                }
            }

        return DirectionalMotionSpec(breakpoints.toList(), mappings.toList(), semantics)
    }

    private fun continueWithTargetValueImpl(target: Float) {
+54 −4
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.mechanics.spec

import androidx.compose.ui.util.fastFirstOrNull
import com.android.mechanics.spring.SpringParameters

/**
@@ -70,6 +71,37 @@ data class MotionSpec(
        return get(segmentKey.direction).findSegmentIndex(segmentKey) != -1
    }

    /**
     * The semantic state for [key] at segment with [segmentKey].
     *
     * Returns `null` if no semantic value with [key] is defined. Throws [NoSuchElementException] if
     * [segmentKey] does not exist in this [MotionSpec].
     */
    fun <T> semanticState(key: SemanticKey<T>, segmentKey: SegmentKey): T? {
        with(get(segmentKey.direction)) {
            val semanticValues = semantics.fastFirstOrNull { it.key == key } ?: return null
            val segmentIndex = findSegmentIndex(segmentKey)
            if (segmentIndex < 0) throw NoSuchElementException()

            @Suppress("UNCHECKED_CAST")
            return semanticValues.values[segmentIndex] as T
        }
    }

    /**
     * All [SemanticValue]s associated with the segment identified with [segmentKey].
     *
     * Throws [NoSuchElementException] if [segmentKey] does not exist in this [MotionSpec].
     */
    fun semantics(segmentKey: SegmentKey): List<SemanticValue<*>> {
        with(get(segmentKey.direction)) {
            val segmentIndex = findSegmentIndex(segmentKey)
            if (segmentIndex < 0) throw NoSuchElementException()

            return semantics.map { it[segmentIndex] }
        }
    }

    /**
     * The [SegmentData] for an input with the specified [position] and [direction].
     *
@@ -139,8 +171,17 @@ 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].
 */
data class DirectionalMotionSpec(val breakpoints: List<Breakpoint>, val mappings: List<Mapping>) {
data class DirectionalMotionSpec(
    val breakpoints: List<Breakpoint>,
    val mappings: List<Mapping>,
    val semantics: List<SegmentSemanticValues<*>> = emptyList(),
) {
    /** Maps all [BreakpointKey]s used in this spec to its index in [breakpoints]. */
    private val breakpointIndexByKey: Map<BreakpointKey, Int>

    init {
        require(breakpoints.size >= 2)
        require(breakpoints.first() == Breakpoint.minLimit)
@@ -149,6 +190,15 @@ data class DirectionalMotionSpec(val breakpoints: List<Breakpoint>, val mappings
            "Breakpoints are not sorted ascending ${breakpoints.map { "${it.key}@${it.position}" }}"
        }
        require(mappings.size == breakpoints.size - 1)

        breakpointIndexByKey =
            breakpoints.mapIndexed { index, breakpoint -> breakpoint.key to index }.toMap()

        semantics.forEach {
            require(it.values.size == mappings.size) {
                "Semantics ${it.key} does not include correct number of segments"
            }
        }
    }

    /**
@@ -182,13 +232,13 @@ data class DirectionalMotionSpec(val breakpoints: List<Breakpoint>, val mappings
     * exists.
     */
    fun findBreakpointIndex(breakpointKey: BreakpointKey): Int {
        return breakpoints.indexOfFirst { it.key == breakpointKey }
        return breakpointIndexByKey[breakpointKey] ?: -1
    }

    /** Index into [mappings] for the specified [segmentKey], or `-1` if no such segment exists. */
    fun findSegmentIndex(segmentKey: SegmentKey): Int {
        val result = breakpoints.indexOfFirst { it.key == segmentKey.minBreakpoint }
        if (result < 0 || breakpoints[result + 1].key != segmentKey.maxBreakpoint) return -1
        val result = breakpointIndexByKey[segmentKey.minBreakpoint] ?: return -1
        if (breakpoints[result + 1].key != segmentKey.maxBreakpoint) return -1

        return result
    }
Loading