Prettier visualization for motion mechanics debug.
Bug: 390325138
Test: Manual / demo code only
Flag: com.android.systemui.scene_container
Change-Id: I2e10b93fd3cd019a1c38e740a0982f7a1690bc52
diff --git a/mechanics/Android.bp b/mechanics/Android.bp
index 7b8748a..a091c09 100644
--- a/mechanics/Android.bp
+++ b/mechanics/Android.bp
@@ -31,6 +31,7 @@
min_sdk_version: "current",
static_libs: [
"androidx.compose.runtime_runtime",
+ "androidx.compose.material3_material3",
"androidx.compose.ui_ui-util",
"androidx.compose.foundation_foundation-layout",
],
diff --git a/mechanics/src/com/android/mechanics/debug/DebugVisualization.kt b/mechanics/src/com/android/mechanics/debug/DebugVisualization.kt
index c7ab635..9744350 100644
--- a/mechanics/src/com/android/mechanics/debug/DebugVisualization.kt
+++ b/mechanics/src/com/android/mechanics/debug/DebugVisualization.kt
@@ -17,18 +17,26 @@
package com.android.mechanics.debug
import androidx.compose.foundation.layout.Spacer
+import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.graphics.drawscope.scale
+import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.node.DrawModifierNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.ObserverModifierNode
@@ -39,9 +47,13 @@
import com.android.mechanics.MotionValue
import com.android.mechanics.spec.DirectionalMotionSpec
import com.android.mechanics.spec.Guarantee
+import com.android.mechanics.spec.InputDirection
import com.android.mechanics.spec.MotionSpec
+import com.android.mechanics.spec.SegmentKey
import kotlin.math.max
import kotlin.math.min
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
/**
* A debug visualization of the [motionValue].
@@ -52,25 +64,48 @@
*
* @param motionValue The [MotionValue] to inspect.
* @param inputRange The relevant range of the input (x) axis, for which to draw the graph.
- * @param color Color for the dots indicating the value
- * @param historySize Number of past values to draw as a trail.
+ * @param maxAgeMillis Max age of the elements in the history trail.
*/
@Composable
fun DebugMotionValueVisualization(
motionValue: MotionValue,
inputRange: ClosedFloatingPointRange<Float>,
modifier: Modifier = Modifier,
- color: Color = Color.DarkGray,
- historySize: Int = 100,
+ maxAgeMillis: Long = 1000L,
) {
val spec = motionValue.spec
val outputRange = remember(spec, inputRange) { spec.computeOutputValueRange(inputRange) }
+ val inspector = remember(motionValue) { motionValue.debugInspector() }
+
+ DisposableEffect(inspector) { onDispose { inspector.dispose() } }
+
+ val colorScheme = MaterialTheme.colorScheme
+ val axisColor = colorScheme.outline
+ val specColor = colorScheme.tertiary
+ val valueColor = colorScheme.primary
+
+ val primarySpec = motionValue.spec.get(inspector.frame.gestureDirection)
+ val activeSegment = inspector.frame.segmentKey
+
Spacer(
modifier =
modifier
- .debugMotionSpecGraph(spec, inputRange, outputRange)
- .debugMotionValueGraph(motionValue, color, inputRange, outputRange, historySize)
+ .debugMotionSpecGraph(
+ primarySpec,
+ inputRange,
+ outputRange,
+ axisColor,
+ specColor,
+ activeSegment,
+ )
+ .debugMotionValueGraph(
+ motionValue,
+ valueColor,
+ inputRange,
+ outputRange,
+ maxAgeMillis,
+ )
)
}
@@ -83,17 +118,15 @@
* @param outputRange The range of the output (y) axis.
*/
fun Modifier.debugMotionSpecGraph(
- spec: MotionSpec,
+ spec: DirectionalMotionSpec,
inputRange: ClosedFloatingPointRange<Float>,
outputRange: ClosedFloatingPointRange<Float>,
+ axisColor: Color = Color.Gray,
+ specColor: Color = Color.Blue,
+ activeSegment: SegmentKey? = null,
): Modifier = drawBehind {
- drawAxis(Color.Gray)
- if (spec.isUnidirectional) {
- drawDirectionalSpec(spec.maxDirection, inputRange, outputRange, Color.Red)
- } else {
- drawDirectionalSpec(spec.minDirection, inputRange, outputRange, Color.Red)
- drawDirectionalSpec(spec.maxDirection, inputRange, outputRange, Color.Blue)
- }
+ drawAxis(axisColor)
+ drawDirectionalSpec(spec, inputRange, outputRange, specColor, activeSegment)
}
/**
@@ -107,7 +140,7 @@
* @param color Color for the dots indicating the value
* @param inputRange The range of the input (x) axis
* @param outputRange The range of the output (y) axis.
- * @param historySize Number of past values to draw as a trail.
+ * @param maxAgeMillis Max age of the elements in the history trail.
*/
@Composable
fun Modifier.debugMotionValueGraph(
@@ -115,9 +148,10 @@
color: Color,
inputRange: ClosedFloatingPointRange<Float>,
outputRange: ClosedFloatingPointRange<Float>,
- historySize: Int = 100,
+ maxAgeMillis: Long = 1000L,
): Modifier =
- this then DebugMotionValueGraphElement(motionValue, color, inputRange, outputRange, historySize)
+ this then
+ DebugMotionValueGraphElement(motionValue, color, inputRange, outputRange, maxAgeMillis)
/**
* Utility to compute the min/max output values of the spec for the given input.
@@ -176,22 +210,22 @@
val color: Color,
val inputRange: ClosedFloatingPointRange<Float>,
val outputRange: ClosedFloatingPointRange<Float>,
- val historySize: Int,
+ val maxAgeMillis: Long,
) : ModifierNodeElement<DebugMotionValueGraphNode>() {
init {
- require(historySize > 0)
+ require(maxAgeMillis > 0)
}
override fun create() =
- DebugMotionValueGraphNode(motionValue, color, inputRange, outputRange, historySize)
+ DebugMotionValueGraphNode(motionValue, color, inputRange, outputRange, maxAgeMillis)
override fun update(node: DebugMotionValueGraphNode) {
node.motionValue = motionValue
node.color = color
node.inputRange = inputRange
node.outputRange = outputRange
- node.historySize = historySize
+ node.maxAgeMillis = maxAgeMillis
}
override fun InspectorInfo.inspectableProperties() {
@@ -204,21 +238,12 @@
var color: Color,
var inputRange: ClosedFloatingPointRange<Float>,
var outputRange: ClosedFloatingPointRange<Float>,
- historySize: Int,
+ var maxAgeMillis: Long,
) : DrawModifierNode, ObserverModifierNode, Modifier.Node() {
private var debugInspector by mutableStateOf<DebugInspector?>(null)
private val history = mutableStateListOf<FrameData>()
- var historySize = historySize
- set(value) {
- field = value
-
- if (history.size > value) {
- history.removeRange(0, value - historySize)
- }
- }
-
var motionValue = motionValue
set(value) {
if (value != field) {
@@ -233,6 +258,25 @@
override fun onAttach() {
acquireDebugInspector()
+
+ coroutineScope.launch {
+ while (true) {
+ if (history.size > 1) {
+
+ withFrameNanos { thisFrameTime ->
+ while (
+ history.size > 1 &&
+ (thisFrameTime - history.first().frameTimeNanos) >
+ maxAgeMillis * 1_000_000
+ ) {
+ history.removeFirst()
+ }
+ }
+ }
+
+ snapshotFlow { history.size > 1 }.first { it }
+ }
+ }
}
override fun onDetach() {
@@ -251,6 +295,9 @@
}
override fun ContentDrawScope.draw() {
+ if (history.isNotEmpty()) {
+ drawDirectionAndAnimationStatus(history.last())
+ }
drawInputOutputTrail(history, inputRange, outputRange, color)
drawContent()
}
@@ -260,12 +307,7 @@
observeReads { lastFrame = debugInspector?.frame }
- lastFrame?.also {
- history.add(it)
- if (history.size > historySize) {
- history.removeFirst()
- }
- }
+ lastFrame?.also { history.add(it) }
}
override fun onObservedReadsChanged() {
@@ -297,12 +339,16 @@
inputRange: ClosedFloatingPointRange<Float>,
outputRange: ClosedFloatingPointRange<Float>,
color: Color,
+ activeSegment: SegmentKey?,
) {
val startSegment = spec.findBreakpointIndex(inputRange.start)
val endSegment = spec.findBreakpointIndex(inputRange.endInclusive)
for (segmentIndex in startSegment..endSegment) {
+ val isActiveSegment =
+ activeSegment?.let { spec.findSegmentIndex(it) == segmentIndex } ?: false
+
val mapping = spec.mappings[segmentIndex]
val startBreakpoint = spec.breakpoints[segmentIndex]
val segmentStart = startBreakpoint.position
@@ -317,14 +363,18 @@
val start = Offset(mapPointInInputToX(fromInput, inputRange), fromY)
val end = Offset(mapPointInInputToX(toInput, inputRange), toY)
- drawLine(color, start, end)
+
+ val strokeWidth = if (isActiveSegment) 2.dp.toPx() else Stroke.HairlineWidth
+ val dotSize = if (isActiveSegment) 4.dp.toPx() else 2.dp.toPx()
+
+ drawLine(color, start, end, strokeWidth = strokeWidth)
if (segmentStart == fromInput) {
- drawCircle(color, 2.dp.toPx(), start)
+ drawCircle(color, dotSize, start)
}
if (segmentEnd == toInput) {
- drawCircle(color, 2.dp.toPx(), end)
+ drawCircle(color, dotSize, end)
}
val guarantee = startBreakpoint.guarantee
@@ -351,6 +401,53 @@
}
}
+private fun DrawScope.drawDirectionAndAnimationStatus(currentFrame: FrameData) {
+ val indicatorSize = min(this.size.height, 24.dp.toPx())
+
+ this.scale(
+ scaleX = if (currentFrame.gestureDirection == InputDirection.Max) 1f else -1f,
+ scaleY = 1f,
+ ) {
+ val color = if (currentFrame.isStable) Color.Green else Color.Red
+ val strokeWidth = 1.dp.toPx()
+ val d1 = indicatorSize / 2f
+ val d2 = indicatorSize / 3f
+
+ translate(left = 2.dp.toPx()) {
+ drawLine(
+ color,
+ Offset(center.x - d2, center.y - d1),
+ center,
+ strokeWidth = strokeWidth,
+ cap = StrokeCap.Round,
+ )
+ drawLine(
+ color,
+ Offset(center.x - d2, center.y + d1),
+ center,
+ strokeWidth = strokeWidth,
+ cap = StrokeCap.Round,
+ )
+ }
+ translate(left = -2.dp.toPx()) {
+ drawLine(
+ color,
+ Offset(center.x - d2, center.y - d1),
+ center,
+ strokeWidth = strokeWidth,
+ cap = StrokeCap.Round,
+ )
+ drawLine(
+ color,
+ Offset(center.x - d2, center.y + d1),
+ center,
+ strokeWidth = strokeWidth,
+ cap = StrokeCap.Round,
+ )
+ }
+ }
+}
+
private fun DrawScope.drawInputOutputTrail(
history: List<FrameData>,
inputRange: ClosedFloatingPointRange<Float>,
diff --git a/mechanics/tests/Android.bp b/mechanics/tests/Android.bp
index b064017..8fdf904 100644
--- a/mechanics/tests/Android.bp
+++ b/mechanics/tests/Android.bp
@@ -33,6 +33,7 @@
static_libs: [
// ":mechanics" dependencies
"androidx.compose.runtime_runtime",
+ "androidx.compose.material3_material3",
"androidx.compose.ui_ui-util",
"androidx.compose.foundation_foundation-layout",