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?
Related
Looking at the documentation for LazyRow I was wondering if it was possible to reduce the scroll speed, it looks like LazyRow inherits from ScrollState but I can't find anything useful on how to reduce the speed of the scroll
LazyRow doesn't have a parameter to customize scroll speed so you have to do it manually.
You could first capture the scroll gesture something like the below (from google example here ):
#Composable
fun ScrollableSample() {
// actual composable state
var offset by remember { mutableStateOf(0f) }
Box(
Modifier
.size(150.dp)
.scrollable(
orientation = Orientation.Vertical,
// Scrollable state: describes how to consume
// scrolling delta and update offset
state = rememberScrollableState { delta ->
offset += delta
delta
}
)
.background(Color.LightGray),
contentAlignment = Alignment.Center
) {
Text(offset.toString())
}
}
Then you have to implement the scroll state at your desired speed for the LazyRow by manually changing the LazyRow's state which is one of the LazyRow parameters.
you will also have to disable LazyRow userscrolling
something like this:
LazyRow(
...
state = yourCustomState,
userScrollEnabled = false,
...
){ ... }
below is a complete working solution:
var stateIsGood = rememberLazyListState()
var offset = 0f
LazyRow(
modifier = Modifier
.scrollable(
orientation = Orientation.Horizontal,
// Scrollable state: describes how to consume
// scrolling delta and update offset
state = rememberScrollableState { delta ->
offset += delta
vm.viewModelScope.launch {
//dividing by 8 the delta for 8 times slower horizontal scroll,
//you can change direction by making it a negative number
stateIsGood.scrollBy(-delta/8)
}
delta
}
),
state = stateIsGood,
userScrollEnabled = false,
) {
item {
Text(text = "Header")
}
// Add 3 items
items(3) { index ->
Text(text = "SCROLL ME First List items : $index")
}
// Add 2 items
items(2) { index ->
Text(text = "Second List Items: $index")
}
// Add another single item
item {
Text(text = "Footer")
}
}
As a shorter alternative to David's Code,
#Composable
fun LazyStack(ssd: Int = 1) { // Scroll-Speed-Divisor
val lazyStackState = rememberLazyListState()
val lazyStackScope = rememberCoroutineScope()
LazyColumn(
modifier = Modifier.pointerInput(Unit) {
detectVerticalDragGestures { _, dragAmount ->
lazyStackScope.launch {
lazyStackState.scrollBy(-dragAmount / ssd).log("checkpoint")
}
}
},
state = lazyStackState,
userScrollEnabled = false
) {...}
}
And yes, LazyStack is short for LazyStackOverflower
instead of prevent userScroll we can use flingBehavior parameter. When we update initialVelocity it should change fling effect.
#Composable
fun maxScrollFlingBehavior(): FlingBehavior {
val flingSpec = rememberSplineBasedDecay<Float>()
return remember(flingSpec) {
ScrollSpeedFlingBehavior(flingSpec)
}
}
private class ScrollSpeedFlingBehavior(
private val flingDecay: DecayAnimationSpec<Float>
) : FlingBehavior {
override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
// Prevent very fast scroll
val newVelocity =
if (initialVelocity > 0F) minOf(initialVelocity, 15_000F)
else maxOf(initialVelocity, -15_000F)
return if (abs(newVelocity) > 1f) {
var velocityLeft = newVelocity
var lastValue = 0f
AnimationState(
initialValue = 0f,
initialVelocity = newVelocity,
).animateDecay(flingDecay) {
val delta = value - lastValue
val consumed = scrollBy(delta)
lastValue = value
velocityLeft = this.velocity
// avoid rounding errors and stop if anything is unconsumed
if (abs(delta - consumed) > 0.5f) this.cancelAnimation()
}
velocityLeft
} else newVelocity
}
}
and you can use like this
LazyColumn(
modifier = Modifier.fillMaxSize(),
flingBehavior = maxScrollFlingBehavior()
) {
// Content Here
}
If you want to implement your own logic, you can update initialVelocity param. Higher its 'absolute' value, the faster it moves.
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.
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),
)
}
}
}
I am trying to convert my View based code to Compose. I have a composable which takes an image (Painter) as argument and displays it using Image composable. What I want is that whenever the argument value changes, my Image should do a 360 degree rotation and the image should change while angle is approx. 180 degree (i.e. mid-way in the animation)
This is the composable I made.
#Composable
fun MyImage(displayImage: Painter) {
Image(
painter = displayImage,
contentDescription = null,
modifier = Modifier
.size(36.dp)
.clip(CircleShape)
)
}
Right now when the displayImage changes, the new image is displayed immediately without any animation (obviously). How can I achieve the desired animation?
The code that I am trying to convert looks like this:
fun onImageChange(imageRes: Int) {
ObjectAnimator.ofFloat(imageView, View.ROTATION, 0f, 360f)
.apply {
addUpdateListener {
if (animatedFraction == 0.5f) {
imageView.setImageResource(imageRes)
}
}
start()
}
}
It can be done using Animatable.
Compose animations are based on coroutines, so you can wait for the animateTo suspend function to complete, change the image and run another animation. Here's a basic example:
var flag by remember { mutableStateOf(true) }
val resourceId = remember(flag) { if (flag) R.drawable.profile else R.drawable.profile_inverted }
val rotation = remember { Animatable(0f) }
val scope = rememberCoroutineScope()
Column(Modifier.padding(30.dp)) {
Button(onClick = {
scope.launch {
rotation.animateTo(
targetValue = 180f,
animationSpec = tween(1000, easing = LinearEasing)
)
flag = !flag
rotation.animateTo(
targetValue = 360f,
animationSpec = tween(1000, easing = LinearEasing)
)
rotation.snapTo(0f)
}
}) {
Text("Rotate")
}
Image(
painterResource(id = resourceId),
contentDescription = null,
modifier = Modifier
.size(300.dp)
.rotate(rotation.value)
)
}
Output:
If you want to animate the changing images, you have to put two images in a Box and animate the opacity of both as they rotate using one more Animatable.
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: