From b78c522d7c692dc696209d975d88ac8d438a3d7e Mon Sep 17 00:00:00 2001 From: Nicolas Roard Date: Thu, 10 Jun 2021 18:16:24 -0700 Subject: [PATCH] Various improvements to the JSON syntax - add support for variables, generators, lists - add support for macros - add support for variable overrides - added ScreenExample12 --- .../compose/ConstraintLayout.kt | 32 ++- .../compose/ConstraintLayoutTag.kt | 72 +++++++ .../compose/ConstraintSetParser.kt | 192 ++++++++++++++---- .../constraintlayout/compose/MotionLayout.kt | 2 +- .../core/state/ConstraintReference.java | 10 + .../constraintlayout/core/state/State.java | 26 +++ .../java/com/example/constraintlayout/test.kt | 53 ++++- 7 files changed, 338 insertions(+), 49 deletions(-) create mode 100644 constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/ConstraintLayoutTag.kt diff --git a/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/ConstraintLayout.kt b/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/ConstraintLayout.kt index 76a2b7633..0138d0ef4 100644 --- a/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/ConstraintLayout.kt +++ b/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/ConstraintLayout.kt @@ -117,6 +117,11 @@ internal fun rememberConstraintLayoutMeasurePolicy( } } } + + override fun override(name: String, value: Float) : ConstraintSet { + // nothing here yet + return this + } } val layoutSize = measurer.performMeasure( constraints, @@ -1231,14 +1236,32 @@ interface ConstraintSet { * Applies the [ConstraintSet] to a state. */ fun applyTo(state: State, measurables: List) + + fun override(name: String, value: Float) : ConstraintSet } fun ConstraintSet(@Language("json5") content : String) = object : ConstraintSet { + private val overridedVariables = HashMap() + override fun applyTo(state: State, measurables: List) { measurables.forEach { measurable -> - state.map((measurable.layoutId ?: createId()), measurable) + var layoutId = measurable.layoutId ?: measurable.constraintLayoutId ?: createId() + state.map(layoutId, measurable) + var tag = measurable.constraintLayoutTag + if (tag != null && tag is String && layoutId is String) { + state.setTag(layoutId, tag) + } } - parseJSON(content, state) + val layoutVariables = LayoutVariables() + for (name in overridedVariables.keys) { + layoutVariables.putOverride(name, overridedVariables[name]!!) + } + parseJSON(content, state, layoutVariables) + } + + override fun override(name: String, value: Float) : ConstraintSet { + overridedVariables[name] = value + return this } } @@ -1254,6 +1277,11 @@ fun ConstraintSet(description: ConstraintSetScope.() -> Unit) = object : Constra scope.description() scope.applyTo(state) } + + override fun override(name: String, value: Float) : ConstraintSet { + // nothing yet + return this + } } /** diff --git a/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/ConstraintLayoutTag.kt b/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/ConstraintLayoutTag.kt new file mode 100644 index 000000000..94a116556 --- /dev/null +++ b/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/ConstraintLayoutTag.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2021 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 androidx.constraintlayout.compose + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.Measurable +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.unit.Density + +fun Modifier.layoutId(layoutId: String, tag: String) = this.then( + ConstraintLayoutTag( + constraintLayoutId = layoutId, + constraintLayoutTag = tag, + inspectorInfo = debugInspectorInfo { + name = "constraintLayoutId" + value = layoutId + } + ) +) + +@Immutable +private class ConstraintLayoutTag( + override val constraintLayoutTag: String, + override val constraintLayoutId: String, + inspectorInfo: InspectorInfo.() -> Unit +) : ParentDataModifier, ConstraintLayoutTagParentData, InspectorValueInfo(inspectorInfo) { + override fun Density.modifyParentData(parentData: Any?): Any? { + return this@ConstraintLayoutTag + } + + override fun hashCode(): Int = + constraintLayoutTag.hashCode() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + val otherModifier = other as? ConstraintLayoutTag ?: return false + return constraintLayoutTag == otherModifier.constraintLayoutTag + } + + override fun toString(): String = + "ConstraintLayoutTag(id=$constraintLayoutTag)" +} + +interface ConstraintLayoutTagParentData { + val constraintLayoutId: String + val constraintLayoutTag: String +} + +val Measurable.constraintLayoutTag: Any? + get() = (parentData as? ConstraintLayoutTagParentData)?.constraintLayoutTag + +val Measurable.constraintLayoutId: Any? + get() = (parentData as? ConstraintLayoutTagParentData)?.constraintLayoutId \ No newline at end of file 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 39da73bf1..34720becb 100644 --- a/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/ConstraintSetParser.kt +++ b/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/ConstraintSetParser.kt @@ -27,8 +27,85 @@ import org.json.JSONObject internal val PARSER_DEBUG = false -internal fun parseJSON(content: String, state: State) { +class LayoutVariables { val margins = HashMap() + val generators = HashMap() + val arrayIds = HashMap>() + + fun put(elementName: String, element: Int) { + margins[elementName] = element + } + + fun put(elementName: String, start: Float, incrementBy: Float) { + if (generators.containsKey(elementName)) { + if (generators[elementName] is OverrideValue) { + return + } + } + var generator = Generator(start, incrementBy) + generators[elementName] = generator + } + + fun putOverride(elementName: String, value: Float) { + var generator = OverrideValue(value) + generators[elementName] = generator + } + + fun get(elementName: Any): Float { + if (elementName is String) { + if (generators.containsKey(elementName)) { + val value = generators[elementName]!!.value() + return value + } + if (margins.containsKey(elementName)) { + return margins[elementName]!!.toFloat() + } + } else if (elementName is Int) { + return elementName.toFloat() + } else if (elementName is Float) { + return elementName + } + return 0f + } + + fun getList(elementName: String) : ArrayList? { + if (arrayIds.containsKey(elementName)) { + return arrayIds[elementName] + } + return null + } + + fun put(elementName: String, elements: ArrayList) { + arrayIds[elementName] = elements + } + +} +interface GeneratedValue { + fun value() : Float +} + +class Generator(start: Float, incrementBy: Float) : GeneratedValue { + var start : Float = start + var incrementBy: Float = incrementBy + var current : Float = start + var stop = false + + override fun value() : Float { + if (!stop) { + current += incrementBy + } + return current + } +} + +class OverrideValue(value: Float) : GeneratedValue { + var value : Float = value + override fun value() : Float { + return value + } +} + +internal fun parseJSON(content: String, state: State, layoutVariables: LayoutVariables) { val json = JSONObject(content) val elements = json.names() ?: return (0 until elements.length()).forEach { i -> @@ -38,8 +115,9 @@ internal fun parseJSON(content: String, state: State) { System.out.println("element <$elementName = $element> " + element.javaClass) } when (elementName) { - "Variables" -> parseVariables(margins, element) - "Helpers" -> parseHelpers(state, margins, element) + "Variables" -> parseVariables(state, layoutVariables, element) + "Helpers" -> parseHelpers(state, layoutVariables, element) + "Generate" -> parseGenerate(state, layoutVariables, element) else -> { if (element is JSONObject) { var type = lookForType(element) @@ -47,10 +125,10 @@ internal fun parseJSON(content: String, state: State) { when (type) { "hGuideline" -> parseGuidelineParams(ConstraintWidget.HORIZONTAL, state, elementName, element) "vGuideline" -> parseGuidelineParams(ConstraintWidget.VERTICAL, state, elementName, element) - "barrier" -> parseBarrier(state, margins, elementName, element) + "barrier" -> parseBarrier(state, elementName, element) } } else if (type == null) { - parseWidget(state, margins, elementName, element) + parseWidget(state, layoutVariables, elementName, element) } } } @@ -58,7 +136,7 @@ internal fun parseJSON(content: String, state: State) { } } -fun parseVariables(margins: HashMap, json: Any) { +fun parseVariables(state: State, layoutVariables: LayoutVariables, json: Any) { if (!(json is JSONObject)) { return } @@ -67,12 +145,28 @@ fun parseVariables(margins: HashMap, json: Any) { val elementName = elements[i].toString() val element = json[elementName] if (element is Int) { - margins[elementName] = element + layoutVariables.put(elementName, element) + } else if (element is JSONObject) { + if (element.has("start") && element.has("increment")) { + var start = layoutVariables.get(element["start"]) + var increment = layoutVariables.get(element["increment"]) + layoutVariables.put(elementName, start, increment) + } else if (element.has("ids")) { + var ids = element.getJSONArray("ids"); + var arrayIds = arrayListOf() + for (i in 0..ids.length()-1) { + arrayIds.add(ids.getString(i)) + } + layoutVariables.put(elementName, arrayIds) + } else if (element.has("tag")) { + var arrayIds = state.getIdsForTag(element.getString("tag")) + layoutVariables.put(elementName, arrayIds) + } } } } -fun parseHelpers(state: State, margins: HashMap, element: Any) { +fun parseHelpers(state: State, layoutVariables: LayoutVariables, element: Any) { if (!(element is JSONArray)) { return } @@ -80,16 +174,33 @@ fun parseHelpers(state: State, margins: HashMap, element: Any) { val helper = element[i] if (helper is JSONArray && helper.length() > 1) { when (helper[0]) { - "hChain" -> parseChain(ConstraintWidget.HORIZONTAL, state, margins, helper) - "vChain" -> parseChain(ConstraintWidget.VERTICAL, state, margins, helper) - "hGuideline" -> parseGuideline(ConstraintWidget.HORIZONTAL, state, margins, helper) - "vGuideline" -> parseGuideline(ConstraintWidget.VERTICAL, state, margins, helper) + "hChain" -> parseChain(ConstraintWidget.HORIZONTAL, state, layoutVariables, helper) + "vChain" -> parseChain(ConstraintWidget.VERTICAL, state, layoutVariables, helper) + "hGuideline" -> parseGuideline(ConstraintWidget.HORIZONTAL, state, layoutVariables, helper) + "vGuideline" -> parseGuideline(ConstraintWidget.VERTICAL, state, layoutVariables, helper) + } + } + } +} + +fun parseGenerate(state: State, layoutVariables: LayoutVariables, json: Any) { + if (!(json is JSONObject)) { + return + } + val elements = json.names() ?: return + (0 until elements.length()).forEach { i -> + val elementName = elements[i].toString() + val element = json[elementName] + var arrayIds = layoutVariables.getList(elementName) + if (arrayIds != null && element is JSONObject) { + for (id in arrayIds) { + parseWidget(state, layoutVariables, id, element) } } } } -fun parseChain(orientation: Int, state: State, margins: java.util.HashMap, helper: JSONArray) { +fun parseChain(orientation: Int, state: State, margins: LayoutVariables, helper: JSONArray) { var chain = if (orientation == ConstraintWidget.HORIZONTAL) state.horizontalChain() else state.verticalChain() var refs = helper[1] if (!(refs is JSONArray) || refs.length() < 1) { @@ -131,7 +242,7 @@ fun parseChain(orientation: Int, state: State, margins: java.util.HashMap, helper: JSONArray) { +fun parseGuideline(orientation: Int, state: State, margins: LayoutVariables, helper: JSONArray) { var params = helper[1] if (!(params is JSONObject)) { return @@ -187,8 +298,9 @@ private fun parseGuidelineParams( } } -fun parseBarrier(state: State, margins: HashMap, - elementName: String, element: JSONObject) { +fun parseBarrier( + state: State, + elementName: String, element: JSONObject) { val reference = state.barrier(elementName, androidx.constraintlayout.core.state.State.Direction.END) val constraints = element.names() ?: return var barrierReference = reference @@ -223,7 +335,7 @@ fun parseBarrier(state: State, margins: HashMap, fun parseWidget( state: State, - margins: HashMap, + layoutVariables: LayoutVariables, elementName: String, element: JSONObject ) { @@ -273,31 +385,40 @@ fun parseWidget( reference.bottomToBottom(targetReference) } "alpha" -> { - reference.alpha(element.getDouble(constraintName).toFloat()) + var value = layoutVariables.get(element[constraintName]) + reference.alpha(value) +// reference.alpha(element.getDouble(constraintName).toFloat()) } "scaleX" -> { - reference.scaleX(element.getDouble(constraintName).toFloat()) + var value = layoutVariables.get(element[constraintName]) + reference.scaleX(value) //element.getDouble(constraintName).toFloat()) } "scaleY" -> { - reference.scaleY(element.getDouble(constraintName).toFloat()) + var value = layoutVariables.get(element[constraintName]) + reference.scaleY(value) //element.getDouble(constraintName).toFloat()) } "translationX" -> { - reference.translationX(element.getDouble(constraintName).toFloat()) + var value = layoutVariables.get(element[constraintName]) + reference.translationX(value) //element.getDouble(constraintName).toFloat()) } "translationY" -> { - reference.translationY(element.getDouble(constraintName).toFloat()) + var value = layoutVariables.get(element[constraintName]) + reference.translationY(value) //element.getDouble(constraintName).toFloat()) } "rotationX" -> { - reference.rotationX(element.getDouble(constraintName).toFloat()) + var value = layoutVariables.get(element[constraintName]) + reference.rotationX(value) //element.getDouble(constraintName).toFloat()) } "rotationY" -> { - reference.rotationY(element.getDouble(constraintName).toFloat()) + var value = layoutVariables.get(element[constraintName]) + reference.rotationY(value) //element.getDouble(constraintName).toFloat()) } "rotationZ" -> { - reference.rotationZ(element.getDouble(constraintName).toFloat()) + var value = layoutVariables.get(element[constraintName]) + reference.rotationZ(value) // element.getDouble(constraintName).toFloat()) } else -> { - parseConstraint(state, margins, element, reference, constraintName) + parseConstraint(state, layoutVariables, element, reference, constraintName) } } } @@ -305,7 +426,7 @@ fun parseWidget( private fun parseConstraint( state: State, - margins: HashMap, + layoutVariables: LayoutVariables, element: JSONObject, reference: ConstraintReference, constraintName: String @@ -316,14 +437,7 @@ private fun parseConstraint( val anchor = constraint[1] var margin: Int = 0 if (constraint.length() > 2) { - if (constraint[2] is String) { - val resolvedMargin = margins[constraint[2]] - if (resolvedMargin != null) { - margin = resolvedMargin - } - } else if (constraint[2] is Int) { - margin = constraint[2] as Int - } + margin = layoutVariables.get(constraint[2]).toInt() } margin = state.convertDimension(Dp(margin.toFloat())) @@ -334,13 +448,7 @@ private fun parseConstraint( } when (constraintName) { "circular" -> { - var angle = 0f - if (constraint[1] is Float) { - angle = constraint[1] as Float - } - if (constraint[1] is Int) { - angle = (constraint[1] as Int).toFloat() - } + var angle = layoutVariables.get(constraint[1]) reference.circularConstraint(targetReference, angle, 0f) } "start" -> { 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 d2b95e1e0..04676056f 100644 --- a/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/MotionLayout.kt +++ b/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/MotionLayout.kt @@ -99,7 +99,7 @@ internal fun rememberMotionLayoutMeasurePolicy( constraintSetEnd: ConstraintSet, progress: MutableState, measurer: MotionMeasurer -) = remember(optimizationLevel, constraintSetStart, constraintSetEnd, progress) { +) = remember(optimizationLevel, constraintSetStart, constraintSetEnd) { measurer.clear() MeasurePolicy { measurables, constraints -> val layoutSize = measurer.performInterpolationMeasure( 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 31319b482..1065f8f04 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 @@ -37,12 +37,22 @@ public Object getKey() { return key; } + public void setTag(String tag) { + mTag = tag; + } + + public String getTag() { + return mTag; + } + public interface ConstraintReferenceFactory { ConstraintReference create(State state); } final State mState; + String mTag = null; + Facade mFacade = null; int mHorizontalChainStyle = ConstraintWidget.CHAIN_SPREAD; diff --git a/constraintlayout/core/src/main/java/androidx/constraintlayout/core/state/State.java b/constraintlayout/core/src/main/java/androidx/constraintlayout/core/state/State.java index e41a8de7c..94232324f 100644 --- a/constraintlayout/core/src/main/java/androidx/constraintlayout/core/state/State.java +++ b/constraintlayout/core/src/main/java/androidx/constraintlayout/core/state/State.java @@ -26,6 +26,7 @@ import androidx.constraintlayout.core.state.helpers.VerticalChainReference; import androidx.constraintlayout.core.state.helpers.HorizontalChainReference; +import java.util.ArrayList; import java.util.HashMap; /** @@ -35,6 +36,7 @@ public class State { protected HashMap mReferences = new HashMap<>(); protected HashMap mHelperReferences = new HashMap<>(); + HashMap> mTags = new HashMap(); final static int UNKNOWN = -1; final static int CONSTRAINT_SPREAD = 0; @@ -95,6 +97,7 @@ public State() { public void reset() { mHelperReferences.clear(); + mTags.clear(); } /** @@ -276,6 +279,29 @@ public void map(Object key, Object view) { } } + public void setTag(String key, String tag) { + Reference ref = constraints(key); + if (ref instanceof ConstraintReference) { + ConstraintReference reference = (ConstraintReference) ref; + reference.setTag(tag); + ArrayList list = null; + if (!mTags.containsKey(tag)) { + list = new ArrayList(); + mTags.put(tag, list); + } else { + list = mTags.get(tag); + } + list.add(key); + } + } + + public ArrayList getIdsForTag(String tag) { + if (mTags.containsKey(tag)) { + return mTags.get(tag); + } + return null; + } + public void apply(ConstraintWidgetContainer container) { container.removeAllChildren(); mParent.getWidth().apply(this, container, ConstraintWidget.HORIZONTAL); 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 cb123e806..88542f33b 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 @@ -12,11 +12,14 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.material.Text import androidx.compose.runtime.* -import androidx.compose.ui.geometry.Offset 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.constraintlayout.core.widgets.Optimizer +import androidx.compose.ui.unit.Density import com.example.constraintlayout.R import java.util.* @@ -919,11 +922,53 @@ public fun ScreenExample11() { } } +@Preview(group = "motion4") +@Composable +public fun ScreenExample12() { + var animateToEnd by remember { mutableStateOf(false) } + val progress by animateFloatAsState( + targetValue = if (animateToEnd) 1f else 0f, + animationSpec = tween(2000) + ) + var baseConstraintSet = """ + { + Variables: { + angle: { start: 0, increment: 10 }, + rotation: { start: 'startRotation', increment: 10 }, + distance: 100, + mylist: { tag: 'box' } + }, + Generate: { + mylist: { + width: 200, + height: 40, + circular: ['parent', 'angle', 'distance'], + rotationZ: 'rotation' + } + } + } + """ + var cs1 = ConstraintSet(baseConstraintSet).override("startRotation", 0f) + var cs2 = ConstraintSet(baseConstraintSet).override("startRotation", 90f) + Column { + Button(onClick = { animateToEnd = !animateToEnd }) { + Text(text = "Run") + } + MotionLayout(cs1, cs2, + progress = progress, + modifier = Modifier + .fillMaxSize().background(Color.White) + ) { + var colors = arrayListOf(Color.Red, Color.Green, Color.Blue, Color.Cyan, Color.Yellow) - - + for (i in 1..36) { + Box(modifier = Modifier.layoutId("h$i", "box").background(colors[i%colors.size])) + } + } + } +}