From e58298e64883c44b00e572c03b85b8d327bc5791 Mon Sep 17 00:00:00 2001 From: Nicolas Roard Date: Fri, 11 Jun 2021 14:30:23 -0700 Subject: [PATCH] Add support for custom properties in MotionLayout/Compose - added a MotionLayoutScope with utility functions - supports float/int/color/sp/dp for now - add ScreenExample13 showing custom properties --- .../compose/ConstraintSetParser.kt | 46 ++++++ .../constraintlayout/compose/MotionLayout.kt | 137 +++++++++++++++++- .../core/state/ConstraintReference.java | 22 +++ .../core/state/WidgetFrame.java | 75 +++++++++- .../ComposeConstraintLayout/app/build.gradle | 2 +- .../java/com/example/constraintlayout/test.kt | 63 +++++++- 6 files changed, 325 insertions(+), 20 deletions(-) diff --git a/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/ConstraintSetParser.kt b/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/ConstraintSetParser.kt index 34720becb..70bf0e0f7 100644 --- a/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/ConstraintSetParser.kt +++ b/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/ConstraintSetParser.kt @@ -417,6 +417,9 @@ fun parseWidget( var value = layoutVariables.get(element[constraintName]) reference.rotationZ(value) // element.getDouble(constraintName).toFloat()) } + "custom" -> { + parseCustomProperties(state, layoutVariables, element, reference, constraintName) + } else -> { parseConstraint(state, layoutVariables, element, reference, constraintName) } @@ -424,6 +427,49 @@ fun parseWidget( } } +private fun parseCustomProperties( + state: State, + layoutVariables: LayoutVariables, + element: JSONObject, + reference: ConstraintReference, + constraintName: String +) { + var json = element.optJSONObject(constraintName) + if (json == null) { + return + } + val properties = json.names() ?: return + (0 until properties.length()).forEach { i -> + val property = properties[i].toString() + val value = json[property] + if (value is Int) { + reference.addCustomFloat(property, value.toFloat()) + } else if (value is Float) { + reference.addCustomFloat(property, value) + } else if (value is String) { + if (value.startsWith('#')) { + var r = 0f + var g = 0f + var b = 0f + var a = 1f + if (value.length == 7 || value.length == 9) { + var hr = Integer.valueOf(value.substring(1, 3), 16) + var hg = Integer.valueOf(value.substring(3, 5), 16) + var hb = Integer.valueOf(value.substring(5, 7), 16) + r = hr.toFloat() / 255f + g = hg.toFloat() / 255f + b = hb.toFloat() / 255f + } + if (value.length == 9) { + var ha = Integer.valueOf(value.substring(5, 7), 16) + a = ha.toFloat() / 255f + } + reference.addCustomColor(property, r, g, b, a) + } + } + } +} + private fun parseConstraint( state: State, layoutVariables: LayoutVariables, diff --git a/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/MotionLayout.kt b/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/MotionLayout.kt index 04676056f..1c6a51d3c 100644 --- a/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/MotionLayout.kt +++ b/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/MotionLayout.kt @@ -20,6 +20,7 @@ import android.graphics.Matrix import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.LayoutScopeMarker import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset @@ -35,9 +36,7 @@ import androidx.compose.ui.layout.MeasureScope import androidx.compose.ui.layout.MultiMeasureLayout import androidx.compose.ui.layout.layoutId import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.unit.Constraints -import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.* import androidx.constraintlayout.core.state.Dimension import androidx.constraintlayout.core.state.WidgetFrame import androidx.constraintlayout.core.widgets.Optimizer @@ -57,9 +56,10 @@ inline fun MotionLayout( debug: EnumSet = EnumSet.of(MotionLayoutDebugFlags.NONE), modifier: Modifier = Modifier, optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD, - noinline content: @Composable () -> Unit + crossinline content: @Composable MotionLayoutScope.() -> Unit ) { val measurer = remember { MotionMeasurer() } + val scope = remember { MotionLayoutScope(measurer) } val progressState = remember { mutableStateOf(0f) } SideEffect { progressState.value = progress } val measurePolicy = @@ -70,7 +70,7 @@ inline fun MotionLayout( (MultiMeasureLayout( modifier = modifier.semantics { designInfoProvider = measurer }, measurePolicy = measurePolicy, - content = content + content = { scope.content() } )) with(measurer) { drawDebug() @@ -81,11 +81,78 @@ inline fun MotionLayout( (MultiMeasureLayout( modifier = modifier.semantics { designInfoProvider = measurer }, measurePolicy = measurePolicy, - content = content + content = { scope.content() } )) } } +@LayoutScopeMarker +class MotionLayoutScope @PublishedApi internal constructor(measurer: MotionMeasurer) { + private var myMeasurer = measurer + + class MotionProperties internal constructor(id: String, tag: String?, measurer: MotionMeasurer) { + private var myId = id + private var myTag = null + private var myMeasurer = measurer + + fun id() : String { + return myId + } + + fun tag() : String? { + return myTag + } + + fun color(name: String) : Color { + return myMeasurer.getCustomColor(myId, name) + } + + fun float(name: String) : Float { + return myMeasurer.getCustomFloat(myId, name) + } + + fun int(name: String): Int { + return myMeasurer.getCustomFloat(myId, name).toInt() + } + + fun distance(name: String): Dp { + return myMeasurer.getCustomFloat(myId, name).dp + } + + fun fontSize(name: String) : TextUnit { + return myMeasurer.getCustomFloat(myId, name).sp + } + } + + fun motionProperties(id: String): MotionProperties { + return MotionProperties(id, null, myMeasurer) + } + + fun motionProperties(id: String, tag: String): MotionProperties{ + return MotionProperties(id, tag, myMeasurer) + } + + fun motionColor(id: String, name: String): Color { + return myMeasurer.getCustomColor(id, name) + } + + fun motionFloat(id: String, name: String): Float { + return myMeasurer.getCustomFloat(id, name) + } + + fun motionInt(id: String, name: String): Int { + return myMeasurer.getCustomFloat(id, name).toInt() + } + + fun motionDistance(id: String, name: String): Dp { + return myMeasurer.getCustomFloat(id, name).dp + } + + fun motionFontSize(id: String, name: String): TextUnit { + return myMeasurer.getCustomFloat(id, name).sp + } +} + enum class MotionLayoutDebugFlags { NONE, SHOW_ALL @@ -126,6 +193,8 @@ internal class MotionMeasurer : Measurer() { var framesStart = ArrayList() var framesEnd = ArrayList() + fun getProgress() : Float { return motionProgress } + private fun measureConstraintSet(optimizationLevel: Int, constraintSetStart: ConstraintSet, measurables: List, constraints: Constraints ) { @@ -329,6 +398,62 @@ internal class MotionMeasurer : Measurer() { fun clear() { frameCache.clear() } + + private fun interpolateColor(start: WidgetFrame.Color, end: WidgetFrame.Color, progress: Float) : Color { + if (progress < 0) { + return Color(start.r, start.g, start.b, start.a) + } + if (progress > 1) { + return Color(end.r, end.g, end.b, end.a) + } + val r = (1f - progress) * start.r + progress * (end.r) + val g = (1f - progress) * start.g + progress * (end.g) + val b = (1f - progress) * start.b + progress * (end.b) + return Color(r, g, b) + } + + fun findChild(id: String) : Int { + if (root.children.size == 0) { + return -1 + } + val ref = state.constraints(id) + val cw = ref.constraintWidget + var index = 0; + for (child in root.children) { + if (cw == child) { + return index + } + index++ + } + return -1 + } + + fun getCustomColor(id: String, name: String): Color { + val index = findChild(id) + if (index == -1) { + return Color.Black + } + val startFrame = framesStart[index] + val endFrame = framesEnd[index] + val startColor = startFrame.getCustomColor(name) + val endColor = endFrame.getCustomColor(name) + if (startColor != null && endColor != null) { + return interpolateColor(startColor, endColor, motionProgress) + } + return Color.Black + } + + fun getCustomFloat(id: String, name: String): Float { + val index = findChild(id) + if (index == -1) { + return 0f; + } + val startFrame = framesStart[index] + val endFrame = framesEnd[index] + val startFloat = startFrame.getCustomFloat(name) + val endFloat = endFrame.getCustomFloat(name) + return (1f - motionProgress) * startFloat + motionProgress * endFloat + } } private val DEBUG = false \ No newline at end of file diff --git a/constraintlayout/core/src/main/java/androidx/constraintlayout/core/state/ConstraintReference.java b/constraintlayout/core/src/main/java/androidx/constraintlayout/core/state/ConstraintReference.java index 1065f8f04..2851b3f5a 100644 --- a/constraintlayout/core/src/main/java/androidx/constraintlayout/core/state/ConstraintReference.java +++ b/constraintlayout/core/src/main/java/androidx/constraintlayout/core/state/ConstraintReference.java @@ -21,6 +21,7 @@ import androidx.constraintlayout.core.widgets.ConstraintWidget; import java.util.ArrayList; +import java.util.HashMap; import static androidx.constraintlayout.core.widgets.ConstraintWidget.HORIZONTAL; import static androidx.constraintlayout.core.widgets.ConstraintWidget.VERTICAL; @@ -113,6 +114,9 @@ public interface ConstraintReferenceFactory { private Object mView; private ConstraintWidget mConstraintWidget; + private HashMap mCustomColors = null; + private HashMap mCustomFloats = null; + public void setView(Object view) { mView = view; if (mConstraintWidget != null) { @@ -355,6 +359,21 @@ public ConstraintReference baseline() { return this; } + public void addCustomColor(String name, float r, float g, float b, float a) { + WidgetFrame.Color color = new WidgetFrame.Color(r, g, b, a); + if (mCustomColors == null) { + mCustomColors = new HashMap<>(); + } + mCustomColors.put(name, color); + } + + public void addCustomFloat(String name, float value) { + if (mCustomFloats == null) { + mCustomFloats = new HashMap<>(); + } + mCustomFloats.put(name, value); + } + private void dereference() { mLeftToLeft = get(mLeftToLeft); mLeftToRight = get(mLeftToRight); @@ -835,5 +854,8 @@ public void apply() { mConstraintWidget.frame.scaleX = mScaleX; mConstraintWidget.frame.scaleY = mScaleY; mConstraintWidget.frame.alpha = mAlpha; + + mConstraintWidget.frame.mCustomFloats = mCustomFloats; + mConstraintWidget.frame.mCustomColors = mCustomColors; } } diff --git a/constraintlayout/core/src/main/java/androidx/constraintlayout/core/state/WidgetFrame.java b/constraintlayout/core/src/main/java/androidx/constraintlayout/core/state/WidgetFrame.java index 52dd1d346..b94dc64d2 100644 --- a/constraintlayout/core/src/main/java/androidx/constraintlayout/core/state/WidgetFrame.java +++ b/constraintlayout/core/src/main/java/androidx/constraintlayout/core/state/WidgetFrame.java @@ -16,8 +16,12 @@ package androidx.constraintlayout.core.state; +import androidx.constraintlayout.core.ArrayRow; import androidx.constraintlayout.core.widgets.ConstraintWidget; +import java.util.HashMap; +import java.util.Map; + /** * Utility class to encapsulate layout of a widget */ @@ -42,6 +46,24 @@ public class WidgetFrame { public float alpha = Float.NaN; + public HashMap mCustomColors = null; + public HashMap mCustomFloats = null; + + public static class Color { + public float r; + public float g; + public float b; + public float a; + + public Color(float r, float g, float b, float a) { + this.r = r; + this.g = g; + this.b = b; + this.a = a; + } + } + + public int width() { return right - left; } public int height() { return bottom - top; } @@ -63,17 +85,25 @@ public WidgetFrame(WidgetFrame frame) { scaleX = frame.scaleX; scaleY = frame.scaleY; alpha = frame.alpha; + if (frame.mCustomColors != null) { + mCustomColors = new HashMap<>(); + mCustomColors.putAll(frame.mCustomColors); + } + if (frame.mCustomFloats != null) { + mCustomFloats = new HashMap<>(); + mCustomFloats.putAll(frame.mCustomFloats); + } } public boolean isDefaultTransform() { - return rotationX == Float.NaN - && rotationY == Float.NaN - && rotationZ == Float.NaN - && translationX == Float.NaN - && translationY == Float.NaN - && scaleX == Float.NaN - && scaleY == Float.NaN - && alpha == Float.NaN; + return Float.isNaN(rotationX) + && Float.isNaN(rotationY) + && Float.isNaN(rotationZ) + && Float.isNaN(translationX) + && Float.isNaN(translationY) + && Float.isNaN(scaleX) + && Float.isNaN(scaleY) + && Float.isNaN(alpha); } public static void interpolate(WidgetFrame frame, WidgetFrame start, WidgetFrame end, float progress) { @@ -128,4 +158,33 @@ public WidgetFrame update() { } return this; } + + public void addCustomColor(String name, float r, float g, float b, float a) { + Color color = new Color(r, g, b, a); + if (mCustomColors == null) { + mCustomColors = new HashMap<>(); + } + mCustomColors.put(name, color); + } + + public Color getCustomColor(String name) { + if (mCustomColors == null) { + return null; + } + return mCustomColors.get(name); + } + + public void addCustomFloat(String name, float value) { + if (mCustomFloats == null) { + mCustomFloats = new HashMap<>(); + } + mCustomFloats.put(name, value); + } + + public float getCustomFloat(String name) { + if (mCustomFloats == null) { + return 0f; + } + return mCustomFloats.get(name); + } } diff --git a/projects/ComposeConstraintLayout/app/build.gradle b/projects/ComposeConstraintLayout/app/build.gradle index 1909a1606..8fd3a6a2a 100644 --- a/projects/ComposeConstraintLayout/app/build.gradle +++ b/projects/ComposeConstraintLayout/app/build.gradle @@ -11,7 +11,7 @@ android { defaultConfig { applicationId "androidx.constaitnlayout.experiments.template" - minSdkVersion 21 + minSdkVersion 30 targetSdkVersion 30 versionCode 1 versionName "1.0" diff --git a/projects/ComposeConstraintLayout/app/src/main/java/com/example/constraintlayout/test.kt b/projects/ComposeConstraintLayout/app/src/main/java/com/example/constraintlayout/test.kt index 88542f33b..9fc06e90e 100644 --- a/projects/ComposeConstraintLayout/app/src/main/java/com/example/constraintlayout/test.kt +++ b/projects/ComposeConstraintLayout/app/src/main/java/com/example/constraintlayout/test.kt @@ -13,13 +13,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ParentDataModifier import androidx.compose.ui.layout.layoutId -import androidx.compose.ui.platform.InspectorInfo -import androidx.compose.ui.platform.InspectorValueInfo -import androidx.compose.ui.platform.debugInspectorInfo import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Density import com.example.constraintlayout.R import java.util.* @@ -970,6 +965,64 @@ public fun ScreenExample12() { } } +@Preview(group = "motion5") +@Composable +public fun ScreenExample13() { + var animateToEnd by remember { mutableStateOf(false) } + val progress by animateFloatAsState( + targetValue = if (animateToEnd) 1f else 0f, + animationSpec = tween(2000) + ) + Column { + MotionLayout( + modifier = Modifier.fillMaxWidth().height(400.dp), + start = ConstraintSet(""" + { + a: { + start: ['parent', 'start', 16], + bottom: ['parent', 'bottom', 16], + custom: { + background: '#FFFF00', + textColor: '#000000', + textSize: 12 + } + } + } + """ + ), + end = ConstraintSet( + """ + { + a: { + end: ['parent', 'end', 16], + top: ['parent', 'top', 16], + rotationZ: 360, + custom: { + background: '#0000FF', + textColor: '#FFFFFF', + textSize: 36 + } + } + } + """ + ), + debug = EnumSet.of(MotionLayoutDebugFlags.SHOW_ALL), + progress = progress + ) { + var properties = motionProperties("a") + Text(text = "Hello", modifier = Modifier + .layoutId(properties.id()) + .background(properties.color("background")), + color = properties.color("textColor"), + fontSize = properties.fontSize("textSize")) + } + + Button(onClick = { animateToEnd = !animateToEnd }) { + Text(text = "Run") + } + } +} +