[go: nahoru, domu]

blob: 0b17e862b13d45f707f20305703a49cd39484871 [file] [log] [blame]
/*
* 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.
*
* &nbsp;
*
* 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].
*
* &nbsp;
*
* 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.
*
* &nbsp;
*
* 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
)