@@ -20,7 +20,7 @@ import android.annotation.SuppressLint
2020import android.graphics.Matrix
2121import androidx.compose.animation.core.Animatable
2222import androidx.compose.animation.core.AnimationSpec
23- import androidx.compose.animation.core.spring
23+ import androidx.compose.animation.core.tween
2424import androidx.compose.foundation.Canvas
2525import androidx.compose.foundation.layout.Box
2626import androidx.compose.foundation.layout.BoxScope
@@ -41,12 +41,11 @@ import androidx.compose.ui.layout.*
4141import androidx.compose.ui.semantics.semantics
4242import androidx.compose.ui.unit.*
4343import androidx.constraintlayout.core.motion.Motion
44- import androidx.constraintlayout.core.parser.CLObject
4544import androidx.constraintlayout.core.parser.CLParser
4645import androidx.constraintlayout.core.parser.CLParsingException
47- import androidx.constraintlayout.core.state.*
4846import androidx.constraintlayout.core.state.Dimension
4947import androidx.constraintlayout.core.state.Transition
48+ import androidx.constraintlayout.core.state.WidgetFrame
5049import androidx.constraintlayout.core.widgets.Optimizer
5150import kotlinx.coroutines.channels.Channel
5251import org.intellij.lang.annotations.Language
@@ -67,6 +66,149 @@ inline fun MotionLayout(
6766 modifier : Modifier = Modifier ,
6867 optimizationLevel : Int = Optimizer .OPTIMIZATION_STANDARD ,
6968 crossinline content : @Composable MotionLayoutScope .() -> Unit
69+ ) {
70+ MotionLayout (
71+ start = start,
72+ end = end,
73+ transition = transition,
74+ progress = progress,
75+ debug = debug,
76+ informationReceiver = null ,
77+ modifier = modifier,
78+ optimizationLevel = optimizationLevel,
79+ content = content
80+ )
81+ }
82+
83+ /* *
84+ * Layout that takes a MotionScene and animates by providing a [constraintSetName] to animate to.
85+ *
86+ * During recomposition, MotionLayout will interpolate from whichever ConstraintSet it is currently
87+ * in, to [constraintSetName].
88+ *
89+ * Typically the first value of [constraintSetName] should match the start ConstraintSet in the
90+ * default transition, or be null.
91+ *
92+ * Animation is run by [animationSpec], and will only start another animation once any other ones
93+ * are finished. Use [finishedAnimationListener] to know when a transition has stopped.
94+ */
95+ @Composable
96+ inline fun MotionLayout (
97+ motionScene : MotionScene ,
98+ constraintSetName : String? = null,
99+ animationSpec : AnimationSpec <Float > = tween<Float >(),
100+ debug : EnumSet <MotionLayoutDebugFlags > = EnumSet .of(MotionLayoutDebugFlags .NONE ),
101+ modifier : Modifier = Modifier ,
102+ optimizationLevel : Int = Optimizer .OPTIMIZATION_STANDARD ,
103+ noinline finishedAnimationListener : (() -> Unit )? = null,
104+ crossinline content : @Composable (MotionLayoutScope .() -> Unit )
105+ ) {
106+ val needsUpdate = remember {
107+ mutableStateOf(0L )
108+ }
109+ motionScene.setUpdateFlag(needsUpdate)
110+
111+ var usedDebugMode = debug
112+ if (motionScene.getForcedDrawDebug() != MotionLayoutDebugFlags .UNKNOWN ) {
113+ usedDebugMode = EnumSet .of(motionScene.getForcedDrawDebug())
114+ }
115+
116+ val transitionContent = remember(motionScene, needsUpdate.value) {
117+ motionScene.getTransition(" default" )
118+ }
119+
120+ val transition: androidx.constraintlayout.compose.Transition ? =
121+ transitionContent?.let { Transition (it) }
122+
123+ val startId = transition?.getStartConstraintSetId() ? : " start"
124+ val endId = transition?.getEndConstraintSetId() ? : " end"
125+
126+ val startContent = remember(motionScene, needsUpdate.value) {
127+ motionScene.getConstraintSet(startId) ? : motionScene.getConstraintSet(0 )
128+ }
129+ val endContent = remember(motionScene, needsUpdate.value) {
130+ motionScene.getConstraintSet(endId) ? : motionScene.getConstraintSet(1 )
131+ }
132+
133+ val targetEndContent = remember(motionScene, constraintSetName) {
134+ constraintSetName?.let { motionScene.getConstraintSet(constraintSetName) }
135+ }
136+
137+ if (startContent == null || endContent == null ) {
138+ return
139+ }
140+
141+ var start: ConstraintSet by remember(motionScene) { mutableStateOf(JSONConstraintSet (content = startContent)) }
142+ var end: ConstraintSet by remember(motionScene) { mutableStateOf(JSONConstraintSet (content = endContent)) }
143+ val targetConstraintSet = targetEndContent?.let { JSONConstraintSet (targetEndContent) }
144+
145+ val progress = remember { Animatable (0f ) }
146+
147+ var animateToEnd by remember(motionScene) { mutableStateOf(true ) }
148+
149+ val channel = remember { Channel <ConstraintSet >(Channel .CONFLATED ) }
150+
151+ if (targetConstraintSet != null ) {
152+ SideEffect {
153+ channel.trySend(targetConstraintSet)
154+ }
155+
156+ LaunchedEffect (motionScene, channel) {
157+ for (constraints in channel) {
158+ val newConstraintSet = channel.tryReceive().getOrNull() ? : constraints
159+ val animTargetValue = if (animateToEnd) 1f else 0f
160+ val currentSet = if (animateToEnd) start else end
161+ if (newConstraintSet != currentSet) {
162+ if (animateToEnd) {
163+ end = newConstraintSet
164+ } else {
165+ start = newConstraintSet
166+ }
167+ progress.animateTo(animTargetValue, animationSpec)
168+ animateToEnd = ! animateToEnd
169+ finishedAnimationListener?.invoke()
170+ }
171+ }
172+ }
173+ }
174+
175+ val lastOutsideProgress = remember { mutableStateOf(0f ) }
176+ val forcedProgress = motionScene.getForcedProgress()
177+
178+ val currentProgress =
179+ if (! forcedProgress.isNaN() && lastOutsideProgress.value == progress.value) {
180+ forcedProgress
181+ } else {
182+ motionScene.resetForcedProgress()
183+ progress.value
184+ }
185+
186+ lastOutsideProgress.value = progress.value
187+
188+ MotionLayout (
189+ start = start,
190+ end = end,
191+ transition = transition,
192+ progress = currentProgress,
193+ debug = usedDebugMode,
194+ informationReceiver = motionScene as ? JSONMotionScene ,
195+ modifier = modifier,
196+ optimizationLevel = optimizationLevel,
197+ content = content
198+ )
199+ }
200+
201+ @Composable
202+ inline fun MotionLayout (
203+ start : ConstraintSet ,
204+ end : ConstraintSet ,
205+ transition : androidx.constraintlayout.compose.Transition ? = null,
206+ progress : Float ,
207+ debug : EnumSet <MotionLayoutDebugFlags > = EnumSet .of(MotionLayoutDebugFlags .NONE ),
208+ informationReceiver : LayoutInformationReceiver ? = null,
209+ modifier : Modifier = Modifier ,
210+ optimizationLevel : Int = Optimizer .OPTIMIZATION_STANDARD ,
211+ crossinline content : @Composable MotionLayoutScope .() -> Unit
70212) {
71213 val measurer = remember { MotionMeasurer () }
72214 val scope = remember { MotionLayoutScope (measurer) }
@@ -83,6 +225,7 @@ inline fun MotionLayout(
83225 progressState,
84226 measurer
85227 )
228+ measurer.addLayoutInformationReceiver(informationReceiver)
86229
87230 val forcedScaleFactor = measurer.forcedScaleFactor
88231 if (! debug.contains(MotionLayoutDebugFlags .NONE ) || ! forcedScaleFactor.isNaN()) {
@@ -220,7 +363,6 @@ inline fun MotionLayout(
220363 content = { scope.content() }
221364 ))
222365 }
223-
224366}
225367
226368@Immutable
@@ -525,11 +667,13 @@ internal class MotionMeasurer : Measurer() {
525667 this .measureScope = measureScope
526668 var layoutSizeChanged = false
527669 if (constraints.hasFixedWidth
528- && ! state.sameFixedWidth(constraints.maxWidth)) {
670+ && ! state.sameFixedWidth(constraints.maxWidth)
671+ ) {
529672 layoutSizeChanged = true
530673 }
531674 if (constraints.hasFixedHeight
532- && ! state.sameFixedHeight(constraints.maxHeight)) {
675+ && ! state.sameFixedHeight(constraints.maxHeight)
676+ ) {
533677 layoutSizeChanged = true
534678 }
535679 if (motionProgress != progress
0 commit comments