How to create animation with Jetpack Compose? - android

I want to use Compose instead of ConstraintLayout in this codelab: https://codelabs.developers.google.com/codelabs/advanced-android-kotlin-training-property-animation/#1
How can I apply any animation to Compose?

As of beta08 now it's lot easier
#Composable
fun RotateLoader() {
val animation = rememberInfiniteTransition()
val angle = animation.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 1000,
easing = LinearEasing
),
repeatMode = RepeatMode.Restart
)
)
Icon(
painter = painterResource(id = R.drawable.ic_loader),
contentDescription = null,
modifier = Modifier.rotate(angle.value)
)
}

Here is the guide how to apply animation:
And piece of code from that tutorial:
private val rotation = FloatPropKey()
private fun createDefinition(duration: Int) = transitionDefinition {
state(0) { this[rotation] = 0f }
state(1) { this[rotation] = 360f }
transition {
rotation using repeatable {
animation = tween {
easing = LinearEasing
this.duration = duration
}
iterations = Infinite
}
}
}
#Composable
fun RotateIndefinitely(durationPerRotation: Int, children: #Composable() () -> Unit) {
Transition(definition = createDefinition(durationPerRotation), initState = 0, toState = 1) {
Rotate(it[rotation], children)
}
}
#Composable
fun Rotate(degree: Float, children: #Composable() () -> Unit) {
Draw(children = children) { canvas, parent ->
val halfWidth = parent.width.value / 2
val halfHeight = parent.height.value / 2
canvas.save()
canvas.translate(halfWidth, halfHeight)
canvas.rotate(degree)
canvas.translate(-halfWidth, -halfHeight)
drawChildren()
canvas.restore()
}
}
#Composable
private fun RotatingPokeBall() {
RotateIndefinitely(durationPerRotation = 4000) {
Opacity(opacity = 0.75f) {
DrawImage(
image = +imageResource(R.drawable.pokeball),
tint = +colorResource(R.color.poke_red)
)
}
}
}

Related

Jetpack Compose Animation skips to target value immediately

I'm trying to achieve a smooth animation of a simple round timer. Like this, but smoother
However it just skips to targetValue immediately and that's it there's no animation at all. I'm trying to do it like this:
#Composable
private fun SampleTimer(duration: Int, modifier: Modifier = Modifier) {
var animatedPercentage by remember { mutableStateOf(1f) }
LaunchedEffect(Unit) {
animate(
initialValue = 1f,
targetValue = 0f,
animationSpec = infiniteRepeatable(
tween(
durationMillis = duration.seconds.inWholeMilliseconds.toInt(),
easing = LinearEasing,
),
),
) { value, _ ->
animatedPercentage = value
}
}
val arcColor = MaterialTheme.colors.primaryVariant
Canvas(
modifier = modifier,
) {
drawArc(
color = arcColor,
useCenter = true,
startAngle = -90f,
sweepAngle = -360f * animatedPercentage,
)
}
}
Why does this happen, what am I missing here?
You can use an Animatable state. The angle will animate from 0–360°.
Something like:
val angle = remember {
Animatable(0f)
}
LaunchedEffect(angle) {
launch {
angle.animateTo(360f, animationSpec =
infiniteRepeatable(
tween(
durationMillis = 5000,
easing = LinearEasing,
),
)
)
}
}
val arcColor = Red
Canvas(
modifier = Modifier.size(100.dp),
) {
drawArc(
color = arcColor,
useCenter = true,
startAngle = -90f,
sweepAngle = -angle.value,
)
}
The problem was that the animations were turned off in developer settings on my device, and I forgot that

How to make FlipCard animation in Jetpack compose

I have an existing app where I have implemented FlipCard animation like below using Objectanimator in XML. If I click on a card it flips horizontally. But now I want to migrate it to jetpack compose. So is it possible to make flip card animation in jetpack compose?
Update
Finally, I have ended up with this. Though I don't know if it is the right way or not but I got exactly what I wanted. If there is any better alternative you can suggest. Thank you.
Method 1: Using animate*AsState
#Composable
fun FlipCard() {
var rotated by remember { mutableStateOf(false) }
val rotation by animateFloatAsState(
targetValue = if (rotated) 180f else 0f,
animationSpec = tween(500)
)
val animateFront by animateFloatAsState(
targetValue = if (!rotated) 1f else 0f,
animationSpec = tween(500)
)
val animateBack by animateFloatAsState(
targetValue = if (rotated) 1f else 0f,
animationSpec = tween(500)
)
val animateColor by animateColorAsState(
targetValue = if (rotated) Color.Red else Color.Blue,
animationSpec = tween(500)
)
Box(
Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Card(
Modifier
.fillMaxSize(.5f)
.graphicsLayer {
rotationY = rotation
cameraDistance = 8 * density
}
.clickable {
rotated = !rotated
},
backgroundColor = animateColor
)
{
Column(
Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = if (rotated) "Back" else "Front",
modifier = Modifier
.graphicsLayer {
alpha = if (rotated) animateBack else animateFront
rotationY = rotation
})
}
}
}
}
Method 2: Encapsulate a Transition and make it reusable.
You will get the same output as method 1. But it is reusable and for the complex case.
enum class BoxState { Front, Back }
#Composable
fun AnimatingBox(
rotated: Boolean,
onRotate: (Boolean) -> Unit
) {
val transitionData = updateTransitionData(
if (rotated) BoxState.Back else BoxState.Front
)
Card(
Modifier
.fillMaxSize(.5f)
.graphicsLayer {
rotationY = transitionData.rotation
cameraDistance = 8 * density
}
.clickable { onRotate(!rotated) },
backgroundColor = transitionData.color
)
{
Column(
Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = if (rotated) "Back" else "Front",
modifier = Modifier
.graphicsLayer {
alpha =
if (rotated) transitionData.animateBack else transitionData.animateFront
rotationY = transitionData.rotation
})
}
}
}
private class TransitionData(
color: State<Color>,
rotation: State<Float>,
animateFront: State<Float>,
animateBack: State<Float>
) {
val color by color
val rotation by rotation
val animateFront by animateFront
val animateBack by animateBack
}
#Composable
private fun updateTransitionData(boxState: BoxState): TransitionData {
val transition = updateTransition(boxState, label = "")
val color = transition.animateColor(
transitionSpec = {
tween(500)
},
label = ""
) { state ->
when (state) {
BoxState.Front -> Color.Blue
BoxState.Back -> Color.Red
}
}
val rotation = transition.animateFloat(
transitionSpec = {
tween(500)
},
label = ""
) { state ->
when (state) {
BoxState.Front -> 0f
BoxState.Back -> 180f
}
}
val animateFront = transition.animateFloat(
transitionSpec = {
tween(500)
},
label = ""
) { state ->
when (state) {
BoxState.Front -> 1f
BoxState.Back -> 0f
}
}
val animateBack = transition.animateFloat(
transitionSpec = {
tween(500)
},
label = ""
) { state ->
when (state) {
BoxState.Front -> 0f
BoxState.Back -> 1f
}
}
return remember(transition) { TransitionData(color, rotation, animateFront, animateBack) }
}
Output
setContent {
ComposeAnimationTheme {
Surface(color = MaterialTheme.colors.background) {
var state by remember {
mutableStateOf(CardFace.Front)
}
FlipCard(
cardFace = state,
onClick = {
state = it.next
},
axis = RotationAxis.AxisY,
back = {
Text(text = "Front", Modifier
.fillMaxSize()
.background(Color.Red))
},
front = {
Text(text = "Back", Modifier
.fillMaxSize()
.background(Color.Green))
}
)
}
}
}
enum class CardFace(val angle: Float) {
Front(0f) {
override val next: CardFace
get() = Back
},
Back(180f) {
override val next: CardFace
get() = Front
};
abstract val next: CardFace
}
enum class RotationAxis {
AxisX,
AxisY,
}
#ExperimentalMaterialApi
#Composable
fun FlipCard(
cardFace: CardFace,
onClick: (CardFace) -> Unit,
modifier: Modifier = Modifier,
axis: RotationAxis = RotationAxis.AxisY,
back: #Composable () -> Unit = {},
front: #Composable () -> Unit = {},
) {
val rotation = animateFloatAsState(
targetValue = cardFace.angle,
animationSpec = tween(
durationMillis = 400,
easing = FastOutSlowInEasing,
)
)
Card(
onClick = { onClick(cardFace) },
modifier = modifier
.graphicsLayer {
if (axis == RotationAxis.AxisX) {
rotationX = rotation.value
} else {
rotationY = rotation.value
}
cameraDistance = 12f * density
},
) {
if (rotation.value <= 90f) {
Box(
Modifier.fillMaxSize()
) {
front()
}
} else {
Box(
Modifier
.fillMaxSize()
.graphicsLayer {
if (axis == RotationAxis.AxisX) {
rotationX = 180f
} else {
rotationY = 180f
}
},
) {
back()
}
}
}
}
Check this article. https://fvilarino.medium.com/creating-a-rotating-card-in-jetpack-compose-ba94c7dd76fb.

How to create timed Instagram story loading bar using Jetpack Compose animation?

I would like to create a composable component very similar to Instagram story loading bar with 10 seconds of duration.
I had an idea how to do it but I'm not how to executed. I was thinking about adding a static bar (grey color) using a BOX and then add another bar (white color) which animates from 0 to final of composable width in 10 seconds.
Do you have any idea how I can implement this component?
You can use this Composable to create a segmented progress bar
private const val BackgroundOpacity = 0.25f
private const val NumberOfSegments = 8
private val StrokeWidth = 4.dp
private val SegmentGap = 8.dp
#Composable
fun SegmentedProgressIndicator(
/*#FloatRange(from = 0.0, to = 1.0)*/
progress: Float,
modifier: Modifier = Modifier,
color: Color = MaterialTheme.colors.primary,
backgroundColor: Color = color.copy(alpha = BackgroundOpacity),
strokeWidth: Dp = StrokeWidth,
numberOfSegments: Int = NumberOfSegments,
segmentGap: Dp = SegmentGap
) {
val gap: Float
val stroke: Float
with(LocalDensity.current) {
gap = segmentGap.toPx()
stroke = strokeWidth.toPx()
}
Canvas(
modifier
.progressSemantics(progress)
.fillMaxWidth()
.height(strokeWidth)
.focusable()
) {
drawSegments(1f, backgroundColor, stroke, numberOfSegments, gap)
drawSegments(progress, color, stroke, numberOfSegments, gap)
}
}
private fun DrawScope.drawSegments(
progress: Float,
color: Color,
strokeWidth: Float,
segments: Int,
segmentGap: Float,
) {
val width = size.width
val start = 0f
val gaps = (segments - 1) * segmentGap
val segmentWidth = (width - gaps) / segments
val barsWidth = segmentWidth * segments
val end = barsWidth * progress + (progress * segments).toInt()* segmentGap
repeat(segments) { index ->
val offset = index * (segmentWidth + segmentGap)
if (offset < end) {
val barEnd = (offset + segmentWidth).coerceAtMost(end)
drawLine(
color,
Offset(start + offset, 0f),
Offset(barEnd, 0f),
strokeWidth
)
}
}
}
You use it like this
var running by remember { mutableStateOf(false) }
val progress: Float by animateFloatAsState(
if (running) 1f else 0f,
animationSpec = tween(
durationMillis = 10_000,
easing = LinearEasing
)
)
Surface(color = MaterialTheme.colors.background) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
SegmentedProgressIndicator(
progress = progress,
modifier = Modifier
.padding(top = 64.dp, start = 32.dp, end = 32.dp)
.fillMaxWidth(),
)
Button(
onClick = { running = !running },
modifier = Modifier.padding(top = 32.dp)
) {
Text(
text = if (running) "Reverse Animation" else "Start Animation"
)
}
}
}
This is the result
You can animate the progress of a LinearProgressIndicator.
Something like:
var enabled by remember { mutableStateOf(false) }
val progress: Float by animateFloatAsState(
if (enabled) 1f else 0.0f,
animationSpec = tween(
durationMillis = 10000,
delayMillis = 40,
easing = LinearOutSlowInEasing
)
)
LinearProgressIndicator(
color = White,
backgroundColor = LightGray,
progress = progress,
modifier = Modifier.width(100.dp)
)
Just set enabled=true to start the animation (it is just an example).
try this solution, it should work:
var progress by remember {mutableStateOf(0.0f)}
var enabled by remember { mutableStateOf(true) }
LaunchedEffect(key1 = progress, key2 = enabled) {
if(progress<1 && enabled) {
delay(100L)
progress += 0.01F
}
}
LinearProgressIndicator(
color = White,
backgroundColor = LightGray,
progress = progress,
modifier = Modifier.width(100.dp)
)
This is can be a full sample solution you can see and try to extend as well as refactor. The code is self-explanatory and you will see how it goes.
A story composable:
#OptIn(ExperimentalAnimationApi::class)
#Composable
fun Story(story: Stories) {
AnimatedContent(story) {
when (story) {
Stories.ONE -> {
StoryContent("1")
}
Stories.TWO -> {
StoryContent("2")
}
Stories.THREE -> {
StoryContent("3")
}
}
}
}
#Composable
private fun StoryContent(content: String) {
Text("Story $content")
}
Story progress indicator:
#Composable
fun StoryProgressIndicator(running: Boolean, modifier: Modifier = Modifier, onTenSecondsOnThisStory: () -> Unit) {
val progress: Float by animateFloatAsState(
if (running) 1f else 0f,
animationSpec = tween(
durationMillis = if (running) 10_000 else 0,
easing = LinearEasing
)
)
if (progress == 1f) {
onTenSecondsOnThisStory()
}
LinearProgressIndicator(
progress, modifier
)
}
And a screen that contains the navigation among stories and playing the animation.
#OptIn(ExperimentalAnimationApi::class)
#Composable
fun InstagramLikeStories() {
var screenWidth by remember { mutableStateOf(1) }
var currentStory by remember { mutableStateOf(Stories.ONE) }
var currentStoryPointer by remember { mutableStateOf(0) }
var runningStoryOne by remember { mutableStateOf(false) }
var runningStoryTwo by remember { mutableStateOf(false) }
var runningStoryThree by remember { mutableStateOf(false) }
val runStoryOne = { runningStoryOne = true; runningStoryTwo = false; runningStoryThree = false }
val runStoryTwo = { runningStoryOne = false; runningStoryTwo = true; runningStoryThree = false }
val runStoryThree = { runningStoryOne = false; runningStoryTwo = false; runningStoryThree = true }
val stories = Stories.values()
LaunchedEffect(Unit) { runStoryOne() }
Column(
Modifier.fillMaxSize().onGloballyPositioned {
screenWidth = it.size.width
}.pointerInput(Unit) {
detectTapGestures(onTap = {
if ((it.x / screenWidth) * 100 > 50) {
if (currentStoryPointer == stories.size - 1) {
currentStoryPointer = 0
} else {
currentStoryPointer++
}
currentStory = stories[currentStoryPointer]
} else {
if (currentStoryPointer != 0) {
currentStoryPointer--
currentStory = stories[currentStoryPointer]
}
}
runStoryIndicator(currentStory, runStoryOne, runStoryTwo, runStoryThree)
})
}
) {
Row(Modifier.fillMaxWidth().padding(horizontal = 16.dp)) {
StoryProgressIndicator(runningStoryOne, Modifier.weight(1f), onTenSecondsOnThisStory = {
runStoryIndicator(currentStory, runStoryOne, runStoryTwo, runStoryThree)
currentStoryPointer = 1
currentStory = stories[currentStoryPointer]
})
Spacer(Modifier.weight(0.1f))
StoryProgressIndicator(runningStoryTwo, Modifier.weight(1f), onTenSecondsOnThisStory = {
runStoryIndicator(currentStory, runStoryOne, runStoryTwo, runStoryThree)
currentStoryPointer = 2
currentStory = stories[currentStoryPointer]
})
Spacer(Modifier.weight(0.1f))
StoryProgressIndicator(runningStoryThree, Modifier.weight(1f), onTenSecondsOnThisStory = {
runStoryIndicator(currentStory, runStoryOne, runStoryTwo, runStoryThree)
// go to first one
currentStoryPointer = 0
currentStory = stories[currentStoryPointer]
})
}
}
Story(currentStory)
Row {
Button(onClick = {
}) {
Text("button")
}
}
}
private fun runStoryIndicator(
currentStory: Stories,
runStoryOne: () -> Unit,
runStoryTwo: () -> Unit,
runStoryThree: () -> Unit
) {
when (currentStory) {
Stories.ONE -> runStoryOne()
Stories.TWO -> runStoryTwo()
Stories.THREE -> runStoryThree()
}
}
enum class Stories {
ONE, TWO, THREE
}

How to apply the Layout modifier to each placeable individually in Jetpack Compose?

I have a draggable modifier in my custom layout. The problem is that all my placeables are moving as a block, whereas I would like them to move individually. What would be the correct way of looping through them to make sure only one placeable is selected at a time? Or is there a better way of going about it? Here is my custom layout:
#Composable
fun CustomLayout(
modifier: Modifier = Modifier,
content: #Composable() () -> Unit
) {
val coroutineScope = rememberCoroutineScope()
val offsetX = remember { Animatable(0f) }
val offsetY = remember { Animatable(0f) }
Layout(
modifier = modifier
.offset {
IntOffset(
offsetX.value.roundToInt(),
offsetY.value.roundToInt()
)
}
.draggable(
state = rememberDraggableState { delta ->
coroutineScope.launch {
offsetX.snapTo(offsetX.value + delta)
}
},
orientation = Orientation.Horizontal,
onDragStarted = {},
onDragStopped = {
coroutineScope.launch {
offsetX.animateTo(
targetValue = 0f,
animationSpec = tween(
durationMillis = 1000,
delayMillis = 0
)
)
}
}
),
content = content
) { measurables, constraints ->
val tileSize = constraints.maxWidth / 7
val childConstraints = constraints.copy(
minWidth = minOf(constraints.minWidth, tileSize),
maxWidth = tileSize
)
val placeables = measurables.map { measurable ->
measurable.measure(childConstraints)
}
layout(constraints.maxWidth, constraints.maxHeight) {
var yPosition = 0
val xPosition = 0
placeables.forEachIndexed { index, placeable ->
if (index <= 6) {
placeable.placeRelative(x = xPosition, y = yPosition)
} else {
placeable.placeRelative(
constraints.maxWidth - tileSize,
yPosition - placeable.height * 7
)
}
yPosition += placeable.height
}
}
}
}
Here I would like to move one tile only at a time:
You solution doesn't work because you're applying the offset to the whole layout, but you need to apply it for a single item.
Layout only intended to layout items: in the MeasureScope we only have access to item sizes/positions, and we can't add modifiers to them, as those will modify the state and it'll lead to recursion.
My suggestion is to pass items count and an item generator to your Composable, so we can add both offset and draggable modifiers to each item:
#Composable
fun DraggableLayout(
modifier: Modifier = Modifier,
count: Int,
item: #Composable (Int, Modifier) -> Unit
) {
val coroutineScope = rememberCoroutineScope()
val offsetsX = remember { mutableStateMapOf<Int, Animatable<Float, AnimationVector1D>>() }
CustomLayout(
modifier = modifier,
content = {
for (i in 0 until count) {
item(
i,
Modifier
.offset {
IntOffset(
offsetsX[i]?.value?.roundToInt() ?: 0,
0
)
}
.draggable(
state = rememberDraggableState { delta ->
coroutineScope.launch {
val offsetX = offsetsX[i] ?: Animatable(0f)
offsetX.snapTo(offsetX.value + delta)
offsetsX[i] = offsetX
}
},
orientation = Orientation.Horizontal,
onDragStarted = {},
onDragStopped = {
coroutineScope.launch {
offsetsX[i]!!.animateTo(
targetValue = 0f,
animationSpec = tween(
durationMillis = 1000,
delayMillis = 0
)
)
}
}
),
)
}
}
)
}
#Composable
fun CustomLayout(
modifier: Modifier = Modifier,
content: #Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content,
) { measurables, constraints ->
val tileSize = constraints.maxWidth / 7
val childConstraints = constraints.copy(
minWidth = minOf(constraints.minWidth, tileSize),
maxWidth = tileSize
)
val placeables = measurables.map { measurable ->
measurable.measure(childConstraints)
}
layout(constraints.maxWidth, constraints.maxHeight) {
var yPosition = 0
val xPosition = 0
placeables.forEachIndexed { index, placeable ->
if (index <= 6) {
placeable.placeRelative(x = xPosition, y = yPosition)
} else {
placeable.placeRelative(
constraints.maxWidth - tileSize,
yPosition - placeable.height * 7
)
}
yPosition += placeable.height
}
}
}
}
And use it like this:
CustomLayout(
count = 10,
item = { i, modifier ->
Text(
"Test $i",
modifier = Modifier
.size(50.dp)
.then(modifier)
)
}
)
The result:

Reset offset animation on draggable item in Jetpack Compose

I have a green square that I can drag vertically. But whenever I stop dragging it, I want it to reset the offset to the start with an animation. I tried it like this, but I can't figure it out. Does someone know how to do it?
#Composable
fun DraggableSquare() {
var currentOffset by remember { mutableStateOf(0F) }
val resetAnimation by animateIntOffsetAsState(targetValue = IntOffset(0, currentOffset.roundToInt()))
var shouldReset = false
Box(contentAlignment = Alignment.TopCenter, modifier = Modifier.fillMaxSize()) {
Surface(
color = Color(0xFF34AB52),
modifier = Modifier
.size(100.dp)
.offset {
when {
shouldReset -> resetAnimation
else -> IntOffset(0, currentOffset.roundToInt())
}
}
.draggable(
state = rememberDraggableState { delta -> currentOffset += delta },
orientation = Orientation.Vertical,
onDragStopped = {
shouldReset = true
currentOffset = 0F
}
)
) {}
}
}
You can define the offset as an Animatable.
While dragging use the method snapTo to update the current value as the initial value and the onDragStopped to start the animation.
val coroutineScope = rememberCoroutineScope()
val offsetY = remember { Animatable(0f) }
Box(contentAlignment = Alignment.TopCenter, modifier = Modifier.fillMaxSize()) {
Surface(
color = Color(0xFF34AB52),
modifier = Modifier
.size(100.dp)
.offset {
IntOffset(0, offsetY.value.roundToInt())
}
.draggable(
state = rememberDraggableState { delta ->
coroutineScope.launch {
offsetY.snapTo(offsetY.value + delta)
}
},
orientation = Orientation.Vertical,
onDragStopped = {
coroutineScope.launch {
offsetY.animateTo(
targetValue = 0f,
animationSpec = tween(
durationMillis = 3000,
delayMillis = 0
)
)
}
}
)
) {
}
}

Categories

Resources