Browse Source
* add BouncingBalls example to falling-balls-mpp * add new bouncing balls at click position instead of a random coordinates * review improvements Co-authored-by: Oleksandr Karpovich <oleksandr.karpovich@jetbrains.com>pull/1703/head
Oleksandr Karpovich
3 years ago
committed by
GitHub
3 changed files with 249 additions and 3 deletions
@ -0,0 +1,191 @@
|
||||
/* |
||||
* Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. |
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. |
||||
*/ |
||||
|
||||
package bouncingBalls |
||||
|
||||
import androidx.compose.foundation.background |
||||
import androidx.compose.foundation.border |
||||
import androidx.compose.foundation.clickable |
||||
import androidx.compose.foundation.gestures.detectTapGestures |
||||
import androidx.compose.foundation.interaction.MutableInteractionSource |
||||
import androidx.compose.foundation.layout.* |
||||
import androidx.compose.foundation.shape.CircleShape |
||||
import androidx.compose.runtime.* |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.composed |
||||
import androidx.compose.ui.geometry.Offset |
||||
import androidx.compose.ui.graphics.Color |
||||
import androidx.compose.ui.input.pointer.pointerInput |
||||
import androidx.compose.ui.layout.onSizeChanged |
||||
import androidx.compose.ui.unit.dp |
||||
import kotlin.math.PI |
||||
import kotlin.math.cos |
||||
import kotlin.math.max |
||||
import kotlin.math.sin |
||||
import kotlin.random.Random |
||||
|
||||
private inline fun Modifier.noRippleClickable(crossinline onClick: (Offset) -> Unit): Modifier = |
||||
composed { |
||||
clickable( |
||||
indication = null, |
||||
interactionSource = remember { MutableInteractionSource() }) { |
||||
}.pointerInput(Unit) { |
||||
detectTapGestures(onTap = { |
||||
println("tap offset = $it") |
||||
onClick(it) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
private var areaWidth = 0 |
||||
private var areaHeight = 0 |
||||
|
||||
@Composable |
||||
fun BouncingBallsApp(initialBallsCount: Int = 5) { |
||||
val items = remember { |
||||
val list = mutableStateListOf<BouncingBall>() |
||||
list.addAll(generateSequence { |
||||
BouncingBall.createBouncingBall() |
||||
}.take(initialBallsCount)) |
||||
list |
||||
} |
||||
|
||||
Box( |
||||
modifier = Modifier.fillMaxWidth() |
||||
.fillMaxHeight() |
||||
.border(width = 1.dp, color = Color.Black) |
||||
.noRippleClickable { |
||||
items += BouncingBall.createBouncingBall(offset = it) |
||||
}.onSizeChanged { |
||||
areaWidth = it.width |
||||
areaHeight = it.height |
||||
} |
||||
) { |
||||
Balls(items) |
||||
} |
||||
|
||||
LaunchedEffect(Unit) { |
||||
var lastTime = 0L |
||||
var dt = 0L |
||||
|
||||
while (true) { |
||||
withFrameNanos { time -> |
||||
dt = time - lastTime |
||||
if (lastTime == 0L) { |
||||
dt = 0 |
||||
} |
||||
lastTime = time |
||||
items.forEach { |
||||
it.recalculate(areaWidth, areaHeight, dt.toFloat()) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
private fun Balls(items: List<BouncingBall>) { |
||||
items.forEachIndexed { ix, ball -> |
||||
key(ix) { |
||||
Box( |
||||
modifier = Modifier |
||||
.offset( |
||||
x = (ball.circle.x.value - ball.circle.r).dp, |
||||
y = (ball.circle.y.value - ball.circle.r).dp |
||||
).size((2 * ball.circle.r).dp) |
||||
.background(ball.color, CircleShape) |
||||
) |
||||
} |
||||
} |
||||
} |
||||
|
||||
private class Circle( |
||||
var x: MutableState<Float>, |
||||
var y: MutableState<Float>, |
||||
val r: Float |
||||
) { |
||||
constructor(x: Float, y: Float, r: Float) : this( |
||||
mutableStateOf(x), mutableStateOf(y), r |
||||
) |
||||
|
||||
fun moveCircle(s: Float, angle: Float, width: Int, height: Int, r: Float) { |
||||
x.value = (x.value + s * sin(angle)).coerceAtLeast(r).coerceAtMost(width.toFloat() - r) |
||||
y.value = (y.value + s * cos(angle)).coerceAtLeast(r).coerceAtMost(height.toFloat() - r) |
||||
} |
||||
} |
||||
|
||||
private fun calculatePosition(circle: Circle, boundingWidth: Int, boundingHeight: Int): Position { |
||||
val southmost = circle.y.value + circle.r |
||||
val northmost = circle.y.value - circle.r |
||||
val westmost = circle.x.value - circle.r |
||||
val eastmost = circle.x.value + circle.r |
||||
|
||||
return when { |
||||
southmost >= boundingHeight -> Position.TOUCHES_SOUTH |
||||
northmost <= 0 -> Position.TOUCHES_NORTH |
||||
eastmost >= boundingWidth -> Position.TOUCHES_EAST |
||||
westmost <= 0 -> Position.TOUCHES_WEST |
||||
else -> Position.INSIDE |
||||
} |
||||
} |
||||
|
||||
private enum class Position { |
||||
INSIDE, |
||||
TOUCHES_SOUTH, |
||||
TOUCHES_NORTH, |
||||
TOUCHES_WEST, |
||||
TOUCHES_EAST |
||||
} |
||||
|
||||
private class BouncingBall( |
||||
val circle: Circle, |
||||
val velocity: Float, |
||||
var angle: Double, |
||||
val color: Color = Color.Red |
||||
) { |
||||
fun recalculate(width: Int, height: Int, dt: Float) { |
||||
val position = calculatePosition(circle, width, height) |
||||
|
||||
val dtMillis = dt / 1000000 |
||||
|
||||
when (position) { |
||||
Position.TOUCHES_SOUTH -> angle = PI - angle |
||||
Position.TOUCHES_EAST -> angle = -angle |
||||
Position.TOUCHES_WEST -> angle = -angle |
||||
Position.TOUCHES_NORTH -> angle = PI - angle |
||||
Position.INSIDE -> angle |
||||
} |
||||
|
||||
circle.moveCircle( |
||||
velocity * (dtMillis.coerceAtMost(500f) / 1000), |
||||
angle.toFloat(), |
||||
width, |
||||
height, |
||||
circle.r |
||||
) |
||||
} |
||||
|
||||
companion object { |
||||
private val random = Random(100) |
||||
private val angles = listOf(PI / 4, -PI / 3, 3 * PI / 4, -PI / 6, -1.1 * PI) |
||||
private val colors = listOf(Color.Red, Color.Black, Color.Green, Color.Magenta) |
||||
|
||||
private fun randomOffset(): Offset { |
||||
return Offset( |
||||
x = random.nextInt(100, 700).toFloat(), |
||||
y = random.nextInt(100, 500).toFloat() |
||||
) |
||||
} |
||||
|
||||
fun createBouncingBall(offset: Offset = randomOffset()): BouncingBall { |
||||
return BouncingBall( |
||||
circle = Circle(x = offset.x, y = offset.y, r = random.nextInt(10, 50).toFloat()), |
||||
velocity = random.nextInt(100, 200).toFloat(), |
||||
angle = angles.random(), |
||||
color = colors.random().copy(alpha = max(0.3f, random.nextFloat())) |
||||
) |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue