I am trying to get a good understanding of how to use MotionLayout
in Jetpack Compose.
At this state, I believe to have a basic understanding of how the MotionLayout
works, by having a MotionScene
(defined in a .json5 file) set to the MotionLayout
and then apply a Modifier.layoutId
to all the Composables, which should have an effect in the animation.
What I am trying to do, is to have a circle, which should stretch out on the X-axis, left side first (for maybe 2-300 ms) then have the right side follow along awards the left (for 2-300 ms), so that it will be a full circle once again - Just at a different position.
An example of what I wish to have, is an animation similar to the three images here.
Sadly I haven't been able to do that yet. In the Transitions
part of the MotionScene
, I have played around with scaleX
, which made the circle misshaped and stretched out to both sides at the same time. And translationX
which ofc, just moves the entire circle instead of just a part of it.
My current implementation looks like this:
Composable
@OptIn(ExperimentalMotionApi::class)
@Preview
@Composable
fun AnimationScreen() {
val context = LocalContext.current
val motionScene = remember {
context.resources.openRawResource(R.raw.motion_scene).readBytes().decodeToString()
}
var progress by remember { mutableStateOf(0f) }
Column {
MotionLayout(
motionScene = MotionScene(content = motionScene),
progress = progress,
modifier = Modifier
.fillMaxWidth()
) {
Row(
modifier = Modifier
.height(200.dp)
.fillMaxWidth()
.layoutId("row_container")
) { }
Box(
modifier = Modifier
.size(100.dp)
.clip(
RoundedCornerShape(
topEnd = 25.dp,
bottomEnd = 25.dp,
topStart = 25.dp,
bottomStart = 25.dp
)
)
.background(Color.Red)
.layoutId("circle")
)
}
Spacer(Modifier.height(32.dp))
Slider(
value = progress,
onValueChange = {
progress = it
}
)
}
}
motion_scene.json5
{
ConstraintSets: {
start: {
circle: {
width: 40,
height: 40,
start: ['logo_pic', 'start', 0],
end: ['logo_pic', 'end', 0],
top: ['logo_pic', 'top', 0],
bottom: ['logo_pic', 'bottom', 0]
},
},
end: {
circle: {
width: 40,
height: 40,
start: ['logo_pic', 'start', 0],
end: ['logo_pic', 'end', 0],
top: ['logo_pic', 'top', 0],
bottom: ['logo_pic', 'bottom', 0],
},
},
},
Transitions: {
default: {
from: 'start',
to: 'end',
pathMotionArc: 'startHorizontal',
KeyFrames: {
KeyAttributes: [
{
target: ['circle'],
frames: [0, 50, 80, 100],
scaleX: [1, 1, 2, 2],
//translationX: [0, 0, 0, -150]
},
],
KeyPosition: [
{
target: ['circle'],
percentWidth: 20
}
]
}
}
}
}
Hopefully it all comes down to my just being new to this framework, and someone just says, "Easy! You just need to...". Any suggestions on, how I would make this work?
There is not a simple way to do this with motionlayout Because the start and end are the same size. But you can add a middle and play some trick with progress. It was coded this way to avoid a few bugs in the latest release.
public const val motionSceneStr = """
{
ConstraintSets: {
start: {
circle: {
width: 40,
height: 40,
start: ['parent', 'start', 0],
top: ['parent', 'top', 0],
bottom: ['parent', 'bottom', 0]
},
},
middle: {
circle: {
width: 'spread',
height: 40,
start: ['parent', 'start', 0],
end: ['parent', 'end', 0],
top: ['parent', 'top', 0],
bottom: ['parent', 'bottom', 0],
},
},
end: {
circle: {
width: 40,
height: 40,
end: ['parent', 'end', 0],
top: ['parent', 'top', 0],
bottom: ['parent', 'bottom', 0],
},
},
},
Transitions: {
part2: { from: 'middle', to: 'end' }
part1: { from: 'middle', to: 'start' }
}
}
""";
@OptIn(ExperimentalMotionApi::class)
@Preview
@Composable
fun AnimationScreen() {
val context = LocalContext.current
val motionScene = remember {motionSceneStr }
var progress by remember { mutableStateOf(0f) }
Column {
MotionLayout(
motionScene = MotionScene(content = motionScene),
progress = if (progress<0.5) 1-progress*2 else progress*2-1,
transitionName = if (progress<0.5f) "part1" else "part2",
modifier = Modifier
.fillMaxWidth()
) {
Row(
modifier = Modifier
.height(200.dp)
.fillMaxWidth()
.layoutId("row_container")
) { }
Box(
modifier = Modifier
.size(100.dp)
.clip(
RoundedCornerShape(
topEnd = 25.dp,
bottomEnd = 25.dp,
topStart = 25.dp,
bottomStart = 25.dp
)
)
.background(Color.Red)
.layoutId("circle")
)
}
Spacer(Modifier.height(32.dp))
Slider(
value = progress,
onValueChange = {
progress = it
}
)
}
}