diff --git a/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/ConstraintLayout.kt b/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/ConstraintLayout.kt index a9ea9d077..1f8720971 100644 --- a/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/ConstraintLayout.kt +++ b/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/ConstraintLayout.kt @@ -21,6 +21,9 @@ import android.os.Handler import android.os.Looper import android.util.Log import androidx.annotation.FloatRange +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.tween import androidx.compose.foundation.Canvas import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -63,6 +66,7 @@ import androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviou import androidx.constraintlayout.core.widgets.analyzer.BasicMeasure import androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.Measure.TRY_GIVEN_DIMENSIONS import androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.Measure.USE_GIVEN_DIMENSIONS +import kotlinx.coroutines.channels.Channel import org.intellij.lang.annotations.Language import java.lang.StringBuilder import java.util.* @@ -209,6 +213,11 @@ private class ConstraintSetForInlineDsl( * * Example usage: * @sample androidx.compose.foundation.layout.samples.DemoConstraintSet + * + * When recomposed with different constraintsets, you can use the animateChanges parameter + * to animate the layout changes (animationSpec and finishedAnimationListener attributes can + * also be useful in this mode). This is only intended for basic transitions, if more control + * is needed, we recommend using MotionLayout instead. */ @Suppress("NOTHING_TO_INLINE") @Composable @@ -216,50 +225,94 @@ inline fun ConstraintLayout( constraintSet: ConstraintSet, modifier: Modifier = Modifier, optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD, + animateChanges: Boolean = false, + animationSpec: AnimationSpec = tween(), + noinline finishedAnimationListener: (() -> Unit)? = null, noinline content: @Composable () -> Unit ) { - val needsUpdate = remember { - mutableStateOf(0L) - } + if (animateChanges) { + var startConstraint by remember { mutableStateOf(constraintSet) } + var endConstraint by remember { mutableStateOf(constraintSet) } + val progress = remember { Animatable(0.0f) } + val channel = remember { Channel(Channel.CONFLATED) } + val direction = remember { mutableStateOf(1) } + + SideEffect { + channel.trySend(constraintSet) + } + + LaunchedEffect(channel) { + for (constraints in channel) { + val newConstraints = channel.tryReceive().getOrNull() ?: constraints + val currentConstraints = if (direction.value == 1) startConstraint else endConstraint + if (newConstraints != currentConstraints) { + if (direction.value == 1) { + endConstraint = newConstraints + } else { + startConstraint = newConstraints + } + progress.animateTo(direction.value.toFloat(), animationSpec) + direction.value = if (direction.value == 1) 0 else 1 + finishedAnimationListener?.invoke() + } + } + } - val measurer = remember { Measurer() } - val measurePolicy = rememberConstraintLayoutMeasurePolicy(optimizationLevel, needsUpdate, constraintSet, measurer) - if (constraintSet is EditableJSONLayout) { - constraintSet.setUpdateFlag(needsUpdate) - } - if (constraintSet is JSONConstraintSet) { - measurer.addLayoutInformationReceiver(constraintSet) + MotionLayout( + start = startConstraint, + end = endConstraint, + progress = progress.value, + modifier = modifier, + content = { content() }) } else { - measurer.addLayoutInformationReceiver(null) - } + val needsUpdate = remember { + mutableStateOf(0L) + } - val forcedScaleFactor = measurer.forcedScaleFactor - if (!forcedScaleFactor.isNaN()) { - var mod = modifier.scale(measurer.forcedScaleFactor) - Box { + val measurer = remember { Measurer() } + val measurePolicy = rememberConstraintLayoutMeasurePolicy( + optimizationLevel, + needsUpdate, + constraintSet, + measurer + ) + if (constraintSet is EditableJSONLayout) { + constraintSet.setUpdateFlag(needsUpdate) + } + if (constraintSet is JSONConstraintSet) { + measurer.addLayoutInformationReceiver(constraintSet) + } else { + measurer.addLayoutInformationReceiver(null) + } + + val forcedScaleFactor = measurer.forcedScaleFactor + if (!forcedScaleFactor.isNaN()) { + var mod = modifier.scale(measurer.forcedScaleFactor) + Box { + @Suppress("DEPRECATION") + MultiMeasureLayout( + modifier = mod.semantics { designInfoProvider = measurer }, + measurePolicy = measurePolicy, + content = { + measurer.createDesignElements() + content() + } + ) + with(measurer) { + drawDebugBounds(forcedScaleFactor) + } + } + } else { @Suppress("DEPRECATION") MultiMeasureLayout( - modifier = mod.semantics { designInfoProvider = measurer }, + modifier = modifier.semantics { designInfoProvider = measurer }, measurePolicy = measurePolicy, content = { measurer.createDesignElements() content() } ) - with(measurer) { - drawDebugBounds(forcedScaleFactor) - } } - } else { - @Suppress("DEPRECATION") - MultiMeasureLayout( - modifier = modifier.semantics { designInfoProvider = measurer }, - measurePolicy = measurePolicy, - content = { - measurer.createDesignElements() - content() - } - ) } } diff --git a/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/MotionLayout.kt b/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/MotionLayout.kt index fff2fc799..fa1aa4b8f 100644 --- a/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/MotionLayout.kt +++ b/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/MotionLayout.kt @@ -49,49 +49,6 @@ import kotlinx.coroutines.channels.Channel import org.intellij.lang.annotations.Language import java.util.* -private val defaultAnimation = spring() -/** - * Layout that interpolate its children layout given a set of constraints and - * animates any changes to those constraints - */ -@Composable -fun MotionLayout( - constraintSet: ConstraintSet, - modifier: Modifier = Modifier, - animationSpec: AnimationSpec = defaultAnimation, - finishedListener: (() -> Unit)? = null, - content: @Composable MotionLayoutScope.() -> Unit -) { - var currentConstraints by remember { mutableStateOf(constraintSet) } - val progress = remember { Animatable(0.0f) } - val channel = remember { Channel(Channel.CONFLATED) } - - SideEffect { - channel.trySend(constraintSet) - } - - LaunchedEffect(channel) { - for (constraints in channel) { - val newConstraints = channel.tryReceive().getOrNull() ?: constraints - if (newConstraints != currentConstraints) { - progress.snapTo(0f) - progress.animateTo(1f, animationSpec) - - currentConstraints = newConstraints - finishedListener?.invoke() - } - } - - } - - MotionLayout( - start = currentConstraints, - end = constraintSet, - progress = progress.value, - modifier = modifier, - content = content) -} - /** * Layout that interpolate its children layout given two sets of constraint and * a progress (from 0 to 1) diff --git a/projects/ComposeConstraintLayout/app/src/main/java/com/example/constraintlayout/MotionComposeExamples.kt b/projects/ComposeConstraintLayout/app/src/main/java/com/example/constraintlayout/MotionComposeExamples.kt index 99895b196..de0f2c4b6 100644 --- a/projects/ComposeConstraintLayout/app/src/main/java/com/example/constraintlayout/MotionComposeExamples.kt +++ b/projects/ComposeConstraintLayout/app/src/main/java/com/example/constraintlayout/MotionComposeExamples.kt @@ -27,6 +27,61 @@ import androidx.compose.ui.unit.sp import androidx.constraintlayout.compose.* import java.util.* + +@Preview(group = "constraintlayout1") +@Composable +public fun AnimatedConstraintLayoutExample1() { + var animateToEnd by remember { mutableStateOf(false) } + + val baseConstraintSetStart = """ + { + box: { + width: 100, + height: 150, + centerHorizontally: 'parent', + top: ['parent', 'top', 16] + } + } + + """ + + val baseConstraintSetEnd = """ + { + box: { + width: 100, + height: 150, + centerHorizontally: 'parent', + bottom: ['parent', 'bottom', 16] + } + } + """ + + val cs1 = ConstraintSet(baseConstraintSetStart) + val cs2 = ConstraintSet(baseConstraintSetEnd) + + val constraints = if (animateToEnd) cs2 else cs1 + Column { + Button(onClick = { animateToEnd = !animateToEnd }) { + Text(text = "Run") + } + ConstraintLayout( + constraints, + animateChanges = true, + modifier = Modifier + .fillMaxSize() + .background(Color.White) + ) { + Box( + modifier = Modifier + .layoutId("box") + .width(100.dp) + .height(150.dp) + .background(Color.Blue) + ) + } + } +} + @Preview(group = "motion1") @Composable public fun MotionExample1() { @@ -78,8 +133,9 @@ public fun MotionExample1() { Button(onClick = { animateToEnd = !animateToEnd }) { Text(text = "Run") } - MotionLayout( + ConstraintLayout( constraints, + animateChanges = true, modifier = Modifier .fillMaxSize() .background(Color.White)