Skip to content
This repository was archived by the owner on Dec 27, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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