1 /*
2  * Copyright (C) 2022 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 androidx.constraintlayout.compose
18 
19 import androidx.compose.ui.graphics.Color
20 import androidx.compose.ui.graphics.toArgb
21 import androidx.compose.ui.unit.Dp
22 import androidx.compose.ui.unit.TextUnit
23 import androidx.constraintlayout.core.parser.CLObject
24 
25 private const val UNDEFINED_NAME_PREFIX = "androidx.constraintlayout"
26 
27 /**
28  * Returns a [MotionScene] instance defined by [motionSceneContent].
29  *
30  * @see MotionSceneScope
31  * @see TransitionScope
32  * @see ConstraintSetScope
33  */
34 @ExperimentalMotionApi
MotionScenenull35 fun MotionScene(motionSceneContent: MotionSceneScope.() -> Unit): MotionScene {
36     val scope = MotionSceneScope().apply(motionSceneContent)
37     return MotionSceneDslImpl(
38         constraintSetsByName = scope.constraintSetsByName,
39         transitionsByName = scope.transitionsByName
40     )
41 }
42 
43 @ExperimentalMotionApi
44 internal class MotionSceneDslImpl(
45     private val constraintSetsByName: Map<String, ConstraintSet>,
46     private val transitionsByName: Map<String, Transition>
47 ) : MotionScene {
setTransitionContentnull48     override fun setTransitionContent(elementName: String?, toJSON: String?) {
49         // Do Nothing
50     }
51 
getConstraintSetnull52     override fun getConstraintSet(ext: String?): String {
53         // Do nothing
54         return ""
55     }
56 
setConstraintSetContentnull57     override fun setConstraintSetContent(csName: String?, toJSON: String?) {
58         // Do nothing
59         // TODO: Consider overwriting ConstraintSet instance
60     }
61 
setDebugNamenull62     override fun setDebugName(name: String?) {
63         // Do nothing
64     }
65 
getTransitionnull66     override fun getTransition(str: String?): String {
67         // Do nothing
68         return ""
69     }
70 
getConstraintSetnull71     override fun getConstraintSet(index: Int): String {
72         // Do nothing
73         return ""
74     }
75 
getConstraintSetInstancenull76     override fun getConstraintSetInstance(name: String): ConstraintSet? {
77         return constraintSetsByName[name]
78     }
79 
getTransitionInstancenull80     override fun getTransitionInstance(name: String): Transition? {
81         return transitionsByName[name]
82     }
83 
equalsnull84     override fun equals(other: Any?): Boolean {
85         if (this === other) return true
86         if (javaClass != other?.javaClass) return false
87 
88         other as MotionSceneDslImpl
89 
90         if (constraintSetsByName != other.constraintSetsByName) return false
91         if (transitionsByName != other.transitionsByName) return false
92 
93         return true
94     }
95 
hashCodenull96     override fun hashCode(): Int {
97         var result = constraintSetsByName.hashCode()
98         result = 31 * result + transitionsByName.hashCode()
99         return result
100     }
101 }
102 
103 /**
104  * Scope used by the MotionScene DSL.
105  *
106  * Define new [ConstraintSet]s and [Transition]s within this scope using [constraintSet] and
107  * [transition] respectively.
108  *
109  * Alternatively, you may add existing objects to this scope using [addConstraintSet] and
110  * [addTransition].
111  *
112  * The [defaultTransition] **should always be set**. It defines the initial state of the layout and
113  * works as a fallback for undefined `from -> to` transitions.
114  */
115 @ExperimentalMotionApi
116 class MotionSceneScope internal constructor() {
117     /** Count of generated ConstraintSet & Transition names. */
118     private var generatedCount = 0
119 
120     /** Count of generated ConstraintLayoutReference IDs. */
121     private var generatedIdCount = 0
122 
123     /**
124      * Returns a new unique name. Should be used when the user does not provide a specific name for
125      * their ConstraintSets/Transitions.
126      */
nextNamenull127     private fun nextName() = UNDEFINED_NAME_PREFIX + generatedCount++
128 
129     private fun nextId() = UNDEFINED_NAME_PREFIX + "id${generatedIdCount++}"
130 
131     internal var constraintSetsByName = HashMap<String, ConstraintSet>()
132     internal var transitionsByName = HashMap<String, Transition>()
133 
134     internal fun reset() {
135         generatedCount = 0
136         constraintSetsByName.clear()
137         transitionsByName.clear()
138     }
139 
140     /**
141      * Defines the default [Transition], where the [from] and [to] [ConstraintSet]s will be the
142      * initial start and end states of the layout.
143      *
144      * The default [Transition] will also be applied when the combination of `from` and `to`
145      * ConstraintSets was not defined by a [transition] call.
146      *
147      * This [Transition] is required to initialize [MotionLayout].
148      */
defaultTransitionnull149     fun defaultTransition(
150         from: ConstraintSetRef,
151         to: ConstraintSetRef,
152         transitionContent: TransitionScope.() -> Unit = {}
153     ) {
154         transition(from, to, "default", transitionContent)
155     }
156 
157     /**
158      * Creates a [ConstraintSet] that extends the changes applied by [extendConstraintSet] (if not
159      * null).
160      *
161      * A [name] may be provided and it can be used on MotionLayout calls that request a
162      * ConstraintSet name.
163      *
164      * Returns a [ConstraintSetRef] object representing this ConstraintSet, which may be used as a
165      * parameter of [transition].
166      */
constraintSetnull167     fun constraintSet(
168         name: String? = null,
169         extendConstraintSet: ConstraintSetRef? = null,
170         constraintSetContent: ConstraintSetScope.() -> Unit
171     ): ConstraintSetRef {
172         return addConstraintSet(
173             constraintSet =
174                 DslConstraintSet(
175                     description = constraintSetContent,
176                     extendFrom = extendConstraintSet?.let { constraintSetsByName[it.name] }
177                 ),
178             name = name
179         )
180     }
181 
182     /**
183      * Adds a [Transition] defined by [transitionContent]. A [name] may be provided and it can be
184      * used on MotionLayout calls that request a Transition name.
185      *
186      * Where [from] and [to] are the ConstraintSets handled by it.
187      */
transitionnull188     fun transition(
189         from: ConstraintSetRef,
190         to: ConstraintSetRef,
191         name: String? = null,
192         transitionContent: TransitionScope.() -> Unit
193     ) {
194         val transitionName = name ?: nextName()
195         transitionsByName[transitionName] =
196             TransitionImpl(
197                 parsedTransition =
198                     TransitionScope(from = from.name, to = to.name)
199                         .apply(transitionContent)
200                         .getObject()
201             )
202     }
203 
204     /**
205      * Adds an existing [ConstraintSet] object to the scope of this MotionScene. A [name] may be
206      * provided and it can be used on MotionLayout calls that request a ConstraintSet name.
207      *
208      * Returns a [ConstraintSetRef] object representing the added [constraintSet], which may be used
209      * as a parameter of [transition].
210      */
addConstraintSetnull211     fun addConstraintSet(constraintSet: ConstraintSet, name: String? = null): ConstraintSetRef {
212         val cSetName = name ?: nextName()
213         constraintSetsByName[cSetName] = constraintSet
214         return ConstraintSetRef(cSetName)
215     }
216 
217     /**
218      * Adds an existing [Transition] object to the scope of this MotionScene. A [name] may be
219      * provided and it can be used on MotionLayout calls that request a Transition name.
220      *
221      * The [ConstraintSet]s referenced by the transition must match the name of a [ConstraintSet]
222      * added within this scope.
223      *
224      * @see [constraintSet]
225      * @see [addConstraintSet]
226      */
addTransitionnull227     fun addTransition(transition: Transition, name: String? = null) {
228         val transitionName = name ?: nextName()
229         transitionsByName[transitionName] = transition
230     }
231 
232     /**
233      * Creates one [ConstrainedLayoutReference] corresponding to the [ConstraintLayout] element with
234      * [id].
235      */
createRefFornull236     fun createRefFor(id: Any): ConstrainedLayoutReference = ConstrainedLayoutReference(id)
237 
238     /**
239      * Convenient way to create multiple [ConstrainedLayoutReference] with one statement, the [ids]
240      * provided should match Composables within ConstraintLayout using
241      * [androidx.compose.ui.Modifier.layoutId].
242      *
243      * Example:
244      * ```
245      * val (box, text, button) = createRefsFor("box", "text", "button")
246      * ```
247      *
248      * Note that the number of ids should match the number of variables assigned.
249      *
250      * To create a singular [ConstrainedLayoutReference] see [createRefFor].
251      */
252     fun createRefsFor(vararg ids: Any): ConstrainedLayoutReferences =
253         ConstrainedLayoutReferences(arrayOf(*ids))
254 
255     inner class ConstrainedLayoutReferences internal constructor(private val ids: Array<Any>) {
256         operator fun component1(): ConstrainedLayoutReference =
257             ConstrainedLayoutReference(ids.getOrElse(0) { nextId() })
258 
259         operator fun component2(): ConstrainedLayoutReference =
260             createRefFor(ids.getOrElse(1) { nextId() })
261 
262         operator fun component3(): ConstrainedLayoutReference =
263             createRefFor(ids.getOrElse(2) { nextId() })
264 
265         operator fun component4(): ConstrainedLayoutReference =
266             createRefFor(ids.getOrElse(3) { nextId() })
267 
268         operator fun component5(): ConstrainedLayoutReference =
269             createRefFor(ids.getOrElse(4) { nextId() })
270 
271         operator fun component6(): ConstrainedLayoutReference =
272             createRefFor(ids.getOrElse(5) { nextId() })
273 
274         operator fun component7(): ConstrainedLayoutReference =
275             createRefFor(ids.getOrElse(6) { nextId() })
276 
277         operator fun component8(): ConstrainedLayoutReference =
278             createRefFor(ids.getOrElse(7) { nextId() })
279 
280         operator fun component9(): ConstrainedLayoutReference =
281             createRefFor(ids.getOrElse(8) { nextId() })
282 
283         operator fun component10(): ConstrainedLayoutReference =
284             createRefFor(ids.getOrElse(9) { nextId() })
285 
286         operator fun component11(): ConstrainedLayoutReference =
287             createRefFor(ids.getOrElse(10) { nextId() })
288 
289         operator fun component12(): ConstrainedLayoutReference =
290             createRefFor(ids.getOrElse(11) { nextId() })
291 
292         operator fun component13(): ConstrainedLayoutReference =
293             createRefFor(ids.getOrElse(12) { nextId() })
294 
295         operator fun component14(): ConstrainedLayoutReference =
296             createRefFor(ids.getOrElse(13) { nextId() })
297 
298         operator fun component15(): ConstrainedLayoutReference =
299             createRefFor(ids.getOrElse(14) { nextId() })
300 
301         operator fun component16(): ConstrainedLayoutReference =
302             createRefFor(ids.getOrElse(15) { nextId() })
303     }
304 
305     /** Declare a custom Float [value] addressed by [name]. */
customFloatnull306     fun ConstrainScope.customFloat(name: String, value: Float) {
307         if (!containerObject.has("custom")) {
308             containerObject.put("custom", CLObject(charArrayOf()))
309         }
310         val customPropsObject = containerObject.getObjectOrNull("custom") ?: return
311         customPropsObject.putNumber(name, value)
312     }
313 
314     /** Declare a custom Color [value] addressed by [name]. */
customColornull315     fun ConstrainScope.customColor(name: String, value: Color) {
316         if (!containerObject.has("custom")) {
317             containerObject.put("custom", CLObject(charArrayOf()))
318         }
319         val customPropsObject = containerObject.getObjectOrNull("custom") ?: return
320         customPropsObject.putString(name, value.toJsonHexString())
321     }
322 
323     /** Declare a custom Int [value] addressed by [name]. */
customIntnull324     fun ConstrainScope.customInt(name: String, value: Int) {
325         customFloat(name, value.toFloat())
326     }
327 
328     /** Declare a custom Dp [value] addressed by [name]. */
customDistancenull329     fun ConstrainScope.customDistance(name: String, value: Dp) {
330         customFloat(name, value.value)
331     }
332 
333     /** Declare a custom TextUnit [value] addressed by [name]. */
customFontSizenull334     fun ConstrainScope.customFontSize(name: String, value: TextUnit) {
335         customFloat(name, value.value)
336     }
337 
338     /**
339      * Custom staggered weight. When set, MotionLayout will use these values instead of the default
340      * way of calculating the weight, ignoring those with a `Float.NaN` value.
341      *
342      * The value is `Float.NaN` by default. Note that when all widgets are set to `Float.NaN`,
343      * MotionLayout will use the default way of calculating the weight.
344      *
345      * @see TransitionScope.maxStaggerDelay
346      */
347     var ConstrainScope.staggeredWeight: Float
348         get() {
349             if (!this.containerObject.has("motion")) {
350                 return Float.NaN
351             }
352             val motionObject = this.containerObject.getObject("motion")
353             return motionObject.getFloatOrNaN("stagger")
354         }
355         set(value) {
<lambda>null356             with(this) { setMotionProperty("stagger", value) }
357         }
358 
setMotionPropertynull359     private fun ConstrainScope.setMotionProperty(name: String, value: Float) {
360         if (!this.containerObject.has("motion")) {
361             containerObject.put("motion", CLObject(charArrayOf()))
362         }
363         val motionPropsObject = containerObject.getObjectOrNull("motion") ?: return
364         motionPropsObject.putNumber(name, value)
365     }
366 
367     /** Sets the custom Float [value] at the frame of the current [KeyAttributeScope]. */
customFloatnull368     fun KeyAttributeScope.customFloat(name: String, value: Float) {
369         customPropertiesValue[name] = value
370     }
371 
372     /** Sets the custom Color [value] at the frame of the current [KeyAttributeScope]. */
customColornull373     fun KeyAttributeScope.customColor(name: String, value: Color) {
374         // Colors must be in the following format: "#AARRGGBB"
375         customPropertiesValue[name] = value.toJsonHexString()
376     }
377 
378     /** Sets the custom Int [value] at the frame of the current [KeyAttributeScope]. */
customIntnull379     fun KeyAttributeScope.customInt(name: String, value: Int) {
380         customPropertiesValue[name] = value
381     }
382 
383     /** Sets the custom Dp [value] at the frame of the current [KeyAttributeScope]. */
KeyAttributeScopenull384     fun KeyAttributeScope.customDistance(name: String, value: Dp) {
385         customPropertiesValue[name] = value.value
386     }
387 
388     /** Sets the custom TextUnit [value] at the frame of the current [KeyAttributeScope]. */
KeyAttributeScopenull389     fun KeyAttributeScope.customFontSize(name: String, value: TextUnit) {
390         customPropertiesValue[name] = value.value
391     }
392 
Colornull393     private fun Color.toJsonHexString(): String = String.format("#%08X", this.toArgb())
394 }
395 
396 @Suppress("DataClassDefinition", "DATA_CLASS_COPY_VISIBILITY_WILL_BE_CHANGED_WARNING")
397 data class ConstraintSetRef internal constructor(internal val name: String)
398