constraintlayout icon indicating copy to clipboard operation
constraintlayout copied to clipboard

[Compose] MotionLayout cannot animate the circle angle and distance together

Open rosuH opened this issue 7 months ago • 2 comments

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:

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.

ConstraintLayoutComposeCircleReproduce.zip

rosuH avatar Dec 04 '23 15:12 rosuH