Multiple objects animated at once in Compose - android

I'm trying to animate the movement of multiple circles on canvas at once. So far I managed to animate one, that moves to a random spot on canvas on every user click using Animatable. Now I want to add another 2 circles that do the same but move to another, also randomly chosen spot. Is there a way to achieve it easily without launching multiple coroutines?
My code so far:
#Composable
fun CanvasScreen(){
val animationScope = rememberCoroutineScope()
val animationX = remember{Animatable(0f)}
val animationY = remember{Animatable(0f)}
val randomColor = Color((Math.random() * 16777215).toInt() or (0xFF shl 24))
Canvas(
modifier = Modifier
.fillMaxSize()
.clickable {
animationScope.launch {
launch {
animationX.animateTo(
targetValue = (90..1000)
.random()
.toFloat()
)
}
launch {
animationY.animateTo(
targetValue = (90..1500)
.random()
.toFloat()
)
}
}
}
){
drawCircle(
color = randomColor,
radius = 90f,
center = Offset(animationX.value, animationY.value),
)
}
}

it took me a while but i finally got it done, there may be a simpler way of course but this can work.
#Composable
fun CanvasScreen(){
val circleNumber = 10
val animationScope = rememberCoroutineScope()
val randomColorList = remember { arrayListOf<Color>()}
val animationXList = remember { arrayListOf<Animatable<Float, AnimationVector1D>>() }
val animationYList = remember { arrayListOf<Animatable<Float, AnimationVector1D>>() }
for(i in 0 until circleNumber){
randomColorList.add(Color((Math.random() * 16777215).toInt() or (0xFF shl 24)))
animationXList.add(Animatable(0f))
animationYList.add(Animatable(0f))
}
Canvas(
modifier = Modifier
.fillMaxSize()
.clickable {
animationXList.forEach {
animationScope.launch {
launch {
it.animateTo(
targetValue = (90..1000)
.random()
.toFloat()
)
}
}
}
animationYList.forEach {
animationScope.launch {
launch {
it.animateTo(
targetValue = (90..1500)
.random()
.toFloat()
)
}
}
}
}
){
for(i in 0 until circleNumber) {
drawCircle(
color = randomColorList[i],
radius = 90f,
center = Offset(animationXList[i].value, animationYList[i].value),
)
}
}
}

Related

Android Jetpack Compose - Animate canvas path inside LazyRow

i'm new to android compose, i'm trying to animate a canvas path inside items in a LazyRow. When application is running, if i scroll back and forth, before animations ends, some elements do not draw correctly. Cannot figure out if is a state problem or what else.
Is it also possible to animate only one time per element, so when scrolling the same element animation will not repeat?
Below the code used for the test. Any help is appreciated! Thanks
#Composable
private fun drawPointsAnimation() {
LazyRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
items(15) { _ ->
val points = getPoints(
path = getPathBox(
Size(LocalDensity.current.run { 50.dp.toPx() },
LocalDensity.current.run { 50.dp.toPx() })
)
)
val pointsCopy = mutableListOf<Offset>()
var targetIndexValue by remember {
mutableStateOf(0)
}
val currentIndex by animateIntAsState(
targetValue = targetIndexValue,
animationSpec = tween(4000, easing = LinearEasing)
)
LaunchedEffect(Unit) {
targetIndexValue = points.size - 1
}
Box(
modifier = Modifier
.height(50.dp)
.width(
50.dp
)
.background(Color.Blue)
) {
Canvas(modifier = Modifier.fillMaxSize()) {
pointsCopy.add(points[currentIndex])
drawPoints(
points = pointsCopy,
strokeWidth = 3f,
pointMode = PointMode.Lines,
color = Color.White
)
}
}
}
}
}
private fun getPoints(path: Path): MutableList<Offset> {
val pm = android.graphics.PathMeasure(path.asAndroidPath(), false)
val pointsList = mutableListOf<Offset>()
var i = 0.0f
while (i < 1) {
val aCoordinates = floatArrayOf(0f, 0f)
pm.getPosTan(pm.length * i, aCoordinates, null)
pointsList.add(Offset(x = aCoordinates[0], y = aCoordinates[1]))
i += 0.01f
}
return pointsList
}
private fun getPathBox(size: Size): Path {
val path = Path()
path.moveTo(size.width, 0f)
path.lineTo(0f, size.height)
return path
}
You declare all of the variable to do the animation inside the item() block. I guess everytime the list need to render an item at some index it will re-declare you variable value, so that's why the animation is restarted. Maybe try to declare variable outside of the item() block? Like store animateIntAsState() inside a list and access that value for each of the item index independently?

How can I fix toggle animation in Switch (Compose)?

I have a toggle issue with Switch that can look as following:
In the picture you can see only two of many possible states.
I have complex business logics in ViewModel that updates my whole screen state after clicking on switch.
However, in order to make it easier and demonstrate you the problem I found a simple example that is similar to my real-life scenario.
#Composable
fun MyCoolWidget() {
var isChecked by remember { mutableStateOf(true) }
val scope = rememberCoroutineScope()
Switch(
checked = isChecked,
onCheckedChange = {
scope.launch {
delay(50) // to mimic the business logic and state update delay
// I use the 50 millis delay and then update the state
isChecked = it
}
},
)
}
Now you can test it by putting your finger to the one edge, holding the finger on the screen and moving it to the opposite edge. (Don't click on switch, SWIPE it!)
Observe the result.
How can I fix this problem?
Dependencies:
androidx.compose.material:material:1.1.1. Jetpack Compose version - 1.2.0-rc01. Kotlin version - 1.6.21
Thanks, best wishes!
It looks like a bug.
I would code my custom switch if i were you because you have to disable swipe feature.
Here is my custom ios like switch :
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun MySwitch(
modifier: Modifier = Modifier,
isChecked: Int,
onCheckedChange: (checked: Int) -> Unit
) {
var size by remember {
mutableStateOf(IntSize.Zero)
}
val marbleSize by remember(size) {
mutableStateOf(size.height.div(2))
}
val yOffset by remember(size, marbleSize) {
mutableStateOf((size.height.div(2) - marbleSize.div(2f)).toInt())
}
val marblePadding = 4.dp.value
val scope = rememberCoroutineScope()
val swipeableState = rememberSwipeableState(isChecked)
val backgroundColor = animateColorAsState(
targetValue = if (swipeableState.currentValue != 0) Color(0xFF34C759) else Color(0xD6787880)
)
val sizePx = size.width.minus(marbleSize + marblePadding.times(2))
val anchors = mapOf(0f to 0, sizePx - 1f to 1)
LaunchedEffect(key1 = swipeableState.currentValue, block = {
onCheckedChange.invoke(swipeableState.currentValue)
})
Box(
modifier = modifier
.aspectRatio(2f)
.clip(CircleShape)
.swipeable(
state = swipeableState,
anchors = anchors,
enabled = false, //because you need to disable swipe
orientation = Orientation.Horizontal
)
.pointerInput(Unit) {
detectTapGestures(
onTap = {
if (it.x > size.width.div(2))
scope.launch {
swipeableState.animateTo(
1,
anim = tween(250, easing = LinearEasing)
)
}
else
scope.launch {
swipeableState.animateTo(
0,
anim = tween(250, easing = LinearEasing)
)
}
}
)
}
.background(backgroundColor.value)
.onSizeChanged {
size = it
}
) {
Box(
modifier = Modifier
.padding(horizontal = marblePadding.dp)
.offset {
IntOffset(
x = swipeableState.offset.value.roundToInt(),
y = yOffset
)
}
.size(with(LocalDensity.current) { marbleSize.toDp() })
.clip(CircleShape)
.background(Color.Red)
)
}
}
I hope it helps you.

How to animate a rectangle like a Progress Bar in Jetpack Compose

How to implement a progress bar like animation to the second rectangle, as well as control the animation such as start & pause
#Composable
fun ProgressBar(modifier: Modifier = Modifier.size(300.dp, 180.dp)) {
Canvas(modifier = modifier.fillMaxSize().padding(16.dp)) {
val canvasWidth = size.width
val canvasHeight = size.height
val canvasSize = size
val percentage = 0.2F
drawRoundRect(
color = Pink80,
topLeft = Offset(x = 0F, y = 0F),
size = Size(canvasSize.width, 55F),
cornerRadius = CornerRadius(60F, 60F),
)
drawRoundRect(
color = Purple40,
topLeft = Offset(x = 0F, y = 0F),
size = Size(canvasWidth * 0.2F , 55F),
cornerRadius = CornerRadius(60F, 60F),
)
}
}
Here are two solutions. The first one uses Compose's own LinearProgressIndicator. The second one is a custom built one. It is very simple and has the advantage that you can easily customize it.
No need to use a canvas. When the progress bar reaches it's maximum, the code resets its value back to zero, but you can leave it set to 100% by not resetting the value. The LaunchedEffect is only being used to simulate updating the value. In a production app, you wouldn't use this but rather update the value with some other means that aligns with your code's business logic...
LinearProgressIndicator:
See: LinearProgressIndicator
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startActivity(intent)
setContent {
var enabled by remember { mutableStateOf(false) }
var progress by remember { mutableStateOf(0.1f) }
val animatedProgress by animateFloatAsState(
targetValue = progress,
)
LaunchedEffect(enabled) {
while ((progress < 1) && enabled) {
progress += 0.005f
delay(10)
}
}
if (progress >= 1f) {
enabled = false
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally
) {
LinearProgressIndicator(progress = animatedProgress, modifier = Modifier.requiredHeight(20.dp))
Spacer(Modifier.requiredHeight(30.dp))
Button(
onClick = {
enabled = !enabled
}
) {
if (enabled) {
Text("Pause")
} else {
Text("Start")
}
}
}
}
}
}
Custom Progress Indicator:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startActivity(intent)
setContent {
var enabled by remember { mutableStateOf(false) }.apply { this.value }
var progressValue by remember { mutableStateOf(0) }
LaunchedEffect(enabled) {
while ((progressValue < 100) && enabled) {
progressValue++
delay(10)
}
if (progressValue == 100) {
enabled = false
progressValue = 0
}
}
Column(modifier = Modifier.fillMaxSize().padding(20.dp)) {
ProgressBar(value = progressValue)
Button(
onClick = {
enabled = !enabled
}
) {
if (enabled) {
Text("Pause")
} else {
Text("Start")
}
}
}
}
}
}
#Composable
fun ProgressBar(
value: Int
) {
Box(
modifier = Modifier
.fillMaxWidth()
.requiredHeight(20.dp)
.border(width = 1.dp, color = Color.Black)
) {
Box(
modifier = Modifier
.fillMaxWidth(value.toFloat() / 100f)
.fillMaxHeight()
.background(color = Color.Red)
) {
}
}
}
https://github.com/DogusTeknoloji/compose-progress
You can check this library as support animated and also step progress. The library base on canvas

Weird behaviour from Interactive Line in Cavnas

I created an interactive line, but that might be irrelevant. Even if there was no interaction, this renders unexpected results:-
#Composable
fun PowerClock() {
var dynamicAngle by remember { mutableStateOf(90f.toRadians()) }
val angle by animateFloatAsState(targetValue = dynamicAngle)
Canvas(
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth()
.pointerInput(Unit) { //Irrelevent, the results go wrong even without invoking this at all
coroutineScope {
while (true) {
// val touchDownPointerId = awaitPointerEventScope { awaitFirstDown().id }
detectDragGestures { _, dragAmount ->
dynamicAngle += atan(dragAmount.x / dragAmount.y)
}
}
}
}
) {
val length = 500
val path = Path().apply {
moveTo(size.width / 2, size.height / 2)
relativeLineTo(length * cos(angle), length * sin(angle))
}
drawPath(path, Color.Blue, style = Stroke(10f))
}
}
Here's a bit of a preview,
An intriguing behaviour portrayed by Cavnas is that looking at my implementation, the angle should change based on both the x and y change, right? But in actuality, y is out ignored. I have tested this.
Is this a bug in Cavnas or am I implementing something wrong?
I've followed this answer and adopted code to Compose:
var touchPosition by remember { mutableStateOf(Offset.Zero) }
Canvas(
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth()
.pointerInput(Unit) { //Irrelevent, the results go wrong even without invoking this at all
while (true) {
detectDragGestures { change, _ ->
touchPosition = change.position
}
}
}
) {
val rect = Rect(Offset.Zero, size)
val length = 500
val path = Path().apply {
moveTo(rect.center.x, rect.center.y)
val angle = (touchPosition - rect.center).let { atan2(it.y, it.x) }
relativeLineTo(length * cos(angle), length * sin(angle))
}
drawPath(path, Color.Blue, style = Stroke(10f))
}
Result:

Animate based on dimensions

I want to animate a composable based on a fixed value and the $width of the composable.
How can I get the $width to use it for the animate function?
This is my code
#Composable
fun ExpandingCircle() {
val (checked, setChecked) = remember { mutableStateOf(false) }
val radius = if (checked) **$width** else 4.dp
val radiusAnimated = animate(radius)
Canvas(
modifier = Modifier.fillMaxSize()
.clickable(onClick = { setChecked(!checked) }),
onDraw = {
drawCircle(color = Color.Black, radius = radiusAnimated.toPx())
}
)
}
We can get the size from DrawScope, from the size we can get the width and height of the Canvas, So you can do animation like this.
#Composable
fun ExpandingCircle() {
val (checked, setChecked) = remember { mutableStateOf(false) }
val unCheckedRadius = 4.dp
Canvas(
modifier = Modifier.fillMaxSize()
.clickable(onClick = { setChecked(!checked) }),
onDraw = {
val width = size.width
drawCircle(color = Color.Black, radius = if (checked) width/2 else unCheckedRadius.toPx())
}
)
}
I realized I don't need the width already for animate, but i can just use a animated / interpolating float to use it for the calculation in the DrawScope
#Composable
fun ExpandingCircle() {
val (checked, setChecked) = remember { mutableStateOf(false) }
val radiusExpandFactor = if (checked) 1f else 0f
val radiusExpandFactorAnimated = animate(radiusExpandFactor)
Canvas(
modifier = Modifier.fillMaxSize()
.clickable(onClick = { setChecked(!checked) }),
onDraw = {
val radius = 4.dp.toPx() + (radiusExpandFactorAnimated * (size.width / 2 - 4.dp.toPx()))
drawCircle(color = Color.Black, radius = radius)
}
)
}

Categories

Resources