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

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

Related

How to set diagonal gradient in Compose

I need set Diagonal gradient on a rectangle as background.
I have two colors (Yellow and Green) which needs to be painted as:
Green from top left to bottom right and Yellow from bottom right to topleft.
I see only linearGradient, horizontalGradient and verticalGradient on Brush in Modifier.
But I'm not able to generate the required angle.
Brush.linearGradient() is diagonal by default with default angle 45 degrees. You can change angle of gradient by changing start and values. Rotating by 45 is easy.
I add a demonstration. You can pick start and end values for any angle from GradientOffset function and paste it to Brush.linearGradient
/**
* Offset for [Brush.linearGradient] to rotate gradient depending on [start] and [end] offsets.
*/
data class GradientOffset(val start: Offset, val end: Offset)
enum class GradientAngle {
CW0, CW45, CW90, CW135, CW180, CW225, CW270, CW315
}
fun GradientOffset(angle: GradientAngle = GradientAngle.CW0): GradientOffset {
return when (angle) {
GradientAngle.CW45 -> GradientOffset(
start = Offset.Zero,
end = Offset.Infinite
)
GradientAngle.CW90 -> GradientOffset(
start = Offset.Zero,
end = Offset(0f, Float.POSITIVE_INFINITY)
)
GradientAngle.CW135 -> GradientOffset(
start = Offset(Float.POSITIVE_INFINITY, 0f),
end = Offset(0f, Float.POSITIVE_INFINITY)
)
GradientAngle.CW180 -> GradientOffset(
start = Offset(Float.POSITIVE_INFINITY, 0f),
end = Offset.Zero,
)
GradientAngle.CW225 -> GradientOffset(
start = Offset.Infinite,
end = Offset.Zero
)
GradientAngle.CW270 -> GradientOffset(
start = Offset(0f, Float.POSITIVE_INFINITY),
end = Offset.Zero
)
GradientAngle.CW315 -> GradientOffset(
start = Offset(0f, Float.POSITIVE_INFINITY),
end = Offset(Float.POSITIVE_INFINITY, 0f)
)
else -> GradientOffset(
start = Offset.Zero,
end = Offset(Float.POSITIVE_INFINITY, 0f)
)
}
}
Demonstration of how gradients are formed based on start and end values
#Composable
private fun RotatableGradientSample() {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Offsets for gradients based on selected angle
var gradientOffset by remember {
mutableStateOf(GradientOffset(GradientAngle.CW45))
}
var angleSelection by remember { mutableStateOf(0f) }
var angleText by remember { mutableStateOf("0 Degrees") }
val brush = Brush.linearGradient(
listOf(Color.Green, Color.Yellow),
start = gradientOffset.start,
end = gradientOffset.end
)
Text(
text = angleText,
color = Color.Red,
modifier = Modifier
.padding(8.dp),
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
Slider(
modifier = Modifier.height(50.dp),
value = angleSelection,
onValueChange = {
angleSelection = it
gradientOffset = when (angleSelection.roundToInt()) {
0 -> {
angleText = "0 Degrees"
GradientOffset(GradientAngle.CW0)
}
1 -> {
angleText = "45 Degrees"
GradientOffset(GradientAngle.CW45)
}
2 -> {
angleText = "90 Degrees"
GradientOffset(GradientAngle.CW90)
}
3 -> {
angleText = "135 Degrees"
GradientOffset(GradientAngle.CW135)
}
4 -> {
angleText = "180 Degrees"
GradientOffset(GradientAngle.CW180)
}
5 -> {
angleText = "225 Degrees"
GradientOffset(GradientAngle.CW225)
}
6 -> {
angleText = "270 Degrees"
GradientOffset(GradientAngle.CW270)
}
else -> {
angleText = "315 Degrees"
GradientOffset(GradientAngle.CW315)
}
}
},
steps = 6,
valueRange = 0f..7f
)
Spacer(modifier = Modifier.height(10.dp))
Box(
Modifier
.fillMaxWidth(.4f)
.aspectRatio(5 / 3f)
.background(brush)
)
Spacer(modifier = Modifier.height(10.dp))
Box(
Modifier
.fillMaxWidth(.4f)
.aspectRatio(1f)
.background(brush)
)
Spacer(modifier = Modifier.height(10.dp))
Box(
Modifier
.fillMaxWidth(.4f)
.aspectRatio(1f)
.background(brush, CircleShape)
)
}
}
You can try:
Box(
modifier = Modifier
.size(100.dp, 50.dp)
.background(
brush = Brush.linearGradient(
colors = listOf(
Color.Yellow,
Color.Green
)
)
)
)

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 = ""
)
}
}

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)
)

Jetpack Compose Circle to square animation

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)
) {}
}
}

Show touch point using Android Jetpack Compose?

How show touch point using Android Jetpack Compose?
Example:
Here is my solution.
#Composable
fun TouchableFeedback() {
// Some constants here
val sizeAnimationDuration = 200
val colorAnimationDuration = 200
val boxSize = 100.dp
val startColor = Color.Red.copy(alpha = .05f)
val endColor = Color.Red.copy(alpha = .8f)
// These states are changed to update the animation
var touchedPoint by remember { mutableStateOf(Offset.Zero) }
var visible by remember { mutableStateOf(false) }
// circle color and size in according to the visible state
val colorAnimation by animateColorAsState(
if (visible) startColor else endColor,
animationSpec = tween(
durationMillis = colorAnimationDuration,
easing = LinearEasing
),
finishedListener = {
visible = false
}
)
val sizeAnimation by animateDpAsState(
if (visible) boxSize else 0.dp,
tween(
durationMillis = sizeAnimationDuration,
easing = LinearEasing
)
)
// Box for the whole screen
Box(
Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectTapGestures {
// changing the state to set the point touched on the screen
// and make it visible
touchedPoint = it
visible = true
}
}
) {
// The touch offset is px and we need to convert to Dp
val density = LocalDensity.current
val (xDp, yDp) = with(density) {
(touchedPoint.x.toDp() - boxSize / 2) to (touchedPoint.y.toDp() - boxSize / 2)
}
// This box serves as container. It has a fixed size.
Box(
Modifier
.offset(xDp, yDp)
.size(boxSize),
) {
// And this box is animating the background and the size
Box(
Modifier
.align(Alignment.Center)
.background(colorAnimation, CircleShape)
.height(if (visible) sizeAnimation else 0.dp)
.width(if (visible) sizeAnimation else 0.dp),
)
}
}
}
Here is the result:

Categories

Resources