@@ -40,6 +40,8 @@ import androidx.compose.ui.graphics.nativeCanvas
4040import androidx.compose.ui.layout.*
4141import androidx.compose.ui.semantics.semantics
4242import androidx.compose.ui.unit.*
43+ import androidx.compose.ui.util.fastAny
44+ import androidx.compose.ui.util.fastForEach
4345import androidx.constraintlayout.core.motion.Motion
4446import androidx.constraintlayout.core.parser.CLParser
4547import androidx.constraintlayout.core.parser.CLParsingException
@@ -362,7 +364,7 @@ internal inline fun MotionLayoutCore(
362364 val measurer = remember { MotionMeasurer () }
363365 val scope = remember { MotionLayoutScope (measurer) }
364366 val progressState = remember { mutableStateOf(0f ) }
365- SideEffect { progressState.value = progress }
367+ progressState.value = progress
366368 val measurePolicy =
367369 rememberMotionLayoutMeasurePolicy(
368370 optimizationLevel,
@@ -382,6 +384,7 @@ internal inline fun MotionLayoutCore(
382384 if (! forcedScaleFactor.isNaN()) {
383385 mod = modifier.scale(measurer.forcedScaleFactor)
384386 }
387+ // FIXME: Consider removing this Box wrapper, it breaks the expected behavior of the layout
385388 Box {
386389 @Suppress(" DEPRECATION" )
387390 (MultiMeasureLayout (
@@ -690,8 +693,10 @@ internal class MotionMeasurer : Measurer() {
690693 // make sure that the constraints/dimensions returned are for the start/current ConstraintSet
691694
692695 private fun measureConstraintSet (
693- optimizationLevel : Int , constraintSet : ConstraintSet ,
694- measurables : List <Measurable >, constraints : Constraints
696+ optimizationLevel : Int ,
697+ constraintSet : ConstraintSet ,
698+ measurables : List <Measurable >,
699+ constraints : Constraints
695700 ) {
696701 state.reset()
697702 constraintSet.applyTo(state, measurables)
@@ -731,93 +736,140 @@ internal class MotionMeasurer : Measurer() {
731736 ): IntSize {
732737 this .density = measureScope
733738 this .measureScope = measureScope
734- // TODO: Add another check for whenever a measurable/child has changed size, that triggers a
735- // measure of the constraintsets and interpolation
736- var layoutSizeChanged = false
737- if (constraints.hasFixedWidth
738- && ! state.sameFixedWidth(constraints.maxWidth)
739- ) {
740- layoutSizeChanged = true
741- }
742- if (constraints.hasFixedHeight
743- && ! state.sameFixedHeight(constraints.maxHeight)
744- ) {
745- layoutSizeChanged = true
746- }
739+
740+ val needsRemeasure = needsRemeasure(constraints)
741+
747742 if (motionProgress != progress
748743 || (layoutInformationReceiver?.getForcedWidth() != Int .MIN_VALUE
749744 && layoutInformationReceiver?.getForcedHeight() != Int .MIN_VALUE )
750- || this .transition.isEmpty()
751- || frameCache.isEmpty()
752- || layoutSizeChanged
745+ || needsRemeasure
753746 ) {
754- motionProgress = progress
755- if (layoutSizeChanged || this .transition.isEmpty() || frameCache.isEmpty()) {
756- this .transition.clear()
757- resetMeasureState()
758- state.reset()
759- // Define the size of the ConstraintLayout.
760- state.width(
761- if (constraints.hasFixedWidth) {
762- Dimension .Fixed (constraints.maxWidth)
763- } else {
764- Dimension .Wrap ().min(constraints.minWidth)
765- }
766- )
767- state.height(
768- if (constraints.hasFixedHeight) {
769- Dimension .Fixed (constraints.maxHeight)
770- } else {
771- Dimension .Wrap ().min(constraints.minHeight)
772- }
773- )
774- // Build constraint set and apply it to the state.
775- state.rootIncomingConstraints = constraints
776- state.layoutDirection = layoutDirection
747+ recalculateInterpolation(
748+ constraints = constraints,
749+ layoutDirection = layoutDirection,
750+ constraintSetStart = constraintSetStart,
751+ constraintSetEnd = constraintSetEnd,
752+ transition = transition,
753+ measurables = measurables,
754+ optimizationLevel = optimizationLevel,
755+ progress = progress,
756+ remeasure = needsRemeasure
757+ )
758+ }
759+ return IntSize (root.width, root.height)
760+ }
777761
778- measureConstraintSet(
779- optimizationLevel, constraintSetStart, measurables, constraints
780- )
781- this .transition.updateFrom(root, Transition .START )
782- measureConstraintSet(
783- optimizationLevel, constraintSetEnd, measurables, constraints
784- )
785- this .transition.updateFrom(root, Transition .END )
786- if (transition != null ) {
787- transition.applyTo(this .transition, 0 )
762+ /* *
763+ * Indicates if the layout requires measuring before computing the interpolation.
764+ *
765+ * This might happen if the size of MotionLayout or any of its children changed.
766+ *
767+ * MotionLayout size might change from its parent Layout, and in some cases the children size
768+ * might change (eg: A Text layout has a longer string appended).
769+ */
770+ private fun needsRemeasure (constraints : Constraints ): Boolean {
771+ if (this .transition.isEmpty || frameCache.isEmpty()) {
772+ // Nothing measured (by MotionMeasurer)
773+ return true
774+ }
775+
776+ if ((constraints.hasFixedHeight && ! state.sameFixedHeight(constraints.maxHeight))
777+ || (constraints.hasFixedWidth && ! state.sameFixedWidth(constraints.maxWidth))) {
778+ // Layout size changed
779+ return true
780+ }
781+
782+ return root.children.fastAny { child ->
783+ // Check if measurables have changed their size
784+ val measurable = (child.companionWidget as ? Measurable ) ? : return @fastAny false
785+ val interpolatedFrame = this .transition.getInterpolated(child) ? : return @fastAny false
786+ val placeable = placeables[measurable] ? : return @fastAny false
787+ val currentWidth = placeable.width
788+ val currentHeight = placeable.height
789+
790+ // Need to recalculate interpolation if the size of any element changed
791+ return @fastAny currentWidth != interpolatedFrame.width()
792+ || currentHeight != interpolatedFrame.height()
793+ }
794+ }
795+
796+ /* *
797+ * Remeasures based on [constraintSetStart] and [constraintSetEnd] if needed.
798+ *
799+ * Runs the interpolation for the given [progress].
800+ *
801+ * Finally, updates the [Measurable]s dimension if they changed during interpolation.
802+ */
803+ private fun recalculateInterpolation (
804+ constraints : Constraints ,
805+ layoutDirection : LayoutDirection ,
806+ constraintSetStart : ConstraintSet ,
807+ constraintSetEnd : ConstraintSet ,
808+ transition : androidx.constraintlayout.compose.Transition ? ,
809+ measurables : List <Measurable >,
810+ optimizationLevel : Int ,
811+ progress : Float ,
812+ remeasure : Boolean
813+ ) {
814+ motionProgress = progress
815+ if (remeasure) {
816+ this .transition.clear()
817+ resetMeasureState()
818+ state.reset()
819+ // Define the size of the ConstraintLayout.
820+ state.width(
821+ if (constraints.hasFixedWidth) {
822+ Dimension .Fixed (constraints.maxWidth)
823+ } else {
824+ Dimension .Wrap ().min(constraints.minWidth)
788825 }
789- }
790- this .transition.interpolate(root.width, root.height, progress)
791- var index = 0
792- for (child in root.children) {
793- val measurable = child.companionWidget
794- if (measurable !is Measurable ) continue
795- var interpolatedFrame = this .transition.getInterpolated(child)
796- if (interpolatedFrame == null ) {
797- continue
826+ )
827+ state.height(
828+ if (constraints.hasFixedHeight) {
829+ Dimension .Fixed (constraints.maxHeight)
830+ } else {
831+ Dimension .Wrap ().min(constraints.minHeight)
798832 }
799- val placeable = placeables[measurable]
800- val currentWidth = placeable?.width
801- val currentHeight = placeable?.height
802- if (placeable == null
803- || currentWidth != interpolatedFrame.width()
804- || currentHeight != interpolatedFrame.height()
805- ) {
806- measurable.measure(
807- Constraints .fixed(interpolatedFrame.width(), interpolatedFrame.height())
808- )
809- .also {
810- placeables[measurable] = it
811- }
833+ )
834+ // Build constraint set and apply it to the state.
835+ state.rootIncomingConstraints = constraints
836+ state.layoutDirection = layoutDirection
837+
838+ measureConstraintSet(
839+ optimizationLevel, constraintSetStart, measurables, constraints
840+ )
841+ this .transition.updateFrom(root, Transition .START )
842+ measureConstraintSet(
843+ optimizationLevel, constraintSetEnd, measurables, constraints
844+ )
845+ this .transition.updateFrom(root, Transition .END )
846+ transition?.applyTo(this .transition, 0 )
847+ }
848+
849+ this .transition.interpolate(root.width, root.height, progress)
850+
851+ root.children.fastForEach { child ->
852+ // Update measurables to the interpolated dimensions
853+ val measurable = (child.companionWidget as ? Measurable ) ? : return @fastForEach
854+ val interpolatedFrame = this .transition.getInterpolated(child) ? : return @fastForEach
855+ val placeable = placeables[measurable]
856+ val currentWidth = placeable?.width
857+ val currentHeight = placeable?.height
858+ if (placeable == null
859+ || currentWidth != interpolatedFrame.width()
860+ || currentHeight != interpolatedFrame.height()) {
861+ measurable.measure(
862+ Constraints .fixed(interpolatedFrame.width(), interpolatedFrame.height())
863+ ).also { newPlaceable ->
864+ placeables[measurable] = newPlaceable
812865 }
813- frameCache[measurable] = interpolatedFrame
814- index++
815- }
816- if (layoutInformationReceiver?.getLayoutInformationMode() == LayoutInfoFlags .BOUNDS ) {
817- computeLayoutResult()
818866 }
867+ frameCache[measurable] = interpolatedFrame
868+ }
869+
870+ if (layoutInformationReceiver?.getLayoutInformationMode() == LayoutInfoFlags .BOUNDS ) {
871+ computeLayoutResult()
819872 }
820- return IntSize (root.width, root.height)
821873 }
822874
823875 private fun encodeKeyFrames (
0 commit comments