Jetpack Compose Circle to square animation - android

How can I achieve this animation using jetpack compose?.

#Preview
#Composable
fun BoxAnimation() {
val infiniteTransition = rememberInfiniteTransition()
val cornerPercent by infiniteTransition.animateValue(
initialValue = 1,
targetValue = 50,
typeConverter = Int.VectorConverter,
animationSpec = InfiniteRepeatableSpec(
animation = keyframes {
durationMillis = 3000
0.at(0).with(FastOutSlowInEasing)
0.at(1000).with(FastOutSlowInEasing)
25.at(1500).with(FastOutSlowInEasing)
50.at(2000).with(FastOutSlowInEasing)
50.at(3000).with(FastOutSlowInEasing)
},
repeatMode = RepeatMode.Reverse
)
)
val angle by infiniteTransition.animateValue(
initialValue = 0f,
targetValue = 90f,
typeConverter = Float.VectorConverter,
animationSpec = InfiniteRepeatableSpec(
animation = keyframes {
durationMillis = 3000
0f.at(100).with(FastOutSlowInEasing)
0f.at(1000).with(FastOutSlowInEasing)
45f.at(1500).with(FastOutSlowInEasing)
90f.at(2000).with(FastOutSlowInEasing)
90f.at(3000).with(FastOutSlowInEasing)
},
repeatMode = RepeatMode.Reverse
)
)
Box(
modifier = Modifier
.clipToBounds()
.padding(50.dp)
.rotate(angle)
.clip(RoundedCornerShape(cornerPercent))
.border(
width = 10.dp,
color = Color(0xFF2C6CAD),
shape = RoundedCornerShape(cornerPercent)
)
) {
Box(
modifier = Modifier
.width(150.dp)
.height(150.dp)
.background(Color.Black)
) {}
}
}

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

Jetpack Compose Arc/Circular Progress Bar Animation (How to restart animation)

How do I create a Arc Progress bar animation like this
Currently I've already used Canvas to draw an arc and added animations to the progress bar using animateFloatAsState API. But second pic is not my expected.
[]
// e.g. oldScore = 100f newScore = 350f
// Suppose 250 points are into one level
#Composable
fun ArcProgressbar(
modifier: Modifier = Modifier,
oldScore: Float,
newScore: Float,
level: String,
startAngle: Float = 120f,
limitAngle: Float = 300f,
thickness: Dp = 8.dp
) {
var value by remember { mutableStateOf(oldScore) }
val sweepAngle = animateFloatAsState(
targetValue = (value / 250) * limitAngle, // convert the value to angle
animationSpec = tween(
durationMillis = 1000
)
)
LaunchedEffect(Unit) {
delay(1500)
value = newScore
}
Box(modifier = modifier.fillMaxWidth()) {
Canvas(
modifier = Modifier
.fillMaxWidth(0.45f)
.padding(10.dp)
.aspectRatio(1f)
.align(Alignment.Center),
onDraw = {
// Background Arc
drawArc(
color = Gray100,
startAngle = startAngle,
sweepAngle = limitAngle,
useCenter = false,
style = Stroke(thickness.toPx(), cap = StrokeCap.Square),
size = Size(size.width, size.height)
)
// Foreground Arc
drawArc(
color = Green500,
startAngle = startAngle,
sweepAngle = sweepAngle.value,
useCenter = false,
style = Stroke(thickness.toPx(), cap = StrokeCap.Square),
size = Size(size.width, size.height)
)
}
)
Text(
text = level,
modifier = Modifier
.fillMaxWidth(0.125f)
.align(Alignment.Center)
.offset(y = (-10).dp),
color = Color.White,
fontSize = 82.sp
)
Text(
text = "LEVEL",
modifier = Modifier
.padding(bottom = 8.dp)
.align(Alignment.BottomCenter),
color = Color.White,
fontSize = 20.sp
)
}
}
How can I animate from start again if progress percentage over 100%, just like the one in the gif. Does anybody got some ideas? Thanks!
My first answer doesn't feel like doing any justice since it's far from the gif you posted which shows what you want.
So here's another one that closely resembles it. However, I feel like this implementation is not very efficient in terms of calling sequences of animations, but in terms of re-composition I incorporated some optimization strategy called deferred reading, making sure only the composables that observes the values will be the only parts that will be re-composed. I left a Log statement in the parent progress composable to verify it, the ArcProgressbar is not updating unnecessarily when the progress is animating.
Log.e("ArcProgressBar", "Recomposed")
Full source code that you can copy-and-paste (preferably on a separate file) without any issues.
val maxProgressPerLevel = 200 // you can change this to any max value that you want
val progressLimit = 300f
fun calculate(
score: Float,
level: Int,
) : Float {
return (abs(score - (maxProgressPerLevel * level)) / maxProgressPerLevel) * progressLimit
}
#Composable
fun ArcProgressbar(
modifier: Modifier = Modifier,
score: Float
) {
Log.e("ArcProgressBar", "Recomposed")
var level by remember {
mutableStateOf(score.toInt() / maxProgressPerLevel)
}
var targetAnimatedValue = calculate(score, level)
val progressAnimate = remember { Animatable(targetAnimatedValue) }
val scoreAnimate = remember { Animatable(0f) }
val coroutineScope = rememberCoroutineScope()
LaunchedEffect(level, score) {
if (score > 0f) {
// animate progress
coroutineScope.launch {
progressAnimate.animateTo(
targetValue = targetAnimatedValue,
animationSpec = tween(
durationMillis = 1000
)
) {
if (value >= progressLimit) {
coroutineScope.launch {
level++
progressAnimate.snapTo(0f)
}
}
}
}
// animate score
coroutineScope.launch {
if (scoreAnimate.value > score) {
scoreAnimate.snapTo(0f)
}
scoreAnimate.animateTo(
targetValue = score,
animationSpec = tween(
durationMillis = 1000
)
)
}
}
}
Column(
modifier = modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Box {
PointsProgress(
progress = {
progressAnimate.value // deferred read of progress
}
)
CollectorLevel(
modifier = Modifier.align(Alignment.Center),
level = {
level + 1 // deferred read of level
}
)
}
CollectorScore(
modifier = Modifier.padding(top = 16.dp),
score = {
scoreAnimate.value // deferred read of score
}
)
}
}
#Composable
fun CollectorScore(
modifier : Modifier = Modifier,
score: () -> Float
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Collector Score",
color = Color.White,
fontSize = 16.sp
)
Text(
text = "${score().toInt()} PTS",
color = Color.White,
fontSize = 40.sp
)
}
}
#Composable
fun CollectorLevel(
modifier : Modifier = Modifier,
level: () -> Int
) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
modifier = Modifier
.padding(top = 16.dp),
text = level().toString(),
color = Color.White,
fontSize = 82.sp
)
Text(
text = "LEVEL",
color = Color.White,
fontSize = 16.sp
)
}
}
#Composable
fun BoxScope.PointsProgress(
progress: () -> Float
) {
val start = 120f
val end = 300f
val thickness = 8.dp
Canvas(
modifier = Modifier
.fillMaxWidth(0.45f)
.padding(10.dp)
.aspectRatio(1f)
.align(Alignment.Center),
onDraw = {
// Background Arc
drawArc(
color = Color.LightGray,
startAngle = start,
sweepAngle = end,
useCenter = false,
style = Stroke(thickness.toPx(), cap = StrokeCap.Square),
size = Size(size.width, size.height)
)
// Foreground Arc
drawArc(
color = Color(0xFF3db39f),
startAngle = start,
sweepAngle = progress(),
useCenter = false,
style = Stroke(thickness.toPx(), cap = StrokeCap.Square),
size = Size(size.width, size.height)
)
}
)
}
Sample usage:
#Composable
fun PrizeProgressScreen() {
var score by remember {
mutableStateOf(0f)
}
var scoreInput by remember {
mutableStateOf("0")
}
Column(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFF6b4cba)),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
modifier = Modifier
.padding(vertical = 16.dp),
text = "Progress for every level up: $maxProgressPerLevel",
color = Color.LightGray,
fontSize = 16.sp
)
ArcProgressbar(
score = score,
)
Button(onClick = {
score += scoreInput.toFloat()
}) {
Text("Add Score")
}
TextField(
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
value = scoreInput,
onValueChange = {
scoreInput = it
}
)
}
}
I made some changes in your code to utilize Animatable so we always snap to the beginning before animating to our target value. We also eliminated the computation here since we just want to fill the entire progress every time the score updates, in our case to 300 (limitAngle) and used the newScore state as a key in the LaunchedEffect to trigger the animation every time it increments. Don't mind the +30 increments, its just an arbitrary value that you can change without affecting the animation.
#Composable
fun ArcProgressbar(
modifier: Modifier = Modifier,
newScore: Float,
level: String,
startAngle : Float = 120f,
limitAngle: Float = 300f,
thickness: Dp = 8.dp
) {
val animateValue = remember { Animatable(0f) }
LaunchedEffect(newScore) {
if (newScore > 0f) {
animateValue.snapTo(0f)
delay(10)
animateValue.animateTo(
targetValue = limitAngle,
animationSpec = tween(
durationMillis = 1000
)
)
}
}
Box(modifier = modifier.fillMaxWidth()) {
Canvas(
modifier = Modifier
.fillMaxWidth(0.45f)
.padding(10.dp)
.aspectRatio(1f)
.align(Alignment.Center),
onDraw = {
// Background Arc
drawArc(
color = Color.Gray,
startAngle = startAngle,
sweepAngle = limitAngle,
useCenter = false,
style = Stroke(thickness.toPx(), cap = StrokeCap.Square),
size = Size(size.width, size.height)
)
// Foreground Arc
drawArc(
color = Color.Green,
startAngle = startAngle,
sweepAngle = animateValue.value,
useCenter = false,
style = Stroke(thickness.toPx(), cap = StrokeCap.Square),
size = Size(size.width, size.height)
)
}
)
Column {
Text(
text = level,
modifier = Modifier
.fillMaxWidth(0.125f)
.offset(y = (-10).dp),
color = Color.Gray,
fontSize = 82.sp
)
Text(
text = "LEVEL",
modifier = Modifier
.padding(bottom = 8.dp),
color = Color.Gray,
fontSize = 20.sp
)
Text(
text = "Score ( $newScore ) ",
modifier = Modifier
.padding(bottom = 8.dp),
color = Color.Gray,
fontSize = 20.sp
)
}
}
}
Sample usage:
#Composable
fun ScoreGenerator() {
var newScore by remember {
mutableStateOf(0f)
}
Column {
Button(onClick = {
newScore += 30f
}) {
Text("Add Score + 30")
}
ArcProgressbar(
newScore = newScore,
level = ""
)
}
}

How to detect if two views(composables) are overlapping in jetpack compose?

I'm creating a 2D game with simple animations in jetpack compose. The game is simple, a balloon will float on top, a cannon will shoot arrow towards balloon. If the arrow hits balloon, player gets a point. I'm able to animate all of the above things but one. How would I detect when the arrow and balloon areas intersect?
The arrow is moving upwards with animation, also the balloon is animating side-ways. I want to capture the point at which the arrow touches the balloon. How would I proceed with this?
CODE:
Balloon Composable
#Composable
fun Balloon(modifier: Modifier, gameState: GameState) {
val localConfig = LocalConfiguration.current
val offsetX by animateDpAsState(
targetValue = if (gameState == GameState.START) 0.dp else (localConfig.screenWidthDp.dp - 80.dp),
infiniteRepeatable(
animation = tween((Math.random() * 1000).toInt(), easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
val offsetY by animateDpAsState(
targetValue = if (gameState == GameState.START) 0.dp else (localConfig.screenHeightDp.dp / 3),
infiniteRepeatable(
animation = tween((Math.random() * 1000).toInt(), easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
Box(
modifier = modifier
.offset(offsetX, offsetY)
.size(80.dp)
.background(Color.Red, shape = CircleShape),
contentAlignment = Alignment.Center,
) {
Text(text = "D!!")
}
}
Cannon Composable:
#Composable
fun ShootBalloon(modifier: Modifier) {
val localConfig = LocalConfiguration.current
var gameState by remember { mutableStateOf(GameState.START) }
val offsetX by animateDpAsState(
targetValue = if (gameState == GameState.START) 0.dp else (localConfig.screenWidthDp.dp - 50.dp),
infiniteRepeatable(
animation = tween(2000, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
val offsetY by animateDpAsState(
targetValue = if (gameState == GameState.START) 0.dp else (-(localConfig.screenHeightDp.dp - 50.dp)),
infiniteRepeatable(
animation = tween(500, easing = LinearEasing),
repeatMode = RepeatMode.Restart
)
)
Column(
verticalArrangement = Arrangement.SpaceBetween,
horizontalAlignment = Alignment.Start
) {
if (gameState == GameState.START) {
Button(onClick = { gameState = GameState.PLAYING }) { Text(text = "Play") }
}
Box(
modifier = Modifier
.size(50.dp)
.offset(x = offsetX)
.background(Color.Green),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(R.drawable.ic_baseline_arrow_upward_24),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.offset(y = offsetY)
.onGloballyPositioned { layoutCoordinates ->
val offset = layoutCoordinates.positionInRoot()
}
)
}
}
}
I'm animating following things,
The cannon box moves side ways, the arrow image moves upwards.
The balloon has random movement.
Add a ticker and on each tick check the global coordinate of arrow and offsets of balloon. If values are same, it means they hit on another

Create Animation in jetpack compose corresponds to below code in java?

Below code is in java. We need to do same thing using jetpack compose. please help us to achieve this.
val flashAnimatorSet = AnimatorSet()
val layer = ObjectAnimator.ofInt(
image,
alpha,
255,
77
)
layer.repeatMode = ValueAnimator.REVERSE
layer.repeatCount = ObjectAnimator.INFINITE
flashAnimatorSet.play(lightLayer)
flashAnimatorSet.duration = 100L
flashAnimatorSet.interpolator = AccelerateDecelerateInterpolator()
return flashAnimatorSet
You can use a InfiniteRepeatableSpec to repeat the provided animation infinite amount of times.
Something like:
val infiniteTransition = rememberInfiniteTransition()
val alpha by infiniteTransition.animateFloat(
initialValue = 1F,
targetValue = 0.28F,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 1000,
easing = FastOutSlowInEasing
),
repeatMode = RepeatMode.Reverse
)
)
Then apply it to your Composable, for example:
Image(
painterResource(id = R.drawable.xxx),
modifier = Modifier
.graphicsLayer {
this.alpha = alpha
},
contentDescription = "contentDescription"
)

Android Compose create shake animation

I am trying to make shaking animation of shape in Jetpack Compose. I want to use this animation to show error when user enters invalid Pin code. But all I can find is slide in, slide out animations and some scale animations. Any ideas how I can accomplish this?
Update:
After #Thracian answer. I used code as below, shaking my items horizontally:
fun Modifier.shake(enabled: Boolean, onAnimationFinish: () -> Unit) = composed(
factory = {
val distance by animateFloatAsState(
targetValue = if (enabled) 15f else 0f,
animationSpec = repeatable(
iterations = 8,
animation = tween(durationMillis = 50, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
),
finishedListener = { onAnimationFinish.invoke() }
)
Modifier.graphicsLayer {
translationX = if (enabled) distance else 0f
}
},
inspectorInfo = debugInspectorInfo {
name = "shake"
properties["enabled"] = enabled
}
)
Gif is slower than actual animation unfortunately but it gives an idea of outcome.
This can be done in many ways. You should change scaleX or scaleY or both in short time duration to have a shake effect. If you wish to have rotation change rotationZ of Modifier.graphicsLayer either
#Composable
private fun ShakeAnimationSamples() {
Column(modifier = Modifier
.fillMaxSize()
.padding(10.dp)) {
var enabled by remember {
mutableStateOf(false)
}
val scale by animateFloatAsState(
targetValue = if (enabled) .9f else 1f,
animationSpec = repeatable(
iterations = 5,
animation = tween(durationMillis = 50, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
),
finishedListener = {
enabled = false
}
)
val infiniteTransition = rememberInfiniteTransition()
val scaleInfinite by infiniteTransition.animateFloat(
initialValue = 1f,
targetValue = .85f,
animationSpec = infiniteRepeatable(
animation = tween(30, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
val rotation by infiniteTransition.animateFloat(
initialValue = -10f,
targetValue = 10f,
animationSpec = infiniteRepeatable(
animation = tween(30, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
Icon(
imageVector = Icons.Default.NotificationsActive,
contentDescription = null,
tint = Color.White,
modifier = Modifier
.graphicsLayer {
scaleX = if (enabled) scale else 1f
scaleY = if (enabled) scale else 1f
}
.background(Color.Red, CircleShape)
.size(50.dp)
.padding(10.dp)
)
Icon(
imageVector = Icons.Default.NotificationsActive,
contentDescription = null,
tint = Color.White,
modifier = Modifier
.graphicsLayer {
scaleX = scaleInfinite
scaleY = scaleInfinite
rotationZ = rotation
}
.background(Color.Red, CircleShape)
.size(50.dp)
.padding(10.dp)
)
Button(onClick = { enabled = !enabled }) {
Text("Animation enabled: $enabled")
}
}
}
Also you can do it as a Modifier either
fun Modifier.shake(enabled: Boolean) = composed(
factory = {
val scale by animateFloatAsState(
targetValue = if (enabled) .9f else 1f,
animationSpec = repeatable(
iterations = 5,
animation = tween(durationMillis = 50, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
Modifier.graphicsLayer {
scaleX = if (enabled) scale else 1f
scaleY = if (enabled) scale else 1f
}
},
inspectorInfo = debugInspectorInfo {
name = "shake"
properties["enabled"] = enabled
}
)
Usage
Icon(
imageVector = Icons.Default.NotificationsActive,
contentDescription = null,
tint = Color.White,
modifier = Modifier
.shake(enabled)
.background(Color.Red, CircleShape)
.size(50.dp)
.padding(10.dp)
)

Categories

Resources