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

Commit 68a2c201 authored by Mike Schneider's avatar Mike Schneider Committed by Android (Google) Code Review
Browse files

Merge changes from topics "directional_builder", "remove_fluent_builder" into main

* changes:
  Remove FluentSpecBuilder
  Add `identity` mapping to `DirectionalBuilderScope`
  Make buildDirectionalMotionSpec match the style of `motionSpec` builder
parents acff6f40 c60c7355
Loading
Loading
Loading
Loading
+3 −3
Original line number Diff line number Diff line
@@ -29,7 +29,7 @@ import com.android.mechanics.spec.Guarantee
import com.android.mechanics.spec.InputDirection
import com.android.mechanics.spec.Mapping
import com.android.mechanics.spec.MotionSpec
import com.android.mechanics.spec.buildDirectionalMotionSpec
import com.android.mechanics.spec.builder.directionalMotionSpec
import com.android.mechanics.spring.SpringParameters
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.launch
@@ -169,7 +169,7 @@ class MotionValueBenchmark {
    private val MotionSpec.Companion.ZeroToOne_AtOne
        get() =
            MotionSpec(
                buildDirectionalMotionSpec(
                directionalMotionSpec(
                    defaultSpring = SpringParameters(stiffness = 300f, dampingRatio = .9f),
                    initialMapping = Mapping.Zero,
                ) {
@@ -228,7 +228,7 @@ class MotionValueBenchmark {
    private val MotionSpec.Companion.ZeroToOne_AtOne_WithGuarantee
        get() =
            MotionSpec(
                buildDirectionalMotionSpec(
                directionalMotionSpec(
                    defaultSpring = SpringParameters(stiffness = 300f, dampingRatio = .9f),
                    initialMapping = Mapping.Zero,
                ) {
+33 −33
Original line number Diff line number Diff line
@@ -27,16 +27,13 @@ import androidx.compose.ui.util.fastCoerceIn
import androidx.compose.ui.util.lerp
import com.android.mechanics.spec.Breakpoint
import com.android.mechanics.spec.BreakpointKey
import com.android.mechanics.spec.DirectionalMotionSpec
import com.android.mechanics.spec.InputDirection
import com.android.mechanics.spec.Mapping
import com.android.mechanics.spec.MotionSpec
import com.android.mechanics.spec.OnChangeSegmentHandler
import com.android.mechanics.spec.SegmentData
import com.android.mechanics.spec.SegmentKey
import com.android.mechanics.spec.buildDirectionalMotionSpec
import com.android.mechanics.spec.builder
import com.android.mechanics.spec.reverseBuilder
import com.android.mechanics.spec.builder.directionalMotionSpec
import com.android.mechanics.spring.SpringParameters

/** Motion spec for a vertically expandable container. */
@@ -54,23 +51,38 @@ class VerticalExpandContainerSpec(
    val opacitySpring: SpringParameters = Defaults.OpacitySpring,
) {
    fun createHeightSpec(motionScheme: MotionScheme, density: Density): MotionSpec {
        // TODO: michschn@ - replace with MagneticDetach
        return with(density) {
            val spatialSpring = SpringParameters(motionScheme.defaultSpatialSpec())

            val detachSpec =
                DirectionalMotionSpec.builder(
                directionalMotionSpec(
                    initialMapping = Mapping.Zero,
                    defaultSpring = spatialSpring,
                ) {
                    fractionalInputFromCurrent(
                        breakpoint = 0f,
                        key = Breakpoints.Attach,
                        fraction = preDetachRatio,
                    )
                    identity(
                        breakpoint = detachHeight.toPx(),
                        key = Breakpoints.Detach,
                        spring = detachSpring,
                    )
                    .toBreakpoint(0f, key = Breakpoints.Attach)
                    .continueWith(Mapping.Linear(preDetachRatio))
                    .toBreakpoint(detachHeight.toPx(), key = Breakpoints.Detach)
                    .completeWith(Mapping.Identity, detachSpring)
                }

            val attachSpec =
                DirectionalMotionSpec.reverseBuilder(defaultSpring = spatialSpring)
                    .toBreakpoint(attachHeight.toPx(), key = Breakpoints.Detach)
                    .completeWith(mapping = Mapping.Zero, attachSpring)
                directionalMotionSpec(
                    initialMapping = Mapping.Zero,
                    defaultSpring = spatialSpring,
                ) {
                    identity(
                        breakpoint = attachHeight.toPx(),
                        key = Breakpoints.Detach,
                        spring = attachSpring,
                    )
                }

            val segmentHandlers =
                mapOf<SegmentKey, OnChangeSegmentHandler>(
@@ -102,10 +114,10 @@ class VerticalExpandContainerSpec(
    ): MotionSpec {
        return with(density) {
            if (isFloating) {
                MotionSpec(buildDirectionalMotionSpec(Mapping.Fixed(intrinsicWidth)))
                MotionSpec(directionalMotionSpec(Mapping.Fixed(intrinsicWidth)))
            } else {
                MotionSpec(
                    buildDirectionalMotionSpec({ input ->
                    directionalMotionSpec({ input ->
                        val fraction = (input / detachHeight.toPx()).fastCoerceIn(0f, 1f)
                        intrinsicWidth - lerp(widthOffset.toPx(), 0f, fraction)
                    })
@@ -116,23 +128,11 @@ class VerticalExpandContainerSpec(

    fun createAlphaSpec(motionScheme: MotionScheme, density: Density): MotionSpec {
        return with(density) {
            val detachSpec =
                DirectionalMotionSpec.builder(
                        SpringParameters(motionScheme.defaultEffectsSpec()),
                        initialMapping = Mapping.Zero,
                    )
                    .toBreakpoint(visibleHeight.toPx())
                    .completeWith(Mapping.One, opacitySpring)

            val attachSpec =
                DirectionalMotionSpec.builder(
                        SpringParameters(motionScheme.defaultEffectsSpec()),
                        initialMapping = Mapping.Zero,
            MotionSpec(
                directionalMotionSpec(opacitySpring, initialMapping = Mapping.Zero) {
                    constantValue(breakpoint = visibleHeight.toPx(), value = 1f)
                }
            )
                    .toBreakpoint(visibleHeight.toPx())
                    .completeWith(Mapping.One, opacitySpring)

            MotionSpec(maxDirection = detachSpec, minDirection = attachSpec)
        }
    }

+0 −375
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 com.android.mechanics.spring.SpringParameters

/**
 * Fluent builder for [DirectionalMotionSpec].
 *
 * This builder ensures correctness at compile-time, and simplifies the expression of the
 * input-to-output mapping.
 *
 * The [MotionSpec] is defined by specify interleaved [Mapping]s and [Breakpoint]s. [Breakpoint]s
 * must be specified in ascending order.
 *
 * NOTE: The returned fluent interfaces must only be used for chaining calls to build exactly one
 * [DirectionalMotionSpec], otherwise resulting behavior is undefined, since the builder is
 * internally mutated.
 *
 * @param defaultSpring spring to use for all breakpoints by default.
 * @param initialMapping the [Mapping] from [Breakpoint.minLimit] to the next [Breakpoint].
 * @see reverseBuilder to specify [Breakpoint]s in descending order.
 */
fun DirectionalMotionSpec.Companion.builder(
    defaultSpring: SpringParameters,
    initialMapping: Mapping = Mapping.Identity,
): FluentSpecEndSegmentWithNextBreakpoint<DirectionalMotionSpec> {
    return FluentSpecBuilder(defaultSpring, InputDirection.Max) { it }
        .apply { mappings.add(initialMapping) }
}

/**
 * Fluent builder for [DirectionalMotionSpec], specifying breakpoints and mappings in reverse order.
 *
 * Variant of [DirectionalMotionSpec.Companion.builder], where [Breakpoint]s must be specified in
 * *descending* order. The resulting [DirectionalMotionSpec] will contain the breakpoints in
 * ascending order.
 *
 * @param defaultSpring spring to use for all breakpoints by default.
 * @param initialMapping the [Mapping] from [Breakpoint.maxLimit] to the next [Breakpoint].
 * @see DirectionalMotionSpec.Companion.builder for more documentation.
 */
fun DirectionalMotionSpec.Companion.reverseBuilder(
    defaultSpring: SpringParameters,
    initialMapping: Mapping = Mapping.Identity,
): FluentSpecEndSegmentWithNextBreakpoint<DirectionalMotionSpec> {
    return FluentSpecBuilder(defaultSpring, InputDirection.Min) { it }
        .apply { mappings.add(initialMapping) }
}

/**
 * Fluent builder for a [MotionSpec], which uses the same spec in both directions.
 *
 * @param defaultSpring spring to use for all breakpoints by default.
 * @param initialMapping [Mapping] for the first segment
 * @param resetSpring the [MotionSpec.resetSpring].
 */
fun MotionSpec.Companion.builder(
    defaultSpring: SpringParameters,
    initialMapping: Mapping = Mapping.Identity,
    resetSpring: SpringParameters = defaultSpring,
): FluentSpecEndSegmentWithNextBreakpoint<MotionSpec> {
    return FluentSpecBuilder(defaultSpring, InputDirection.Max) {
            MotionSpec(it, resetSpring = resetSpring)
        }
        .apply { mappings.add(initialMapping) }
}

/** Fluent-interface to end the current segment, by placing the next [Breakpoint]. */
interface FluentSpecEndSegmentWithNextBreakpoint<R> {
    /**
     * Adds a new [Breakpoint] at the specified position.
     *
     * @param atPosition The position of the breakpoint, in the input domain of the [MotionValue].
     * @param key identifies the breakpoint in the [DirectionalMotionSpec]. Must be specified to
     *   reference the breakpoint or segment.
     */
    fun toBreakpoint(
        atPosition: Float,
        key: BreakpointKey = BreakpointKey(),
    ): FluentSpecDefineBreakpointAndStartNextSegment<R>

    /** Completes the spec by placing the last, implicit [Breakpoint]. */
    fun complete(): R
}

/** Fluent-interface to define the [Breakpoint]'s properties and start to start the next segment. */
interface FluentSpecDefineBreakpointAndStartNextSegment<R> {
    /**
     * Default spring parameters for breakpoint, as specified at creation time of the builder.
     *
     * Used as the default `spring` parameters.
     */
    val defaultSpring: SpringParameters

    /**
     * Starts the next segment, using the specified mapping.
     *
     * @param mapping the mapping to use for the next segment.
     * @param spring the spring to animate this breakpoints discontinuity.
     * @param guarantee a guarantee by when the animation must be complete
     */
    fun continueWith(
        mapping: Mapping,
        spring: SpringParameters = defaultSpring,
        guarantee: Guarantee = Guarantee.None,
    ): FluentSpecEndSegmentWithNextBreakpoint<R>

    /**
     * Starts the next linear-mapped segment, by specifying the output [value] this breakpoint.
     *
     * @param value the output value the new mapping will produce at this breakpoints position.
     * @param spring the spring to animate this breakpoints discontinuity.
     * @param guarantee a guarantee by when the animation must be complete
     */
    fun jumpTo(
        value: Float,
        spring: SpringParameters = defaultSpring,
        guarantee: Guarantee = Guarantee.None,
    ): FluentSpecDefineLinearSegmentMapping<R>

    /**
     * Starts the next linear-mapped segment, by offsetting the output by [delta] from the incoming
     * mapping.
     *
     * @param delta the delta in output from the previous mapping's output.
     * @param spring the spring to animate this breakpoints discontinuity.
     * @param guarantee a guarantee by when the animation must be complete
     */
    fun jumpBy(
        delta: Float,
        spring: SpringParameters = defaultSpring,
        guarantee: Guarantee = Guarantee.None,
    ): FluentSpecDefineLinearSegmentMapping<R>

    /**
     * Completes the spec by using [mapping] between the this and the implicit sentinel breakpoint
     * at infinity.
     *
     * @param mapping the mapping to use for the final segment.
     * @param spring the spring to animate this breakpoints discontinuity.
     * @param guarantee a guarantee by when the animation must be complete
     */
    fun completeWith(
        mapping: Mapping,
        spring: SpringParameters = defaultSpring,
        guarantee: Guarantee = Guarantee.None,
    ): R
}

/** Fluent-interface to define a linear mapping between two breakpoints. */
interface FluentSpecDefineLinearSegmentMapping<R> {
    /**
     * The linear-mapping will produce the specified [target] output at the next breakpoint
     * position.
     *
     * @param target the output value the new mapping will produce at the next breakpoint position.
     */
    fun continueWithTargetValue(target: Float): FluentSpecEndSegmentWithNextBreakpoint<R>

    /**
     * Defines the slope for the linear mapping, as a fraction of the input value.
     *
     * @param fraction the multiplier applied to the input value..
     */
    fun continueWithFractionalInput(fraction: Float): FluentSpecEndSegmentWithNextBreakpoint<R>

    /**
     * The linear-mapping will produce a constant value, as defined at the source breakpoint of this
     * segment.
     */
    fun continueWithConstantValue(): FluentSpecEndSegmentWithNextBreakpoint<R>
}

/** Implements the fluent spec builder logic. */
private class FluentSpecBuilder<R>(
    override val defaultSpring: SpringParameters,
    buildDirection: InputDirection = InputDirection.Max,
    private val toResult: (DirectionalMotionSpec) -> R,
) :
    FluentSpecDefineLinearSegmentMapping<R>,
    FluentSpecDefineBreakpointAndStartNextSegment<R>,
    FluentSpecEndSegmentWithNextBreakpoint<R> {
    private val buildForward = buildDirection == InputDirection.Max

    val breakpoints = mutableListOf<Breakpoint>()
    val mappings = mutableListOf<Mapping>()

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

    init {
        val initialBreakpoint = if (buildForward) Breakpoint.minLimit else Breakpoint.maxLimit
        breakpoints.add(initialBreakpoint)
    }

    //  FluentSpecDefineLinearSegmentMapping

    override fun continueWithTargetValue(target: Float): FluentSpecEndSegmentWithNextBreakpoint<R> {
        check(sourceValue.isFinite())

        // memoize for FluentSpecEndSegmentWithNextBreakpoint
        targetValue = target

        return this
    }

    override fun continueWithFractionalInput(
        fraction: Float
    ): FluentSpecEndSegmentWithNextBreakpoint<R> {
        check(sourceValue.isFinite())

        // memoize for FluentSpecEndSegmentWithNextBreakpoint
        fractionalMapping = fraction

        return this
    }

    override fun continueWithConstantValue(): FluentSpecEndSegmentWithNextBreakpoint<R> {
        check(sourceValue.isFinite())

        mappings.add(Mapping.Fixed(sourceValue))

        sourceValue = Float.NaN
        return this
    }

    // FluentSpecDefineBreakpointAndStartNextSegment implementation

    override fun jumpTo(
        value: Float,
        spring: SpringParameters,
        guarantee: Guarantee,
    ): FluentSpecDefineLinearSegmentMapping<R> {
        check(sourceValue.isNaN())

        doAddBreakpoint(spring, guarantee)
        sourceValue = value

        return this
    }

    override fun jumpBy(
        delta: Float,
        spring: SpringParameters,
        guarantee: Guarantee,
    ): FluentSpecDefineLinearSegmentMapping<R> {
        check(sourceValue.isNaN())

        val breakpoint = doAddBreakpoint(spring, guarantee)
        sourceValue = mappings.last().map(breakpoint.position) + delta

        return this
    }

    override fun continueWith(
        mapping: Mapping,
        spring: SpringParameters,
        guarantee: Guarantee,
    ): FluentSpecEndSegmentWithNextBreakpoint<R> {
        check(sourceValue.isNaN())

        doAddBreakpoint(spring, guarantee)
        mappings.add(mapping)

        return this
    }

    override fun completeWith(mapping: Mapping, spring: SpringParameters, guarantee: Guarantee): R {
        check(sourceValue.isNaN())

        doAddBreakpoint(spring, guarantee)
        mappings.add(mapping)

        return complete()
    }

    // FluentSpecEndSegmentWithNextBreakpoint implementation

    override fun toBreakpoint(
        atPosition: Float,
        key: BreakpointKey,
    ): FluentSpecDefineBreakpointAndStartNextSegment<R> {
        check(breakpointPosition.isNaN())
        check(breakpointKey == null)

        if (!targetValue.isNaN() || !fractionalMapping.isNaN()) {
            check(!sourceValue.isNaN())

            val sourcePosition = breakpoints.last().position
            val breakpointDistance = atPosition - sourcePosition
            val mapping =
                if (breakpointDistance == 0f) {
                    Mapping.Fixed(sourceValue)
                } else {
                    if (fractionalMapping.isNaN()) {
                        val delta = targetValue - sourceValue
                        fractionalMapping = delta / breakpointDistance
                    } else {
                        val delta = breakpointDistance * fractionalMapping
                        targetValue = sourceValue + delta
                    }

                    val offset =
                        if (buildForward) sourceValue - (sourcePosition * fractionalMapping)
                        else targetValue - (atPosition * fractionalMapping)
                    Mapping.Linear(fractionalMapping, offset)
                }

            mappings.add(mapping)
            targetValue = Float.NaN
            sourceValue = Float.NaN
            fractionalMapping = Float.NaN
        }

        breakpointPosition = atPosition
        breakpointKey = key

        return this
    }

    override fun complete(): R {
        check(targetValue.isNaN()) { "cant specify target value for last segment" }

        if (!fractionalMapping.isNaN()) {
            check(!sourceValue.isNaN())

            val sourcePosition = breakpoints.last().position

            mappings.add(
                Mapping.Linear(
                    fractionalMapping,
                    sourceValue - (sourcePosition * fractionalMapping),
                )
            )
        }

        if (buildForward) {
            breakpoints.add(Breakpoint.maxLimit)
        } else {
            breakpoints.add(Breakpoint.minLimit)
            breakpoints.reverse()
            mappings.reverse()
        }

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

    private fun doAddBreakpoint(springSpec: SpringParameters, guarantee: Guarantee): Breakpoint {
        check(breakpointPosition.isFinite())
        return Breakpoint(checkNotNull(breakpointKey), breakpointPosition, springSpec, guarantee)
            .also {
                breakpoints.add(it)
                breakpointPosition = Float.NaN
                breakpointKey = null
            }
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -28,7 +28,7 @@ import com.android.mechanics.spring.SpringParameters
/**
 * Internal, reusable implementation of the [DirectionalBuilderScope].
 *
 * Clients must use [buildDirectionalMotionSpec] instead.
 * Clients must use [directionalMotionSpec] instead.
 */
internal class DirectionalBuilderImpl(
    override val defaultSpring: SpringParameters,
+38 −0
Original line number Diff line number Diff line
@@ -229,6 +229,44 @@ interface DirectionalBuilderScope {
        semantics: List<SemanticValue<*>> = emptyList(),
        mapping: Mapping,
    ): CanBeLastSegment

    /**
     * Ends the current segment at the [breakpoint] position and defines the next segment to produce
     * the input value as output (optionally with an offset of [delta]).
     *
     * Note: This segment can be used as the last segment in the specification.
     *
     * @param breakpoint The breakpoint defining the end of the current segment and the start of the
     *   next.
     * @param delta An optional offset to apply to the mapped value to determine the constant value.
     * @param spring The [SpringParameters] for the transition to this breakpoint.
     * @param guarantee The animation guarantee for this transition.
     * @param key A unique [BreakpointKey] for this breakpoint.
     * @param semantics Updated semantics values to be applied. Must be a subset of the
     *   [SemanticKey]s used when first creating this builder.
     */
    fun identity(
        breakpoint: Float,
        delta: Float = 0f,
        spring: SpringParameters = defaultSpring,
        guarantee: Guarantee = Guarantee.None,
        key: BreakpointKey = BreakpointKey(),
        semantics: List<SemanticValue<*>> = emptyList(),
    ): CanBeLastSegment {
        return if (delta == 0f) {
            mapping(breakpoint, spring, guarantee, key, semantics, Mapping.Identity)
        } else {
            fractionalInput(
                breakpoint,
                fraction = 1f,
                from = breakpoint + delta,
                spring = spring,
                guarantee = guarantee,
                key = key,
                semantics = semantics,
            )
        }
    }
}

/** Marker interface to indicate that a segment can be the last one in a [DirectionalMotionSpec]. */
Loading