Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
rosuH opened this issue Dec 4, 2023 · 2 comments
Labels
bug Something isn't working

Comments

@rosuH
Copy link

rosuH commented Dec 4, 2023

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.

Screen_recording_20231204_231220.mp4

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 rosuH added the bug Something isn't working label Dec 4, 2023
@jafu888
Copy link
Collaborator

jafu888 commented Dec 5, 2023

The constraints in the constraintSet define the position for the start and end.
They do not define the motion to get between the two points.

Getting one object to have a path relative to another is not supported in the dsl syntax.
It is supported in the json syntax seen here.

orbit.mp4

@rosuH
Copy link
Author

rosuH commented Dec 5, 2023

I attempted to use JSON syntax and made changes to my code, as shown below:

val jsonScene =
        """
            {
            Variables: {
                    angle: {
                      from: 0,
                      step: 51,
                    },
                    distance: 100,
                    angle2: {
                      from: 51,
                      step: 51,
                    },
                    distance2: 70,
                    mylist: {
                      tag: 'unSelectedColorList',
                    },
                  },
              ConstraintSets: {
                start: {
                  selectedColor: {
                    width: 5,
                    height: 5,
                    center: 'parent',
                  },
                  Generate: {
                    mylist: {
                      width: 5,
                      height: 5,
                      circular: [
                        'parent',
                        'angle',
                        'distance',
                      ],
                    },
                  },
                },
                end: {
                  selectedColor: {
                    width: 70,
                    height: 70,
                    center: 'parent',
                  },
                  Generate: {
                    mylist: {
                      width: 45,
                      height: 45,
                      circular: [
                        'parent',
                        'angle2',
                        'distance2',
                      ],
                    },
                  },
                },
              },
              Transitions: {           
                  default: {            
                    from: 'start',      
                    to: 'end',          
                  }
                }
            }
        """.trimIndent()

    val animateToEnd by remember { mutableStateOf(true) }
    val progress = remember { Animatable(0f) }
    LaunchedEffect(animateToEnd) {
        delay(50)
        progress.animateTo(
            if (animateToEnd) 1f else 0f,
            animationSpec = tween(5000)
        )
    }


    MotionLayout(
        motionScene = MotionScene(jsonScene),
        modifier = Modifier.fillMaxSize(),
        progress = progress.value,
        debugFlags = DebugFlags.All,
    ) {
        unSelectedColorList.forEachIndexed { index, colorItem ->
            ColorItemComponent(
                colorItem = colorItem,
                modifier = Modifier
                    .layoutId("id${index}", "unSelectedColorList")
                    .clip(CircleShape),
            )
        }

        ColorItemComponent(
            colorItem = selectedColor,
            modifier = Modifier
                .border(
                    width = 3.dp,
                    color = Color(white),
                    shape = CircleShape
                )
                .layoutId("selectedColor")
                .clip(CircleShape),
        )
    }

However, the behavior appears to be unexpected. It seems that all constraint nodes within the list are being ignored.

Screen_recording_20231206_002958.mp4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants