Skip to content
This repository was archived by the owner on Dec 27, 2024. It is now read-only.

Commit ad6cd32

Browse files
authored
Move basic transitions on single constraintset to ConstraintLayout instead of MotionLayout (#360)
1 parent d7e308f commit ad6cd32

File tree

3 files changed

+140
-74
lines changed

3 files changed

+140
-74
lines changed

constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/ConstraintLayout.kt

Lines changed: 83 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ import android.os.Handler
2121
import android.os.Looper
2222
import android.util.Log
2323
import androidx.annotation.FloatRange
24+
import androidx.compose.animation.core.Animatable
25+
import androidx.compose.animation.core.AnimationSpec
26+
import androidx.compose.animation.core.tween
2427
import androidx.compose.foundation.Canvas
2528
import androidx.compose.foundation.Image
2629
import androidx.compose.foundation.background
@@ -63,6 +66,7 @@ import androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviou
6366
import androidx.constraintlayout.core.widgets.analyzer.BasicMeasure
6467
import androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.Measure.TRY_GIVEN_DIMENSIONS
6568
import androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.Measure.USE_GIVEN_DIMENSIONS
69+
import kotlinx.coroutines.channels.Channel
6670
import org.intellij.lang.annotations.Language
6771
import java.lang.StringBuilder
6872
import java.util.*
@@ -209,57 +213,106 @@ private class ConstraintSetForInlineDsl(
209213
*
210214
* Example usage:
211215
* @sample androidx.compose.foundation.layout.samples.DemoConstraintSet
216+
*
217+
* When recomposed with different constraintsets, you can use the animateChanges parameter
218+
* to animate the layout changes (animationSpec and finishedAnimationListener attributes can
219+
* also be useful in this mode). This is only intended for basic transitions, if more control
220+
* is needed, we recommend using MotionLayout instead.
212221
*/
213222
@Suppress("NOTHING_TO_INLINE")
214223
@Composable
215224
inline fun ConstraintLayout(
216225
constraintSet: ConstraintSet,
217226
modifier: Modifier = Modifier,
218227
optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD,
228+
animateChanges: Boolean = false,
229+
animationSpec: AnimationSpec<Float> = tween<Float>(),
230+
noinline finishedAnimationListener: (() -> Unit)? = null,
219231
noinline content: @Composable () -> Unit
220232
) {
221-
val needsUpdate = remember {
222-
mutableStateOf(0L)
223-
}
233+
if (animateChanges) {
234+
var startConstraint by remember { mutableStateOf(constraintSet) }
235+
var endConstraint by remember { mutableStateOf(constraintSet) }
236+
val progress = remember { Animatable(0.0f) }
237+
val channel = remember { Channel<ConstraintSet>(Channel.CONFLATED) }
238+
val direction = remember { mutableStateOf(1) }
239+
240+
SideEffect {
241+
channel.trySend(constraintSet)
242+
}
243+
244+
LaunchedEffect(channel) {
245+
for (constraints in channel) {
246+
val newConstraints = channel.tryReceive().getOrNull() ?: constraints
247+
val currentConstraints = if (direction.value == 1) startConstraint else endConstraint
248+
if (newConstraints != currentConstraints) {
249+
if (direction.value == 1) {
250+
endConstraint = newConstraints
251+
} else {
252+
startConstraint = newConstraints
253+
}
254+
progress.animateTo(direction.value.toFloat(), animationSpec)
255+
direction.value = if (direction.value == 1) 0 else 1
256+
finishedAnimationListener?.invoke()
257+
}
258+
}
259+
}
224260

225-
val measurer = remember { Measurer() }
226-
val measurePolicy = rememberConstraintLayoutMeasurePolicy(optimizationLevel, needsUpdate, constraintSet, measurer)
227-
if (constraintSet is EditableJSONLayout) {
228-
constraintSet.setUpdateFlag(needsUpdate)
229-
}
230-
if (constraintSet is JSONConstraintSet) {
231-
measurer.addLayoutInformationReceiver(constraintSet)
261+
MotionLayout(
262+
start = startConstraint,
263+
end = endConstraint,
264+
progress = progress.value,
265+
modifier = modifier,
266+
content = { content() })
232267
} else {
233-
measurer.addLayoutInformationReceiver(null)
234-
}
268+
val needsUpdate = remember {
269+
mutableStateOf(0L)
270+
}
235271

236-
val forcedScaleFactor = measurer.forcedScaleFactor
237-
if (!forcedScaleFactor.isNaN()) {
238-
var mod = modifier.scale(measurer.forcedScaleFactor)
239-
Box {
272+
val measurer = remember { Measurer() }
273+
val measurePolicy = rememberConstraintLayoutMeasurePolicy(
274+
optimizationLevel,
275+
needsUpdate,
276+
constraintSet,
277+
measurer
278+
)
279+
if (constraintSet is EditableJSONLayout) {
280+
constraintSet.setUpdateFlag(needsUpdate)
281+
}
282+
if (constraintSet is JSONConstraintSet) {
283+
measurer.addLayoutInformationReceiver(constraintSet)
284+
} else {
285+
measurer.addLayoutInformationReceiver(null)
286+
}
287+
288+
val forcedScaleFactor = measurer.forcedScaleFactor
289+
if (!forcedScaleFactor.isNaN()) {
290+
var mod = modifier.scale(measurer.forcedScaleFactor)
291+
Box {
292+
@Suppress("DEPRECATION")
293+
MultiMeasureLayout(
294+
modifier = mod.semantics { designInfoProvider = measurer },
295+
measurePolicy = measurePolicy,
296+
content = {
297+
measurer.createDesignElements()
298+
content()
299+
}
300+
)
301+
with(measurer) {
302+
drawDebugBounds(forcedScaleFactor)
303+
}
304+
}
305+
} else {
240306
@Suppress("DEPRECATION")
241307
MultiMeasureLayout(
242-
modifier = mod.semantics { designInfoProvider = measurer },
308+
modifier = modifier.semantics { designInfoProvider = measurer },
243309
measurePolicy = measurePolicy,
244310
content = {
245311
measurer.createDesignElements()
246312
content()
247313
}
248314
)
249-
with(measurer) {
250-
drawDebugBounds(forcedScaleFactor)
251-
}
252315
}
253-
} else {
254-
@Suppress("DEPRECATION")
255-
MultiMeasureLayout(
256-
modifier = modifier.semantics { designInfoProvider = measurer },
257-
measurePolicy = measurePolicy,
258-
content = {
259-
measurer.createDesignElements()
260-
content()
261-
}
262-
)
263316
}
264317
}
265318

constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/MotionLayout.kt

Lines changed: 0 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -49,49 +49,6 @@ import kotlinx.coroutines.channels.Channel
4949
import org.intellij.lang.annotations.Language
5050
import java.util.*
5151

52-
private val defaultAnimation = spring<Float>()
53-
/**
54-
* Layout that interpolate its children layout given a set of constraints and
55-
* animates any changes to those constraints
56-
*/
57-
@Composable
58-
fun MotionLayout(
59-
constraintSet: ConstraintSet,
60-
modifier: Modifier = Modifier,
61-
animationSpec: AnimationSpec<Float> = defaultAnimation,
62-
finishedListener: (() -> Unit)? = null,
63-
content: @Composable MotionLayoutScope.() -> Unit
64-
) {
65-
var currentConstraints by remember { mutableStateOf(constraintSet) }
66-
val progress = remember { Animatable(0.0f) }
67-
val channel = remember { Channel<ConstraintSet>(Channel.CONFLATED) }
68-
69-
SideEffect {
70-
channel.trySend(constraintSet)
71-
}
72-
73-
LaunchedEffect(channel) {
74-
for (constraints in channel) {
75-
val newConstraints = channel.tryReceive().getOrNull() ?: constraints
76-
if (newConstraints != currentConstraints) {
77-
progress.snapTo(0f)
78-
progress.animateTo(1f, animationSpec)
79-
80-
currentConstraints = newConstraints
81-
finishedListener?.invoke()
82-
}
83-
}
84-
85-
}
86-
87-
MotionLayout(
88-
start = currentConstraints,
89-
end = constraintSet,
90-
progress = progress.value,
91-
modifier = modifier,
92-
content = content)
93-
}
94-
9552
/**
9653
* Layout that interpolate its children layout given two sets of constraint and
9754
* a progress (from 0 to 1)

projects/ComposeConstraintLayout/app/src/main/java/com/example/constraintlayout/MotionComposeExamples.kt

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,61 @@ import androidx.compose.ui.unit.sp
2727
import androidx.constraintlayout.compose.*
2828
import java.util.*
2929

30+
31+
@Preview(group = "constraintlayout1")
32+
@Composable
33+
public fun AnimatedConstraintLayoutExample1() {
34+
var animateToEnd by remember { mutableStateOf(false) }
35+
36+
val baseConstraintSetStart = """
37+
{
38+
box: {
39+
width: 100,
40+
height: 150,
41+
centerHorizontally: 'parent',
42+
top: ['parent', 'top', 16]
43+
}
44+
}
45+
46+
"""
47+
48+
val baseConstraintSetEnd = """
49+
{
50+
box: {
51+
width: 100,
52+
height: 150,
53+
centerHorizontally: 'parent',
54+
bottom: ['parent', 'bottom', 16]
55+
}
56+
}
57+
"""
58+
59+
val cs1 = ConstraintSet(baseConstraintSetStart)
60+
val cs2 = ConstraintSet(baseConstraintSetEnd)
61+
62+
val constraints = if (animateToEnd) cs2 else cs1
63+
Column {
64+
Button(onClick = { animateToEnd = !animateToEnd }) {
65+
Text(text = "Run")
66+
}
67+
ConstraintLayout(
68+
constraints,
69+
animateChanges = true,
70+
modifier = Modifier
71+
.fillMaxSize()
72+
.background(Color.White)
73+
) {
74+
Box(
75+
modifier = Modifier
76+
.layoutId("box")
77+
.width(100.dp)
78+
.height(150.dp)
79+
.background(Color.Blue)
80+
)
81+
}
82+
}
83+
}
84+
3085
@Preview(group = "motion1")
3186
@Composable
3287
public fun MotionExample1() {
@@ -78,8 +133,9 @@ public fun MotionExample1() {
78133
Button(onClick = { animateToEnd = !animateToEnd }) {
79134
Text(text = "Run")
80135
}
81-
MotionLayout(
136+
ConstraintLayout(
82137
constraints,
138+
animateChanges = true,
83139
modifier = Modifier
84140
.fillMaxSize()
85141
.background(Color.White)

0 commit comments

Comments
 (0)