Skip to content

Commit

Permalink
Move basic transitions on single constraintset to ConstraintLayout in…
Browse files Browse the repository at this point in the history
…stead of MotionLayout (#360)
  • Loading branch information
camaelon authored Jul 30, 2021
1 parent d7e308f commit ad6cd32
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.*
Expand Down Expand Up @@ -209,57 +213,106 @@ 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
inline fun ConstraintLayout(
constraintSet: ConstraintSet,
modifier: Modifier = Modifier,
optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD,
animateChanges: Boolean = false,
animationSpec: AnimationSpec<Float> = tween<Float>(),
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<ConstraintSet>(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()
}
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,49 +49,6 @@ import kotlinx.coroutines.channels.Channel
import org.intellij.lang.annotations.Language
import java.util.*

private val defaultAnimation = spring<Float>()
/**
* 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<Float> = defaultAnimation,
finishedListener: (() -> Unit)? = null,
content: @Composable MotionLayoutScope.() -> Unit
) {
var currentConstraints by remember { mutableStateOf(constraintSet) }
val progress = remember { Animatable(0.0f) }
val channel = remember { Channel<ConstraintSet>(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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit ad6cd32

Please sign in to comment.