constraintlayout
constraintlayout copied to clipboard
[Compose] MotionLayout cannot animate the circle angle and distance together
Description
I intent to implement a path motion using MotionLayout. I set up a start constraintSet
with an initial angle
of 51f
and distance
of 0
. Subsequently, I Configured an end constraintSet
with angle
set to 0f
and distance
set to 70.dp
.
// start constraintSet
constrain(unSelectedRefs[i]) {
width = Dimension.value(0.dp)
height = Dimension.value(0.dp)
// circle chain to selected item
circular(
other = selectedRef,
angle = 51f,
distance = 0.dp
)
}
// end constraintSet
constrain(unSelectedRefs[i]) {
width = Dimension.value(35.dp)
height = Dimension.value(35.dp)
// circle chain to selected item
circular(
other = selectedRef,
angle = 0f,
distance = 70.dp
)
}
What happened?
I observed that the only one of angle
and distance
can be animated at a time, as demonstrated in the video below.
https://github.com/androidx/constraintlayout/assets/15865017/eaacb266-7840-44b3-acc1-7615b6faf093
Expected Behavior
I expect both angle
and distance
to be animated simultaneously. The desired path is illustreated in the accompanying image:
Env
androidx-constraintlayout-compose = { group = "androidx.constraintlayout", name = "constraintlayout-compose", version.ref = "1.1.0-alpha13" }
androidx-motionlayoout-compose = { group = "androidx.constraintlayout", name = "constraintlayout-compose", version.ref = "1.1.0-alpha13" }
Full code
package me.rosuh.constraintlayoutcomposecirclereproduce
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.Dimension
import androidx.constraintlayout.compose.MotionLayout
import androidx.constraintlayout.compose.MotionScene
import kotlinx.coroutines.delay
private val white by lazy {
android.graphics.Color.WHITE
}
private val black by lazy {
android.graphics.Color.BLACK
}
val yellow by lazy {
android.graphics.Color.parseColor("#FFB800")
}
private val orange by lazy {
android.graphics.Color.parseColor("#FF3535")
}
private val pink by lazy {
android.graphics.Color.parseColor("#FF008A")
}
private val blue by lazy {
android.graphics.Color.parseColor("#00D1FF")
}
private val green by lazy {
android.graphics.Color.parseColor("#1BFF3F")
}
private val customPicker by lazy {
-1
}
private val defaultColorList by lazy {
listOf(
ColorItem(white, description = "white"),
ColorItem(black, description = "black"),
ColorItem(yellow, description = "yellow"),
ColorItem(orange, description = "orange"),
ColorItem(pink, description = "pink"),
ColorItem(blue, description = "blue"),
ColorItem(green, description = "green")
)
}
private data class ColorItem(
val color: Int,
val selected: Boolean = false,
val description: String = color.toString(),
val isIcon: Boolean = false,
val iconResId: Int = 0,
)
@Composable
fun ColorOption(
color: Int
) {
val selectedColor = (defaultColorList.find { it.color == color } ?: ColorItem(
color,
description = "color picker",
isIcon = true
)).copy(
selected = true
)
val unSelectedColorList = defaultColorList.filter { it.color != selectedColor.color }
var scene = MotionScene {
val unSelectedRefs = unSelectedColorList.map { createRefFor(it.description) }.toTypedArray()
val selectedRef = createRefFor(selectedColor.description)
val start1 = constraintSet {
constrain(selectedRef) {
width = Dimension.value(35.dp)
height = Dimension.value(35.dp)
alpha = 1f
centerTo(parent)
customFloat("sat", 0f)
customFloat("bright", 0f)
customFloat("rot", -360f)
}
for (i in unSelectedRefs.indices) {
constrain(unSelectedRefs[i]) {
width = Dimension.value(0.dp)
height = Dimension.value(0.dp)
// circle chain to selected item
circular(
other = selectedRef,
angle = 51f,
distance = 0.dp
)
}
}
}
val end1 = constraintSet {
constrain(selectedRef) {
width = Dimension.value(65.dp)
height = Dimension.value(65.dp)
alpha = 1f
centerTo(parent)
customFloat("sat", 0f)
customFloat("bright", 0f)
customFloat("rot", -360f)
}
for (i in unSelectedRefs.indices) {
constrain(unSelectedRefs[i]) {
width = Dimension.value(35.dp)
height = Dimension.value(35.dp)
// circle chain to selected item
circular(
other = selectedRef,
angle = 0f,
distance = 70.dp
)
}
}
}
transition(start1, end1, "default") {}
}
val animateToEnd by remember { mutableStateOf(true) }
val progress = remember { Animatable(0f) }
LaunchedEffect(animateToEnd) {
delay(50)
progress.animateTo(
if (animateToEnd) 1f else 0f,
animationSpec = tween(3000)
)
}
MotionLayout(
scene,
modifier = Modifier.fillMaxSize(),
progress = progress.value
) {
unSelectedColorList.forEach {
ColorItemComponent(
colorItem = it,
modifier = Modifier.layoutId(it.description).clip(CircleShape),
)
}
ColorItemComponent(
colorItem = selectedColor,
modifier = Modifier.border(
width = 3.dp,
color = Color(white),
shape = CircleShape
).layoutId(selectedColor.description).clip(CircleShape),
)
}
}
@Composable
private fun ColorItemComponent(
colorItem: ColorItem,
modifier: Modifier = Modifier,
) {
Image(
painter = if (colorItem.isIcon) {
painterResource(id = R.drawable.ic_btn_color_picker)
} else {
ColorPainter(Color(colorItem.color))
},
contentDescription = "white",
modifier = modifier,
)
}
Sample Project
For your convenience, I have prepared a sample project.