| /* |
| * Copyright (C) 2022 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.ui.graphics.Color |
| import androidx.compose.ui.graphics.toArgb |
| import androidx.compose.ui.unit.Dp |
| import androidx.compose.ui.unit.TextUnit |
| import androidx.constraintlayout.core.parser.CLObject |
| |
| private const val UNDEFINED_NAME_PREFIX = "androidx.constraintlayout" |
| |
| /** |
| * Returns a [MotionScene] instance defined by [motionSceneContent]. |
| * |
| * @see MotionSceneScope |
| * @see TransitionScope |
| * @see ConstraintSetScope |
| */ |
| @ExperimentalMotionApi |
| fun MotionScene( |
| motionSceneContent: MotionSceneScope.() -> Unit |
| ): MotionScene { |
| val scope = MotionSceneScope().apply(motionSceneContent) |
| return MotionSceneDslImpl( |
| constraintSetsByName = scope.constraintSetsByName, |
| transitionsByName = scope.transitionsByName |
| ) |
| } |
| |
| @ExperimentalMotionApi |
| internal class MotionSceneDslImpl( |
| private val constraintSetsByName: Map<String, ConstraintSet>, |
| private val transitionsByName: Map<String, Transition> |
| ) : MotionScene { |
| override fun setTransitionContent(elementName: String?, toJSON: String?) { |
| // Do Nothing |
| } |
| |
| override fun getConstraintSet(ext: String?): String { |
| // Do nothing |
| return "" |
| } |
| |
| override fun setConstraintSetContent(csName: String?, toJSON: String?) { |
| // Do nothing |
| // TODO: Consider overwriting ConstraintSet instance |
| } |
| |
| override fun setDebugName(name: String?) { |
| // Do nothing |
| } |
| |
| override fun getTransition(str: String?): String { |
| // Do nothing |
| return "" |
| } |
| |
| override fun getConstraintSet(index: Int): String { |
| // Do nothing |
| return "" |
| } |
| |
| override fun getConstraintSetInstance(name: String): ConstraintSet? { |
| return constraintSetsByName[name] |
| } |
| |
| override fun getTransitionInstance(name: String): Transition? { |
| return transitionsByName[name] |
| } |
| |
| override fun equals(other: Any?): Boolean { |
| if (this === other) return true |
| if (javaClass != other?.javaClass) return false |
| |
| other as MotionSceneDslImpl |
| |
| if (constraintSetsByName != other.constraintSetsByName) return false |
| if (transitionsByName != other.transitionsByName) return false |
| |
| return true |
| } |
| |
| override fun hashCode(): Int { |
| var result = constraintSetsByName.hashCode() |
| result = 31 * result + transitionsByName.hashCode() |
| return result |
| } |
| } |
| |
| /** |
| * Scope used by the MotionScene DSL. |
| * |
| * |
| * |
| * Define new [ConstraintSet]s and [Transition]s within this scope using [constraintSet] and |
| * [transition] respectively. |
| * |
| * Alternatively, you may add existing objects to this scope using [addConstraintSet] and |
| * [addTransition]. |
| * |
| * |
| * |
| * The [defaultTransition] **should always be set**. It defines the initial state of the layout and |
| * works as a fallback for undefined `from -> to` transitions. |
| */ |
| @ExperimentalMotionApi |
| class MotionSceneScope internal constructor() { |
| /** |
| * Count of generated ConstraintSet & Transition names. |
| */ |
| private var generatedCount = 0 |
| |
| /** |
| * Count of generated ConstraintLayoutReference IDs. |
| */ |
| private var generatedIdCount = 0 |
| |
| /** |
| * Returns a new unique name. Should be used when the user does not provide a specific name |
| * for their ConstraintSets/Transitions. |
| */ |
| private fun nextName() = UNDEFINED_NAME_PREFIX + generatedCount++ |
| |
| private fun nextId() = UNDEFINED_NAME_PREFIX + "id${generatedIdCount++}" |
| |
| internal var constraintSetsByName = HashMap<String, ConstraintSet>() |
| internal var transitionsByName = HashMap<String, Transition>() |
| |
| internal fun reset() { |
| generatedCount = 0 |
| constraintSetsByName.clear() |
| transitionsByName.clear() |
| } |
| |
| /** |
| * Defines the default [Transition], where the [from] and [to] [ConstraintSet]s will be the |
| * initial start and end states of the layout. |
| * |
| * The default [Transition] will also be applied when the combination of `from` and `to` |
| * ConstraintSets was not defined by a [transition] call. |
| * |
| * This [Transition] is required to initialize [MotionLayout]. |
| */ |
| fun defaultTransition( |
| from: ConstraintSetRef, |
| to: ConstraintSetRef, |
| transitionContent: TransitionScope.() -> Unit = { } |
| ) { |
| transition(from, to, "default", transitionContent) |
| } |
| |
| /** |
| * Creates a [ConstraintSet] that extends the changes applied by [extendConstraintSet] (if not |
| * null). |
| * |
| * A [name] may be provided and it can be used on MotionLayout calls that request a |
| * ConstraintSet name. |
| * |
| * Returns a [ConstraintSetRef] object representing this ConstraintSet, which may be used as a |
| * parameter of [transition]. |
| */ |
| fun constraintSet( |
| name: String? = null, |
| extendConstraintSet: ConstraintSetRef? = null, |
| constraintSetContent: ConstraintSetScope.() -> Unit |
| ): ConstraintSetRef { |
| return addConstraintSet( |
| constraintSet = DslConstraintSet( |
| description = constraintSetContent, |
| extendFrom = extendConstraintSet?.let { constraintSetsByName[it.name] } |
| ), |
| name = name |
| ) |
| } |
| |
| /** |
| * Adds a [Transition] defined by [transitionContent]. A [name] may be provided and it can |
| * be used on MotionLayout calls that request a Transition name. |
| * |
| * Where [from] and [to] are the ConstraintSets handled by it. |
| */ |
| fun transition( |
| from: ConstraintSetRef, |
| to: ConstraintSetRef, |
| name: String? = null, |
| transitionContent: TransitionScope.() -> Unit |
| ) { |
| val transitionName = name ?: nextName() |
| transitionsByName[transitionName] = TransitionImpl( |
| parsedTransition = TransitionScope( |
| from = from.name, |
| to = to.name |
| ).apply(transitionContent).getObject() |
| ) |
| } |
| |
| /** |
| * Adds an existing [ConstraintSet] object to the scope of this MotionScene. A [name] may be |
| * provided and it can be used on MotionLayout calls that request a ConstraintSet name. |
| * |
| * Returns a [ConstraintSetRef] object representing the added [constraintSet], which may be used |
| * as a parameter of [transition]. |
| */ |
| fun addConstraintSet( |
| constraintSet: ConstraintSet, |
| name: String? = null |
| ): ConstraintSetRef { |
| val cSetName = name ?: nextName() |
| constraintSetsByName[cSetName] = constraintSet |
| return ConstraintSetRef(cSetName) |
| } |
| |
| /** |
| * Adds an existing [Transition] object to the scope of this MotionScene. A [name] may be |
| * provided and it can be used on MotionLayout calls that request a Transition name. |
| * |
| * The [ConstraintSet]s referenced by the transition must match the name of a [ConstraintSet] |
| * added within this scope. |
| * |
| * @see [constraintSet] |
| * @see [addConstraintSet] |
| */ |
| fun addTransition( |
| transition: Transition, |
| name: String? = null |
| ) { |
| val transitionName = name ?: nextName() |
| transitionsByName[transitionName] = transition |
| } |
| |
| /** |
| * Creates one [ConstrainedLayoutReference] corresponding to the [ConstraintLayout] element |
| * with [id]. |
| */ |
| fun createRefFor(id: Any): ConstrainedLayoutReference = ConstrainedLayoutReference(id) |
| |
| /** |
| * Convenient way to create multiple [ConstrainedLayoutReference] with one statement, the [ids] |
| * provided should match Composables within ConstraintLayout using [androidx.compose.ui.Modifier.layoutId]. |
| * |
| * Example: |
| * ``` |
| * val (box, text, button) = createRefsFor("box", "text", "button") |
| * ``` |
| * Note that the number of ids should match the number of variables assigned. |
| * |
| * |
| * |
| * To create a singular [ConstrainedLayoutReference] see [createRefFor]. |
| */ |
| fun createRefsFor(vararg ids: Any): ConstrainedLayoutReferences = |
| ConstrainedLayoutReferences(arrayOf(*ids)) |
| |
| inner class ConstrainedLayoutReferences internal constructor( |
| private val ids: Array<Any> |
| ) { |
| operator fun component1(): ConstrainedLayoutReference = |
| ConstrainedLayoutReference(ids.getOrElse(0) { nextId() }) |
| |
| operator fun component2(): ConstrainedLayoutReference = |
| createRefFor(ids.getOrElse(1) { nextId() }) |
| |
| operator fun component3(): ConstrainedLayoutReference = |
| createRefFor(ids.getOrElse(2) { nextId() }) |
| |
| operator fun component4(): ConstrainedLayoutReference = |
| createRefFor(ids.getOrElse(3) { nextId() }) |
| |
| operator fun component5(): ConstrainedLayoutReference = |
| createRefFor(ids.getOrElse(4) { nextId() }) |
| |
| operator fun component6(): ConstrainedLayoutReference = |
| createRefFor(ids.getOrElse(5) { nextId() }) |
| |
| operator fun component7(): ConstrainedLayoutReference = |
| createRefFor(ids.getOrElse(6) { nextId() }) |
| |
| operator fun component8(): ConstrainedLayoutReference = |
| createRefFor(ids.getOrElse(7) { nextId() }) |
| |
| operator fun component9(): ConstrainedLayoutReference = |
| createRefFor(ids.getOrElse(8) { nextId() }) |
| |
| operator fun component10(): ConstrainedLayoutReference = |
| createRefFor(ids.getOrElse(9) { nextId() }) |
| |
| operator fun component11(): ConstrainedLayoutReference = |
| createRefFor(ids.getOrElse(10) { nextId() }) |
| |
| operator fun component12(): ConstrainedLayoutReference = |
| createRefFor(ids.getOrElse(11) { nextId() }) |
| |
| operator fun component13(): ConstrainedLayoutReference = |
| createRefFor(ids.getOrElse(12) { nextId() }) |
| |
| operator fun component14(): ConstrainedLayoutReference = |
| createRefFor(ids.getOrElse(13) { nextId() }) |
| |
| operator fun component15(): ConstrainedLayoutReference = |
| createRefFor(ids.getOrElse(14) { nextId() }) |
| |
| operator fun component16(): ConstrainedLayoutReference = |
| createRefFor(ids.getOrElse(15) { nextId() }) |
| } |
| |
| /** |
| * Declare a custom Float [value] addressed by [name]. |
| */ |
| fun ConstrainScope.customFloat(name: String, value: Float) { |
| if (!containerObject.has("custom")) { |
| containerObject.put("custom", CLObject(charArrayOf())) |
| } |
| val customPropsObject = containerObject.getObjectOrNull("custom") ?: return |
| customPropsObject.putNumber(name, value) |
| } |
| |
| /** |
| * Declare a custom Color [value] addressed by [name]. |
| */ |
| fun ConstrainScope.customColor(name: String, value: Color) { |
| if (!containerObject.has("custom")) { |
| containerObject.put("custom", CLObject(charArrayOf())) |
| } |
| val customPropsObject = containerObject.getObjectOrNull("custom") ?: return |
| customPropsObject.putString(name, value.toJsonHexString()) |
| } |
| |
| /** |
| * Declare a custom Int [value] addressed by [name]. |
| */ |
| fun ConstrainScope.customInt(name: String, value: Int) { |
| customFloat(name, value.toFloat()) |
| } |
| |
| /** |
| * Declare a custom Dp [value] addressed by [name]. |
| */ |
| fun ConstrainScope.customDistance(name: String, value: Dp) { |
| customFloat(name, value.value) |
| } |
| |
| /** |
| * Declare a custom TextUnit [value] addressed by [name]. |
| */ |
| fun ConstrainScope.customFontSize(name: String, value: TextUnit) { |
| customFloat(name, value.value) |
| } |
| |
| /** |
| * Sets the custom Float [value] at the frame of the current [KeyAttributeScope]. |
| */ |
| fun KeyAttributeScope.customFloat(name: String, value: Float) { |
| customPropertiesValue[name] = value |
| } |
| |
| /** |
| * Sets the custom Color [value] at the frame of the current [KeyAttributeScope]. |
| */ |
| fun KeyAttributeScope.customColor(name: String, value: Color) { |
| // Colors must be in the following format: "#AARRGGBB" |
| customPropertiesValue[name] = value.toJsonHexString() |
| } |
| |
| /** |
| * Sets the custom Int [value] at the frame of the current [KeyAttributeScope]. |
| */ |
| fun KeyAttributeScope.customInt(name: String, value: Int) { |
| customPropertiesValue[name] = value |
| } |
| |
| /** |
| * Sets the custom Dp [value] at the frame of the current [KeyAttributeScope]. |
| */ |
| fun KeyAttributeScope.customDistance(name: String, value: Dp) { |
| customPropertiesValue[name] = value.value |
| } |
| |
| /** |
| * Sets the custom TextUnit [value] at the frame of the current [KeyAttributeScope]. |
| */ |
| fun KeyAttributeScope.customFontSize(name: String, value: TextUnit) { |
| customPropertiesValue[name] = value.value |
| } |
| |
| private fun Color.toJsonHexString() = |
| "#${this.toArgb().toUInt().toString(16)}" |
| } |
| |
| data class ConstraintSetRef internal constructor( |
| internal val name: String |
| ) |