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

Commit d9f1302b authored by Mike Schneider's avatar Mike Schneider
Browse files

Implement support for before/after semantics and mappings.

Bug: 401500734
Test: Unit tests
Flag: EXEMPT not yet used in production
Change-Id: I5413bca9ffacf6ed0997d714a6d5c4fe257dae3c
parent da6cc4d5
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -177,7 +177,7 @@ data class DirectionalMotionSpec(

        semantics.forEach {
            require(it.values.size == mappings.size) {
                "Semantics ${it.key} does not include correct number of segments"
                "Semantics ${it.key} contains ${it.values.size} values vs ${mappings.size} expected"
            }
        }
    }
+61 −32
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import com.android.mechanics.spec.DirectionalMotionSpec
import com.android.mechanics.spec.Guarantee
import com.android.mechanics.spec.Mapping
import com.android.mechanics.spec.SegmentSemanticValues
import com.android.mechanics.spec.SemanticKey
import com.android.mechanics.spec.SemanticValue
import com.android.mechanics.spring.SpringParameters

@@ -44,7 +45,7 @@ internal open class DirectionalBuilderImpl(
    private var breakpointKey: BreakpointKey? = null

    init {
        baseSemantics.forEach { semantics.add(SegmentSemanticValuesBuilder(it)) }
        baseSemantics.forEach { getSemantics(it.key).apply { set(0, it.value) } }
    }

    /** Prepares the builder for invoking the [DirectionalBuilderFn] on it. */
@@ -55,15 +56,19 @@ internal open class DirectionalBuilderImpl(
        check(mappings.size == breakpoints.size - 1)

        mappings.add(initialMapping)
        val semanticIndex = mappings.size - 1
        initialSemantics.forEach { semantic ->
            val existingBuilder = semantics.firstOrNull { it.key == semantic.key }
            if (existingBuilder != null) {
                existingBuilder.backfill(mappings.size - 1)
                existingBuilder.append(semantic.value)
            } else {
                SegmentSemanticValuesBuilder(semantic).also { semantics.add(it) }
            getSemantics(semantic.key).apply { set(semanticIndex, semantic.value) }
        }
    }

    internal fun <T> getSemantics(key: SemanticKey<T>): SegmentSemanticValuesBuilder<T> {
        @Suppress("UNCHECKED_CAST")
        var builder = semantics.firstOrNull { it.key == key } as SegmentSemanticValuesBuilder<T>?
        if (builder == null) {
            builder = SegmentSemanticValuesBuilder(key).also { semantics.add(it) }
        }
        return builder
    }

    /**
@@ -93,7 +98,7 @@ internal open class DirectionalBuilderImpl(
        } else {
            check(atPosition.isFinite())
            check(atPosition > breakpoints.last().position) {
                "Breakpoints were placed outside of partial sequence"
                "Breakpoint ${breakpoints.last()} placed after partial sequence (end=$atPosition)"
            }
            applySemantics(semantics)
        }
@@ -116,13 +121,9 @@ internal open class DirectionalBuilderImpl(
        require(mappings.size == breakpoints.size - 1)
        check(breakpoints.last() == Breakpoint.maxLimit)

        val semantics =
            semantics.map { builder ->
                with(builder) {
                    backfill(mappings.size)
                    build()
                }
            }
        val segmentCount = mappings.size

        val semantics = semantics.map { builder -> with(builder) { build(segmentCount) } }

        return DirectionalMotionSpec(breakpoints.toList(), mappings.toList(), semantics)
    }
@@ -235,13 +236,10 @@ internal open class DirectionalBuilderImpl(

    private fun applySemantics(toApply: List<SemanticValue<*>>) {
        toApply.forEach { (key, value) ->
            val semanticValuesBuilder =
                checkNotNull(semantics.first { it.key == key }) {
                    "semantic key $key not initially registered"
            getSemantics(key).apply {
                // applySemantics is called BEFORE adding the mapping
                set(mappings.size, value)
            }

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

@@ -345,21 +343,52 @@ internal open class DirectionalBuilderImpl(
    }
}

internal class SegmentSemanticValuesBuilder<T>(seed: SemanticValue<T>) {
    val key = seed.key
    private val values = mutableListOf(seed.value)
internal class SegmentSemanticValuesBuilder<T>(val key: SemanticKey<T>) {
    private val values = mutableListOf<SemanticValueHolder<T>>()
    private val unspecified = SemanticValueHolder.Unspecified<T>()

    fun backfill(segmentCount: Int) {
        val lastValue = values.last()
        repeat(segmentCount - values.size) { values.add(lastValue) }
    @Suppress("UNCHECKED_CAST")
    fun <V> set(segmentIndex: Int, value: V) {
        if (segmentIndex < values.size) {
            values[segmentIndex] = SemanticValueHolder.Specified(value as T)
        } else {
            backfill(segmentCount = segmentIndex)
            values.add(SemanticValueHolder.Specified(value as T))
        }
    }

    @Suppress("UNCHECKED_CAST")
    fun <V> append(value: V) {
        values.add(value as T)
    fun <V> updateBefore(segmentIndex: Int, value: V) {
        require(segmentIndex < values.size)

        val specified = SemanticValueHolder.Specified(value as T)

        for (i in segmentIndex downTo 0) {
            if (values[i] is SemanticValueHolder.Specified) break
            values[i] = specified
        }
    }

    fun build(segmentCount: Int): SegmentSemanticValues<T> {
        backfill(segmentCount)
        val firstValue = values.firstNotNullOf { it as? SemanticValueHolder.Specified }.value
        return SegmentSemanticValues(
            key,
            values.drop(1).runningFold(firstValue) { lastValue, thisHolder ->
                if (thisHolder is SemanticValueHolder.Specified) thisHolder.value else lastValue
            },
        )
    }

    private fun backfill(segmentCount: Int) {
        repeat(segmentCount - values.size) { values.add(unspecified) }
    }
}

internal sealed interface SemanticValueHolder<T> {
    class Specified<T>(val value: T) : SemanticValueHolder<T>

    fun build() = SegmentSemanticValues(key, values.toList())
    class Unspecified<T>() : SemanticValueHolder<T>
}

private data object CanBeLastSegmentImpl : CanBeLastSegment
+8 −2
Original line number Diff line number Diff line
@@ -64,7 +64,6 @@ value class EffectPlacement internal constructor(val value: Long) {

    val isForward: Boolean
        get() {

            return when (type) {
                EffectPlacemenType.At -> true
                EffectPlacemenType.Before -> false
@@ -74,7 +73,14 @@ value class EffectPlacement internal constructor(val value: Long) {
        }

    internal val sortOrder: Float
        get() = if (isForward) start.nextUp() else start.nextDown()
        get() {
            return when (type) {
                EffectPlacemenType.At -> start
                EffectPlacemenType.Before -> start.nextDown()
                EffectPlacemenType.After -> start.nextUp()
                EffectPlacemenType.Between -> (start + end) / 2
            }
        }

    internal val min: Float
        get() = min(start, end)
+17 −7
Original line number Diff line number Diff line
@@ -226,11 +226,14 @@ internal class MotionSpecBuilderImpl(
        }

        check(absoluteEffectPlacements.isNotEmpty())
        // Implementation note: sortedAbsolutePlacedEffects should be an IntArray, but that cannot
        // be sorted with a custom comparator, hence using a typed array.
        val sortedAbsolutePlacedEffects =
            IntArray(absoluteEffectPlacements.size).also { array ->
            Array(absoluteEffectPlacements.size) { 0 }
                .also { array ->
                    var index = 0
                    absoluteEffectPlacements.forEachKey { array[index++] = it }
                array.sortedBy { EffectPlacement(absoluteEffectPlacements[it]).sortOrder }
                    array.sortBy { EffectPlacement(absoluteEffectPlacements[it]).sortOrder }
                }

        sortedAbsolutePlacedEffects.forEach { effectId ->
@@ -383,7 +386,7 @@ internal class MotionSpecBuilderImpl(
            val maxBreakpoint =
                Breakpoint.create(maxLimitKey, actualPlacement.max, defaultSpring, Guarantee.None)
            builders.forEach { builder ->
                builder.mappings += baseMapping
                builder.mappings += builder.afterMapping ?: baseMapping
                builder.breakpoints += maxBreakpoint
            }
            return
@@ -433,7 +436,14 @@ internal class MotionSpecBuilderImpl(
                        guarantee = builder.beforeGuarantee ?: oldMinBreakpoint.guarantee,
                    )
            }
            // FIXME mappings & semantics

            builder.beforeMapping
                ?.takeIf { initialSize >= 2 && builder.mappings[initialSize - 2] === baseMapping }
                ?.also { builder.mappings[initialSize - 2] = it }

            builder.beforeSemantics?.forEach {
                builder.getSemantics(it.key).updateBefore(initialSize - 2, it.value)
            }
        }
    }

+5 −4
Original line number Diff line number Diff line
@@ -25,7 +25,6 @@ import com.android.mechanics.spec.with
import com.android.mechanics.spring.SpringParameters
import com.android.mechanics.testing.DirectionalMotionSpecSubject.Companion.assertThat
import com.android.mechanics.testing.FakeMotionSpecBuilderContext
import kotlin.test.assertFailsWith
import org.junit.Test
import org.junit.runner.RunWith

@@ -221,8 +220,8 @@ class DirectionalBuilderImplTest {
    }

    @Test
    fun directionalSpec_semantics_changingUndeclaredSemantics_throws() {
        assertFailsWith<NoSuchElementException> {
    fun directionalSpec_semantics_changingUndeclaredSemantics_backfills() {
        val result =
            directionalMotionSpec(Spring) {
                mapping(
                    breakpoint = 0f,
@@ -230,7 +229,9 @@ class DirectionalBuilderImplTest {
                    semantics = listOf(S1 with "Two"),
                )
            }
        }

        assertThat(result).mappings().hasSize(2)
        assertThat(result).semantics().withKey(S1).containsExactly("Two", "Two").inOrder()
    }

    @Test
Loading