1 /* <lambda>null2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.mechanics.spec 18 19 import com.android.mechanics.spring.SpringParameters 20 21 /** 22 * Handler to allow for custom segment-change logic. 23 * 24 * This handler is called whenever the new input (position or direction) does not match 25 * [currentSegment] anymore (see [SegmentData.isValidForInput]). 26 * 27 * This is intended to implement custom effects on direction-change. 28 * 29 * Implementations can return: 30 * 1. [currentSegment] to delay/suppress segment change. 31 * 2. `null` to use the default segment lookup based on [newPosition] and [newDirection] 32 * 3. manually looking up segments on this [MotionSpec] 33 * 4. create a [SegmentData] that is not in the spec. 34 */ 35 typealias OnChangeSegmentHandler = 36 MotionSpec.( 37 currentSegment: SegmentData, newPosition: Float, newDirection: InputDirection, 38 ) -> SegmentData? 39 40 /** 41 * Specification for the mapping of input values to output values. 42 * 43 * The spec consists of two independent directional spec's, while only one the one matching 44 * `MotionInput`'s `direction` is used at any given time. 45 * 46 * @param maxDirection spec used when the MotionInput's direction is [InputDirection.Max] 47 * @param minDirection spec used when the MotionInput's direction is [InputDirection.Min] 48 * @param resetSpring spring parameters to animate a difference in output, if the difference is 49 * caused by setting this new spec. 50 * @param segmentHandlers allow for custom segment-change logic, when the `MotionValue` runtime 51 * would leave the [SegmentKey]. 52 */ 53 data class MotionSpec( 54 val maxDirection: DirectionalMotionSpec, 55 val minDirection: DirectionalMotionSpec = maxDirection, 56 val resetSpring: SpringParameters = DefaultResetSpring, 57 val segmentHandlers: Map<SegmentKey, OnChangeSegmentHandler> = emptyMap(), 58 ) { 59 60 /** The [DirectionalMotionSpec] for the specified [direction]. */ 61 operator fun get(direction: InputDirection): DirectionalMotionSpec { 62 return when (direction) { 63 InputDirection.Min -> minDirection 64 InputDirection.Max -> maxDirection 65 } 66 } 67 68 /** Whether this spec contains a segment with the specified [segmentKey]. */ 69 fun containsSegment(segmentKey: SegmentKey): Boolean { 70 return get(segmentKey.direction).findSegmentIndex(segmentKey) != -1 71 } 72 73 /** 74 * The [SegmentData] for an input with the specified [position] and [direction]. 75 * 76 * The returned [SegmentData] will be cached while [SegmentData.isValidForInput] returns `true`. 77 */ 78 fun segmentAtInput(position: Float, direction: InputDirection): SegmentData { 79 require(position.isFinite()) 80 81 return with(get(direction)) { 82 var idx = findBreakpointIndex(position) 83 if (direction == InputDirection.Min && breakpoints[idx].position == position) { 84 // The segment starts at `position`. Since the breakpoints are sorted ascending, no 85 // matter the spec's direction, need to return the previous segment in the min 86 // direction. 87 idx-- 88 } 89 90 SegmentData( 91 this@MotionSpec, 92 breakpoints[idx], 93 breakpoints[idx + 1], 94 direction, 95 mappings[idx], 96 ) 97 } 98 } 99 100 /** 101 * Looks up the new [SegmentData] once the [currentSegment] is not valid for an input with 102 * [newPosition] and [newDirection]. 103 * 104 * This will delegate to the [segmentHandlers], if registered for the [currentSegment]'s key. 105 */ 106 internal fun onChangeSegment( 107 currentSegment: SegmentData, 108 newPosition: Float, 109 newDirection: InputDirection, 110 ): SegmentData { 111 val segmentChangeHandler = segmentHandlers[currentSegment.key] 112 return segmentChangeHandler?.invoke(this, currentSegment, newPosition, newDirection) 113 ?: segmentAtInput(newPosition, newDirection) 114 } 115 116 companion object { 117 /** 118 * Default spring parameters for the reset spring. Matches the Fast Spatial spring of the 119 * standard motion scheme. 120 */ 121 private val DefaultResetSpring = SpringParameters(stiffness = 1400f, dampingRatio = 1f) 122 123 /* Empty motion spec, the output is the same as the input. */ 124 val Empty = MotionSpec(DirectionalMotionSpec.Empty) 125 } 126 } 127 128 /** 129 * Defines the [breakpoints], as well as the [mappings] in-between adjacent [Breakpoint] pairs. 130 * 131 * This [DirectionalMotionSpec] is applied in the direction defined by the containing [MotionSpec]: 132 * especially the direction in which the `breakpoint` [Guarantee] are applied depend on how this is 133 * used; this type does not have an inherit direction. 134 * 135 * All [breakpoints] are sorted in ascending order by their `position`, with the first and last 136 * breakpoints are guaranteed to be sentinel values for negative and positive infinity respectively. 137 * 138 * @param breakpoints All breakpoints in the spec, must contain [Breakpoint.minLimit] as the first 139 * element, and [Breakpoint.maxLimit] as the last element. 140 * @param mappings All mappings in between the breakpoints, thus must always contain 141 * `breakpoints.size - 1` elements. 142 */ 143 data class DirectionalMotionSpec(val breakpoints: List<Breakpoint>, val mappings: List<Mapping>) { 144 init { 145 require(breakpoints.size >= 2) 146 require(breakpoints.first() == Breakpoint.minLimit) 147 require(breakpoints.last() == Breakpoint.maxLimit) <lambda>null148 require(breakpoints.zipWithNext { a, b -> a <= b }.all { it }) { <lambda>null149 "Breakpoints are not sorted ascending ${breakpoints.map { "${it.key}@${it.position}" }}" 150 } 151 require(mappings.size == breakpoints.size - 1) 152 } 153 154 /** 155 * Returns the index of the closest breakpoint where `Breakpoint.position <= position`. 156 * 157 * Guaranteed to be a valid index into [breakpoints], and guaranteed to be neither the first nor 158 * the last element. 159 * 160 * @param position the position in the input domain. 161 * @return Index into [breakpoints], guaranteed to be in range `1..breakpoints.size - 2` 162 */ findBreakpointIndexnull163 fun findBreakpointIndex(position: Float): Int { 164 require(position.isFinite()) 165 val breakpointPosition = breakpoints.binarySearchBy(position) { it.position } 166 167 val result = 168 when { 169 // position is between two anchors, return the min one. 170 breakpointPosition < 0 -> -breakpointPosition - 2 171 else -> breakpointPosition 172 } 173 174 check(result >= 0) 175 check(result < breakpoints.size - 1) 176 177 return result 178 } 179 180 /** 181 * The index of the breakpoint with the specified [breakpointKey], or `-1` if no such breakpoint 182 * exists. 183 */ findBreakpointIndexnull184 fun findBreakpointIndex(breakpointKey: BreakpointKey): Int { 185 return breakpoints.indexOfFirst { it.key == breakpointKey } 186 } 187 188 /** Index into [mappings] for the specified [segmentKey], or `-1` if no such segment exists. */ findSegmentIndexnull189 fun findSegmentIndex(segmentKey: SegmentKey): Int { 190 val result = breakpoints.indexOfFirst { it.key == segmentKey.minBreakpoint } 191 if (result < 0 || breakpoints[result + 1].key != segmentKey.maxBreakpoint) return -1 192 193 return result 194 } 195 196 companion object { 197 /* Empty spec, the full input domain is mapped to output using [Mapping.identity]. */ 198 val Empty = 199 DirectionalMotionSpec( 200 listOf(Breakpoint.minLimit, Breakpoint.maxLimit), 201 listOf(Mapping.Identity), 202 ) 203 } 204 } 205