Loading mechanics/src/com/android/mechanics/spec/MotionSpec.kt +4 −0 Original line number Diff line number Diff line Loading @@ -126,6 +126,8 @@ data class MotionSpec( ?: segmentAtInput(newPosition, newDirection) } override fun toString() = toDebugString() companion object { /** * Default spring parameters for the reset spring. Matches the Fast Spatial spring of the Loading Loading @@ -224,6 +226,8 @@ data class DirectionalMotionSpec( return result } override fun toString() = toDebugString() companion object { /* Empty spec, the full input domain is mapped to output using [Mapping.identity]. */ val Empty = Loading mechanics/src/com/android/mechanics/spec/MotionSpecDebugFormatter.kt 0 → 100644 +121 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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 /** Returns a string representation of the [MotionSpec] for debugging by humans. */ fun MotionSpec.toDebugString(): String { return buildString { if (minDirection == maxDirection) { appendLine("unidirectional:") appendLine(minDirection.toDebugString().prependIndent(" ")) } else { appendLine("maxDirection:") appendLine(maxDirection.toDebugString().prependIndent(" ")) appendLine("minDirection:") appendLine(minDirection.toDebugString().prependIndent(" ")) } if (segmentHandlers.isNotEmpty()) { appendLine("segmentHandlers:") segmentHandlers.keys.forEach { appendIndent(2) appendSegmentKey(it) appendLine() } } } .trim() } /** Returns a string representation of the [DirectionalMotionSpec] for debugging by humans. */ fun DirectionalMotionSpec.toDebugString(): String { return buildString { appendBreakpointLine(breakpoints.first()) for (i in mappings.indices) { appendMappingLine(mappings[i], indent = 2) semantics.forEach { appendSemanticsLine(it.key, it.values[i], indent = 4) } appendBreakpointLine(breakpoints[i + 1]) } } .trim() } private fun StringBuilder.appendIndent(indent: Int) { repeat(indent) { append(' ') } } private fun StringBuilder.appendBreakpointLine(breakpoint: Breakpoint, indent: Int = 0) { appendIndent(indent) append("@") append(breakpoint.position) append(" [") appendBreakpointKey(breakpoint.key) append("]") if (breakpoint.guarantee != Guarantee.None) { append(" guarantee=") append(breakpoint.key.debugLabel) } if (!breakpoint.spring.isSnapSpring) { append(" spring=") append(breakpoint.spring.stiffness) append("/") append(breakpoint.spring.dampingRatio) } appendLine() } private fun StringBuilder.appendBreakpointKey(key: BreakpointKey) { if (key.debugLabel != null) { append(key.debugLabel) append("|") } append("id:0x") append(System.identityHashCode(key.identity).toString(16).padStart(8, '0')) } private fun StringBuilder.appendSegmentKey(key: SegmentKey) { appendBreakpointKey(key.minBreakpoint) if (key.direction == InputDirection.Min) append(" << ") else append(" >> ") appendBreakpointKey(key.maxBreakpoint) } private fun StringBuilder.appendMappingLine(mapping: Mapping, indent: Int = 0) { appendIndent(indent) append(mapping.toString()) appendLine() } private fun StringBuilder.appendSemanticsLine( semanticKey: SemanticKey<*>, value: Any?, indent: Int = 0, ) { appendIndent(indent) append(semanticKey.debugLabel) append("[id:0x") append(System.identityHashCode(semanticKey.identity).toString(16).padStart(8, '0')) append("]") append("=") append(value) appendLine() } mechanics/tests/src/com/android/mechanics/spec/MotionSpecDebugFormatterTest.kt 0 → 100644 +145 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.spec.ChangeSegmentHandlers.PreventDirectionChangeWithinCurrentSegment import com.android.mechanics.spec.builder.MotionBuilderContext import com.android.mechanics.spec.builder.effectsDirectionalMotionSpec import com.android.mechanics.spec.builder.spatialDirectionalMotionSpec import com.android.mechanics.testing.FakeMotionSpecBuilderContext import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class MotionSpecDebugFormatterTest : MotionBuilderContext by FakeMotionSpecBuilderContext.Default { @Test fun motionSpec_unidirectionalSpec_formatIsUseful() { val spec = MotionSpec(effectsDirectionalMotionSpec { fixedValue(0f, value = 1f) }) assertThat(formatForTest(spec.toDebugString())) .isEqualTo( """ unidirectional: @-Infinity [built-in::min|id:0x1234cdef] Fixed(value=0.0) @0.0 [id:0x1234cdef] spring=1600.0/1.0 Fixed(value=1.0) @Infinity [built-in::max|id:0x1234cdef]""" .trimIndent() ) } @Test fun motionSpec_bidirectionalSpec_formatIsUseful() { val spec = MotionSpec( spatialDirectionalMotionSpec(Mapping.Zero) { fixedValue(0f, value = 1f) }, spatialDirectionalMotionSpec(Mapping.One) { fixedValue(0f, value = 0f) }, ) assertThat(formatForTest(spec.toDebugString())) .isEqualTo( """ maxDirection: @-Infinity [built-in::min|id:0x1234cdef] Fixed(value=0.0) @0.0 [id:0x1234cdef] spring=700.0/0.9 Fixed(value=1.0) @Infinity [built-in::max|id:0x1234cdef] minDirection: @-Infinity [built-in::min|id:0x1234cdef] Fixed(value=1.0) @0.0 [id:0x1234cdef] spring=700.0/0.9 Fixed(value=0.0) @Infinity [built-in::max|id:0x1234cdef]""" .trimIndent() ) } @Test fun motionSpec_semantics_formatIsUseful() { val semanticKey = SemanticKey<Float>("foo") val spec = MotionSpec( effectsDirectionalMotionSpec(semantics = listOf(semanticKey with 42f)) { fixedValue(0f, value = 1f, semantics = listOf(semanticKey with 43f)) } ) assertThat(formatForTest(spec.toDebugString())) .isEqualTo( """ unidirectional: @-Infinity [built-in::min|id:0x1234cdef] Fixed(value=0.0) foo[id:0x1234cdef]=42.0 @0.0 [id:0x1234cdef] spring=1600.0/1.0 Fixed(value=1.0) foo[id:0x1234cdef]=43.0 @Infinity [built-in::max|id:0x1234cdef]""" .trimIndent() ) } @Test fun motionSpec_segmentHandlers_formatIsUseful() { val key1 = BreakpointKey("1") val key2 = BreakpointKey("2") val spec = MotionSpec( effectsDirectionalMotionSpec { fixedValue(0f, value = 1f, key = key1) fixedValue(2f, value = 2f, key = key1) }, segmentHandlers = mapOf( SegmentKey(key1, key2, InputDirection.Max) to PreventDirectionChangeWithinCurrentSegment, SegmentKey(key1, key2, InputDirection.Min) to PreventDirectionChangeWithinCurrentSegment, ), ) assertThat(formatForTest(spec.toDebugString())) .isEqualTo( """ unidirectional: @-Infinity [built-in::min|id:0x1234cdef] Fixed(value=0.0) @0.0 [1|id:0x1234cdef] spring=1600.0/1.0 Fixed(value=1.0) @2.0 [1|id:0x1234cdef] spring=1600.0/1.0 Fixed(value=2.0) @Infinity [built-in::max|id:0x1234cdef] segmentHandlers: 1|id:0x1234cdef >> 2|id:0x1234cdef 1|id:0x1234cdef << 2|id:0x1234cdef""" .trimIndent() ) } companion object { private val idMatcher = Regex("id:0x[0-9a-f]{8}") fun formatForTest(debugString: String) = debugString.replace(idMatcher, "id:0x1234cdef").trim() } } Loading
mechanics/src/com/android/mechanics/spec/MotionSpec.kt +4 −0 Original line number Diff line number Diff line Loading @@ -126,6 +126,8 @@ data class MotionSpec( ?: segmentAtInput(newPosition, newDirection) } override fun toString() = toDebugString() companion object { /** * Default spring parameters for the reset spring. Matches the Fast Spatial spring of the Loading Loading @@ -224,6 +226,8 @@ data class DirectionalMotionSpec( return result } override fun toString() = toDebugString() companion object { /* Empty spec, the full input domain is mapped to output using [Mapping.identity]. */ val Empty = Loading
mechanics/src/com/android/mechanics/spec/MotionSpecDebugFormatter.kt 0 → 100644 +121 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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 /** Returns a string representation of the [MotionSpec] for debugging by humans. */ fun MotionSpec.toDebugString(): String { return buildString { if (minDirection == maxDirection) { appendLine("unidirectional:") appendLine(minDirection.toDebugString().prependIndent(" ")) } else { appendLine("maxDirection:") appendLine(maxDirection.toDebugString().prependIndent(" ")) appendLine("minDirection:") appendLine(minDirection.toDebugString().prependIndent(" ")) } if (segmentHandlers.isNotEmpty()) { appendLine("segmentHandlers:") segmentHandlers.keys.forEach { appendIndent(2) appendSegmentKey(it) appendLine() } } } .trim() } /** Returns a string representation of the [DirectionalMotionSpec] for debugging by humans. */ fun DirectionalMotionSpec.toDebugString(): String { return buildString { appendBreakpointLine(breakpoints.first()) for (i in mappings.indices) { appendMappingLine(mappings[i], indent = 2) semantics.forEach { appendSemanticsLine(it.key, it.values[i], indent = 4) } appendBreakpointLine(breakpoints[i + 1]) } } .trim() } private fun StringBuilder.appendIndent(indent: Int) { repeat(indent) { append(' ') } } private fun StringBuilder.appendBreakpointLine(breakpoint: Breakpoint, indent: Int = 0) { appendIndent(indent) append("@") append(breakpoint.position) append(" [") appendBreakpointKey(breakpoint.key) append("]") if (breakpoint.guarantee != Guarantee.None) { append(" guarantee=") append(breakpoint.key.debugLabel) } if (!breakpoint.spring.isSnapSpring) { append(" spring=") append(breakpoint.spring.stiffness) append("/") append(breakpoint.spring.dampingRatio) } appendLine() } private fun StringBuilder.appendBreakpointKey(key: BreakpointKey) { if (key.debugLabel != null) { append(key.debugLabel) append("|") } append("id:0x") append(System.identityHashCode(key.identity).toString(16).padStart(8, '0')) } private fun StringBuilder.appendSegmentKey(key: SegmentKey) { appendBreakpointKey(key.minBreakpoint) if (key.direction == InputDirection.Min) append(" << ") else append(" >> ") appendBreakpointKey(key.maxBreakpoint) } private fun StringBuilder.appendMappingLine(mapping: Mapping, indent: Int = 0) { appendIndent(indent) append(mapping.toString()) appendLine() } private fun StringBuilder.appendSemanticsLine( semanticKey: SemanticKey<*>, value: Any?, indent: Int = 0, ) { appendIndent(indent) append(semanticKey.debugLabel) append("[id:0x") append(System.identityHashCode(semanticKey.identity).toString(16).padStart(8, '0')) append("]") append("=") append(value) appendLine() }
mechanics/tests/src/com/android/mechanics/spec/MotionSpecDebugFormatterTest.kt 0 → 100644 +145 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.spec.ChangeSegmentHandlers.PreventDirectionChangeWithinCurrentSegment import com.android.mechanics.spec.builder.MotionBuilderContext import com.android.mechanics.spec.builder.effectsDirectionalMotionSpec import com.android.mechanics.spec.builder.spatialDirectionalMotionSpec import com.android.mechanics.testing.FakeMotionSpecBuilderContext import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class MotionSpecDebugFormatterTest : MotionBuilderContext by FakeMotionSpecBuilderContext.Default { @Test fun motionSpec_unidirectionalSpec_formatIsUseful() { val spec = MotionSpec(effectsDirectionalMotionSpec { fixedValue(0f, value = 1f) }) assertThat(formatForTest(spec.toDebugString())) .isEqualTo( """ unidirectional: @-Infinity [built-in::min|id:0x1234cdef] Fixed(value=0.0) @0.0 [id:0x1234cdef] spring=1600.0/1.0 Fixed(value=1.0) @Infinity [built-in::max|id:0x1234cdef]""" .trimIndent() ) } @Test fun motionSpec_bidirectionalSpec_formatIsUseful() { val spec = MotionSpec( spatialDirectionalMotionSpec(Mapping.Zero) { fixedValue(0f, value = 1f) }, spatialDirectionalMotionSpec(Mapping.One) { fixedValue(0f, value = 0f) }, ) assertThat(formatForTest(spec.toDebugString())) .isEqualTo( """ maxDirection: @-Infinity [built-in::min|id:0x1234cdef] Fixed(value=0.0) @0.0 [id:0x1234cdef] spring=700.0/0.9 Fixed(value=1.0) @Infinity [built-in::max|id:0x1234cdef] minDirection: @-Infinity [built-in::min|id:0x1234cdef] Fixed(value=1.0) @0.0 [id:0x1234cdef] spring=700.0/0.9 Fixed(value=0.0) @Infinity [built-in::max|id:0x1234cdef]""" .trimIndent() ) } @Test fun motionSpec_semantics_formatIsUseful() { val semanticKey = SemanticKey<Float>("foo") val spec = MotionSpec( effectsDirectionalMotionSpec(semantics = listOf(semanticKey with 42f)) { fixedValue(0f, value = 1f, semantics = listOf(semanticKey with 43f)) } ) assertThat(formatForTest(spec.toDebugString())) .isEqualTo( """ unidirectional: @-Infinity [built-in::min|id:0x1234cdef] Fixed(value=0.0) foo[id:0x1234cdef]=42.0 @0.0 [id:0x1234cdef] spring=1600.0/1.0 Fixed(value=1.0) foo[id:0x1234cdef]=43.0 @Infinity [built-in::max|id:0x1234cdef]""" .trimIndent() ) } @Test fun motionSpec_segmentHandlers_formatIsUseful() { val key1 = BreakpointKey("1") val key2 = BreakpointKey("2") val spec = MotionSpec( effectsDirectionalMotionSpec { fixedValue(0f, value = 1f, key = key1) fixedValue(2f, value = 2f, key = key1) }, segmentHandlers = mapOf( SegmentKey(key1, key2, InputDirection.Max) to PreventDirectionChangeWithinCurrentSegment, SegmentKey(key1, key2, InputDirection.Min) to PreventDirectionChangeWithinCurrentSegment, ), ) assertThat(formatForTest(spec.toDebugString())) .isEqualTo( """ unidirectional: @-Infinity [built-in::min|id:0x1234cdef] Fixed(value=0.0) @0.0 [1|id:0x1234cdef] spring=1600.0/1.0 Fixed(value=1.0) @2.0 [1|id:0x1234cdef] spring=1600.0/1.0 Fixed(value=2.0) @Infinity [built-in::max|id:0x1234cdef] segmentHandlers: 1|id:0x1234cdef >> 2|id:0x1234cdef 1|id:0x1234cdef << 2|id:0x1234cdef""" .trimIndent() ) } companion object { private val idMatcher = Regex("id:0x[0-9a-f]{8}") fun formatForTest(debugString: String) = debugString.replace(idMatcher, "id:0x1234cdef").trim() } }