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

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

Merge changes Ic01bdfb0,Ie03fa9fe into main

* changes:
  Capitalize all `val`s in `companion object`s
  Add query-logic to the [MotionSpec] #MotionMechanics
parents 4e804018 fd9fc8e7
Loading
Loading
Loading
Loading
+3 −3
Original line number Diff line number Diff line
@@ -25,7 +25,7 @@ package com.android.mechanics.spec
 * The directions are labelled [Min] and [Max] to reflect descending and ascending input values
 * respectively, but it does not imply an spatial direction.
 */
enum class InputDirection {
    Min,
    Max,
enum class InputDirection(val sign: Int) {
    Min(sign = -1),
    Max(sign = +1),
}
+76 −2
Original line number Diff line number Diff line
@@ -56,6 +56,63 @@ data class MotionSpec(
    val resetSpring: SpringParameters = DefaultResetSpring,
    val segmentHandlers: Map<SegmentKey, OnChangeSegmentHandler> = emptyMap(),
) {

    /** The [DirectionalMotionSpec] for the specified [direction]. */
    operator fun get(direction: InputDirection): DirectionalMotionSpec {
        return when (direction) {
            InputDirection.Min -> minDirection
            InputDirection.Max -> maxDirection
        }
    }

    /** Whether this spec contains a segment with the specified [segmentKey]. */
    fun containsSegment(segmentKey: SegmentKey): Boolean {
        return get(segmentKey.direction).findSegmentIndex(segmentKey) != -1
    }

    /**
     * The [SegmentData] for an input with the specified [position] and [direction].
     *
     * The returned [SegmentData] will be cached while [SegmentData.isValidForInput] returns `true`.
     */
    fun segmentAtInput(position: Float, direction: InputDirection): SegmentData {
        require(position.isFinite())

        return with(get(direction)) {
            var idx = findBreakpointIndex(position)
            if (direction == InputDirection.Min && breakpoints[idx].position == position) {
                // The segment starts at `position`. Since the breakpoints are sorted ascending, no
                // matter the spec's direction, need to return the previous segment in the min
                // direction.
                idx--
            }

            SegmentData(
                this@MotionSpec,
                breakpoints[idx],
                breakpoints[idx + 1],
                direction,
                mappings[idx],
            )
        }
    }

    /**
     * Looks up the new [SegmentData] once the [currentSegment] is not valid for an input with
     * [newPosition] and [newDirection].
     *
     * This will delegate to the [segmentHandlers], if registered for the [currentSegment]'s key.
     */
    internal fun onChangeSegment(
        currentSegment: SegmentData,
        newPosition: Float,
        newDirection: InputDirection,
    ): SegmentData {
        val segmentChangeHandler = segmentHandlers[currentSegment.key]
        return segmentChangeHandler?.invoke(this, currentSegment, newPosition, newDirection)
            ?: segmentAtInput(newPosition, newDirection)
    }

    companion object {
        /**
         * Default spring parameters for the reset spring. Matches the Fast Spatial spring of the
@@ -97,10 +154,11 @@ data class DirectionalMotionSpec(val breakpoints: List<Breakpoint>, val mappings
    /**
     * Returns the index of the closest breakpoint where `Breakpoint.position <= position`.
     *
     * Guaranteed to be a valid index into [breakpoints], and guaranteed not to be the last element.
     * Guaranteed to be a valid index into [breakpoints], and guaranteed to be neither the first nor
     * the last element.
     *
     * @param position the position in the input domain.
     * @return Index into [breakpoints], guaranteed to be in range `0..breakpoints.size - 2`
     * @return Index into [breakpoints], guaranteed to be in range `1..breakpoints.size - 2`
     */
    fun findBreakpointIndex(position: Float): Int {
        require(position.isFinite())
@@ -119,6 +177,22 @@ data class DirectionalMotionSpec(val breakpoints: List<Breakpoint>, val mappings
        return result
    }

    /**
     * The index of the breakpoint with the specified [breakpointKey], or `-1` if no such breakpoint
     * exists.
     */
    fun findBreakpointIndex(breakpointKey: BreakpointKey): Int {
        return breakpoints.indexOfFirst { it.key == breakpointKey }
    }

    /** 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

        return result
    }

    companion object {
        /* Empty spec, the full input domain is mapped to output using [Mapping.identity]. */
        val Empty =
+13 −0
Original line number Diff line number Diff line
@@ -40,6 +40,7 @@ data class SegmentKey(
 * respective breakpoint.
 */
data class SegmentData(
    val spec: MotionSpec,
    val minBreakpoint: Breakpoint,
    val maxBreakpoint: Breakpoint,
    val direction: InputDirection,
@@ -62,6 +63,18 @@ data class SegmentData(
            InputDirection.Min -> inputPosition > minBreakpoint.position
        }
    }

    /**
     * The breakpoint at the side of the segment's start.
     *
     * The [entryBreakpoint]'s [Guarantee] is the relevant guarantee for this segment.
     */
    val entryBreakpoint: Breakpoint
        get() =
            when (direction) {
                InputDirection.Max -> minBreakpoint
                InputDirection.Min -> maxBreakpoint
            }
}

/**
+177 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.mechanics.spec

import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.mechanics.spring.SpringParameters
import com.google.common.truth.Truth.assertThat
import kotlin.math.nextDown
import kotlin.math.nextUp
import kotlin.test.assertFailsWith
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class DirectionalMotionSpecTest {

    @Test
    fun noBreakpoints_throws() {
        assertFailsWith<IllegalArgumentException> {
            DirectionalMotionSpec(emptyList(), emptyList())
        }
    }

    @Test
    fun wrongSentinelBreakpoints_throws() {
        val breakpoint1 = Breakpoint(B1, position = 10f, Spring, Guarantee.None)
        val breakpoint2 = Breakpoint(B2, position = 20f, Spring, Guarantee.None)

        assertFailsWith<IllegalArgumentException> {
            DirectionalMotionSpec(listOf(breakpoint1, breakpoint2), listOf(Mapping.Identity))
        }
    }

    @Test
    fun tooFewMappings_throws() {
        assertFailsWith<IllegalArgumentException> {
            DirectionalMotionSpec(listOf(Breakpoint.minLimit, Breakpoint.maxLimit), emptyList())
        }
    }

    @Test
    fun tooManyMappings_throws() {
        assertFailsWith<IllegalArgumentException> {
            DirectionalMotionSpec(
                listOf(Breakpoint.minLimit, Breakpoint.maxLimit),
                listOf(Mapping.One, Mapping.Two),
            )
        }
    }

    @Test
    fun breakpointsOutOfOrder_throws() {
        val breakpoint1 = Breakpoint(B1, position = 10f, Spring, Guarantee.None)
        val breakpoint2 = Breakpoint(B2, position = 20f, Spring, Guarantee.None)
        assertFailsWith<IllegalArgumentException> {
            DirectionalMotionSpec(
                listOf(Breakpoint.minLimit, breakpoint2, breakpoint1, Breakpoint.maxLimit),
                listOf(Mapping.Zero, Mapping.One, Mapping.Two),
            )
        }
    }

    @Test
    fun findBreakpointIndex_returnsMinForEmptySpec() {
        val underTest = DirectionalMotionSpec.builder(Spring).complete()

        assertThat(underTest.findBreakpointIndex(0f)).isEqualTo(0)
        assertThat(underTest.findBreakpointIndex(Float.MAX_VALUE)).isEqualTo(0)
        assertThat(underTest.findBreakpointIndex(-Float.MAX_VALUE)).isEqualTo(0)
    }

    @Test
    fun findBreakpointIndex_throwsForNonFiniteInput() {
        val underTest = DirectionalMotionSpec.builder(Spring).complete()

        assertFailsWith<IllegalArgumentException> { underTest.findBreakpointIndex(Float.NaN) }
        assertFailsWith<IllegalArgumentException> {
            underTest.findBreakpointIndex(Float.NEGATIVE_INFINITY)
        }
        assertFailsWith<IllegalArgumentException> {
            underTest.findBreakpointIndex(Float.POSITIVE_INFINITY)
        }
    }

    @Test
    fun findBreakpointIndex_atBreakpoint_returnsIndex() {
        val underTest =
            DirectionalMotionSpec.builder(Spring).toBreakpoint(10f).completeWith(Mapping.Identity)

        assertThat(underTest.findBreakpointIndex(10f)).isEqualTo(1)
    }

    @Test
    fun findBreakpointIndex_afterBreakpoint_returnsPreviousIndex() {
        val underTest =
            DirectionalMotionSpec.builder(Spring).toBreakpoint(10f).completeWith(Mapping.Identity)

        assertThat(underTest.findBreakpointIndex(10f.nextUp())).isEqualTo(1)
    }

    @Test
    fun findBreakpointIndex_beforeBreakpoint_returnsIndex() {
        val underTest =
            DirectionalMotionSpec.builder(Spring).toBreakpoint(10f).completeWith(Mapping.Identity)

        assertThat(underTest.findBreakpointIndex(10f.nextDown())).isEqualTo(0)
    }

    @Test
    fun findBreakpointIndexByKey_returnsIndex() {
        val underTest =
            DirectionalMotionSpec.builder(Spring)
                .toBreakpoint(10f, key = B1)
                .completeWith(Mapping.Identity)

        assertThat(underTest.findBreakpointIndex(B1)).isEqualTo(1)
    }

    @Test
    fun findBreakpointIndexByKey_unknown_returnsMinusOne() {
        val underTest =
            DirectionalMotionSpec.builder(Spring)
                .toBreakpoint(10f, key = B1)
                .completeWith(Mapping.Identity)

        assertThat(underTest.findBreakpointIndex(B2)).isEqualTo(-1)
    }

    @Test
    fun findSegmentIndex_returnsIndexForSegment_ignoringDirection() {
        val underTest =
            DirectionalMotionSpec.builder(Spring)
                .toBreakpoint(10f, key = B1)
                .continueWith(Mapping.One)
                .toBreakpoint(20f, key = B2)
                .completeWith(Mapping.Identity)

        assertThat(underTest.findSegmentIndex(SegmentKey(B1, B2, InputDirection.Max))).isEqualTo(1)
        assertThat(underTest.findSegmentIndex(SegmentKey(B1, B2, InputDirection.Min))).isEqualTo(1)
    }

    @Test
    fun findSegmentIndex_forInvalidKeys_returnsMinusOne() {
        val underTest =
            DirectionalMotionSpec.builder(Spring)
                .toBreakpoint(10f, key = B1)
                .continueWith(Mapping.One)
                .toBreakpoint(20f, key = B2)
                .continueWith(Mapping.One)
                .toBreakpoint(30f, key = B3)
                .completeWith(Mapping.Identity)

        assertThat(underTest.findSegmentIndex(SegmentKey(B2, B1, InputDirection.Max))).isEqualTo(-1)
        assertThat(underTest.findSegmentIndex(SegmentKey(B1, B3, InputDirection.Max))).isEqualTo(-1)
    }

    companion object {
        val B1 = BreakpointKey("one")
        val B2 = BreakpointKey("two")
        val B3 = BreakpointKey("three")
        val Spring = SpringParameters(stiffness = 100f, dampingRatio = 1f)
    }
}
+33 −34
Original line number Diff line number Diff line
@@ -27,7 +27,7 @@ class FluentSpecBuilderTest {

    @Test
    fun directionalSpec_buildEmptySpec() {
        val result = DirectionalMotionSpec.builder(spring).complete()
        val result = DirectionalMotionSpec.builder(Spring).complete()

        assertThat(result).breakpoints().isEmpty()
        assertThat(result).mappings().containsExactly(Mapping.Identity)
@@ -35,7 +35,7 @@ class FluentSpecBuilderTest {

    @Test
    fun directionalSpec_buildEmptySpec_inReverse() {
        val result = DirectionalMotionSpec.reverseBuilder(spring).complete()
        val result = DirectionalMotionSpec.reverseBuilder(Spring).complete()

        assertThat(result).breakpoints().isEmpty()
        assertThat(result).mappings().containsExactly(Mapping.Identity)
@@ -44,15 +44,15 @@ class FluentSpecBuilderTest {
    @Test
    fun motionSpec_sameSpecInBothDirections() {
        val result =
            MotionSpec.builder(spring, Mapping.Zero)
                .toBreakpoint(0f, b1)
            MotionSpec.builder(Spring, Mapping.Zero)
                .toBreakpoint(0f, B1)
                .continueWith(Mapping.One)
                .toBreakpoint(10f, b2)
                .toBreakpoint(10f, B2)
                .completeWith(Mapping.Two)

        assertThat(result.maxDirection).isSameInstanceAs(result.minDirection)

        assertThat(result.minDirection).breakpoints().keys().containsExactly(b1, b2).inOrder()
        assertThat(result.minDirection).breakpoints().keys().containsExactly(B1, B2).inOrder()
        assertThat(result.minDirection)
            .mappings()
            .containsExactly(Mapping.Zero, Mapping.One, Mapping.Two)
@@ -62,15 +62,15 @@ class FluentSpecBuilderTest {
    @Test
    fun directionalSpec_addBreakpointsAndMappings() {
        val result =
            DirectionalMotionSpec.builder(spring, Mapping.Zero)
                .toBreakpoint(0f, b1)
            DirectionalMotionSpec.builder(Spring, Mapping.Zero)
                .toBreakpoint(0f, B1)
                .continueWith(Mapping.One)
                .toBreakpoint(10f, b2)
                .toBreakpoint(10f, B2)
                .completeWith(Mapping.Two)

        assertThat(result).breakpoints().keys().containsExactly(b1, b2).inOrder()
        assertThat(result).breakpoints().withKey(b1).isAt(0f)
        assertThat(result).breakpoints().withKey(b2).isAt(10f)
        assertThat(result).breakpoints().keys().containsExactly(B1, B2).inOrder()
        assertThat(result).breakpoints().withKey(B1).isAt(0f)
        assertThat(result).breakpoints().withKey(B2).isAt(10f)
        assertThat(result)
            .mappings()
            .containsExactly(Mapping.Zero, Mapping.One, Mapping.Two)
@@ -80,15 +80,15 @@ class FluentSpecBuilderTest {
    @Test
    fun directionalSpec_addBreakpointsAndMappings_inReverse() {
        val result =
            DirectionalMotionSpec.reverseBuilder(spring, Mapping.Two)
                .toBreakpoint(10f, b2)
            DirectionalMotionSpec.reverseBuilder(Spring, Mapping.Two)
                .toBreakpoint(10f, B2)
                .continueWith(Mapping.One)
                .toBreakpoint(0f, b1)
                .toBreakpoint(0f, B1)
                .completeWith(Mapping.Zero)

        assertThat(result).breakpoints().keys().containsExactly(b1, b2).inOrder()
        assertThat(result).breakpoints().withKey(b1).isAt(0f)
        assertThat(result).breakpoints().withKey(b2).isAt(10f)
        assertThat(result).breakpoints().keys().containsExactly(B1, B2).inOrder()
        assertThat(result).breakpoints().withKey(B1).isAt(0f)
        assertThat(result).breakpoints().withKey(B2).isAt(10f)
        assertThat(result)
            .mappings()
            .containsExactly(Mapping.Zero, Mapping.One, Mapping.Two)
@@ -98,20 +98,20 @@ class FluentSpecBuilderTest {
    @Test
    fun directionalSpec_mappingBuilder_setsDefaultSpring() {
        val result =
            DirectionalMotionSpec.builder(spring)
            DirectionalMotionSpec.builder(Spring)
                .toBreakpoint(10f)
                .jumpTo(20f)
                .continueWithConstantValue()
                .complete()

        assertThat(result).breakpoints().atPosition(10f).spring().isEqualTo(spring)
        assertThat(result).breakpoints().atPosition(10f).spring().isEqualTo(Spring)
    }

    @Test
    fun directionalSpec_mappingBuilder_canOverrideDefaultSpring() {
        val otherSpring = SpringParameters(stiffness = 10f, dampingRatio = 0.1f)
        val result =
            DirectionalMotionSpec.builder(spring)
            DirectionalMotionSpec.builder(Spring)
                .toBreakpoint(10f)
                .jumpTo(20f, spring = otherSpring)
                .continueWithConstantValue()
@@ -123,7 +123,7 @@ class FluentSpecBuilderTest {
    @Test
    fun directionalSpec_mappingBuilder_defaultsToNoGuarantee() {
        val result =
            DirectionalMotionSpec.builder(spring)
            DirectionalMotionSpec.builder(Spring)
                .toBreakpoint(10f)
                .jumpTo(20f)
                .continueWithConstantValue()
@@ -136,7 +136,7 @@ class FluentSpecBuilderTest {
    fun directionalSpec_mappingBuilder_canSetGuarantee() {
        val guarantee = Guarantee.InputDelta(10f)
        val result =
            DirectionalMotionSpec.builder(spring)
            DirectionalMotionSpec.builder(Spring)
                .toBreakpoint(10f)
                .jumpTo(20f, guarantee = guarantee)
                .continueWithConstantValue()
@@ -148,7 +148,7 @@ class FluentSpecBuilderTest {
    @Test
    fun directionalSpec_mappingBuilder_jumpTo_setsAbsoluteValue() {
        val result =
            DirectionalMotionSpec.builder(spring, Mapping.Fixed(99f))
            DirectionalMotionSpec.builder(Spring, Mapping.Fixed(99f))
                .toBreakpoint(10f)
                .jumpTo(20f)
                .continueWithConstantValue()
@@ -161,7 +161,7 @@ class FluentSpecBuilderTest {
    @Test
    fun directionalSpec_mappingBuilder_jumpBy_setsRelativeValue() {
        val result =
            DirectionalMotionSpec.builder(spring, Mapping.Linear(factor = 0.5f))
            DirectionalMotionSpec.builder(Spring, Mapping.Linear(factor = 0.5f))
                .toBreakpoint(10f)
                .jumpBy(30f)
                .continueWithConstantValue()
@@ -174,7 +174,7 @@ class FluentSpecBuilderTest {
    @Test
    fun directionalSpec_mappingBuilder_continueWithConstantValue_usesSourceValue() {
        val result =
            DirectionalMotionSpec.builder(spring, Mapping.Linear(factor = 0.5f))
            DirectionalMotionSpec.builder(Spring, Mapping.Linear(factor = 0.5f))
                .toBreakpoint(5f)
                .jumpBy(0f)
                .continueWithConstantValue()
@@ -186,7 +186,7 @@ class FluentSpecBuilderTest {
    @Test
    fun directionalSpec_mappingBuilder_continueWithFractionalInput_matchesLinearMapping() {
        val result =
            DirectionalMotionSpec.builder(spring)
            DirectionalMotionSpec.builder(Spring)
                .toBreakpoint(5f)
                .jumpTo(1f)
                .continueWithFractionalInput(fraction = .1f)
@@ -201,7 +201,7 @@ class FluentSpecBuilderTest {
    @Test
    fun directionalSpec_mappingBuilder_reverse_continueWithFractionalInput_matchesLinearMapping() {
        val result =
            DirectionalMotionSpec.reverseBuilder(spring)
            DirectionalMotionSpec.reverseBuilder(Spring)
                .toBreakpoint(15f)
                .jumpTo(2f)
                .continueWithFractionalInput(fraction = .1f)
@@ -216,7 +216,7 @@ class FluentSpecBuilderTest {
    @Test
    fun directionalSpec_mappingBuilder_continueWithTargetValue_matchesLinearMapping() {
        val result =
            DirectionalMotionSpec.builder(spring)
            DirectionalMotionSpec.builder(Spring)
                .toBreakpoint(5f)
                .jumpTo(1f)
                .continueWithTargetValue(target = 20f)
@@ -232,7 +232,7 @@ class FluentSpecBuilderTest {
    @Test
    fun directionalSpec_mappingBuilder_reverse_continueWithTargetValue_matchesLinearMapping() {
        val result =
            DirectionalMotionSpec.reverseBuilder(spring)
            DirectionalMotionSpec.reverseBuilder(Spring)
                .toBreakpoint(30f)
                .jumpTo(20f)
                .continueWithTargetValue(target = 1f)
@@ -246,9 +246,8 @@ class FluentSpecBuilderTest {
    }

    companion object {
        val spring = SpringParameters(stiffness = 100f, dampingRatio = 1f)
        val b1 = BreakpointKey("One")
        val b2 = BreakpointKey("Two")
        val b3 = BreakpointKey("Three")
        val Spring = SpringParameters(stiffness = 100f, dampingRatio = 1f)
        val B1 = BreakpointKey("One")
        val B2 = BreakpointKey("Two")
    }
}
Loading