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