Jetpack Compose animateFLoatAsState replaced by Transition.animateFloat is not working - android

Could you tell me why it stops working when I use val animateAngle: Float by transition.animateFloat instead of val animateAngle by animateFloatAsState?
It seems that the variable rotated is not updated when the button is clicked.
var rotated by remember {
mutableStateOf(false)
}
val transition = updateTransition(
targetState = rotated,
label = "Rotate"
)
val animateAngle: Float by transition.animateFloat(
transitionSpec = {
tween(2000)
},
label = "Rotate box"
) { state ->
when(state){
rotated -> 360f
else -> 0f
}
}
Column (
) {
Image(
painter = painterResource(id = R.drawable.propeller),
contentDescription = "fan",
modifier = Modifier
.rotate(animateAngle)
.padding(30.dp)
.size(100.dp))
Button(
onClick = {
rotated = !rotated
},
) {
Text(text = "Rotate Box")
}
}

You have a boolean mutableState that you use as a targetState for your transition, true or false will be its target state.
val transition = updateTransition(
targetState = rotated,
label = "Rotate"
)
Now you are reading that boolean mutableState inside targetValueByState lambda
when(state) {
rotated -> 360f
else -> 0f
}
and because you are reading it there, the animationAngle will always get a value of 360f, regardless if its true or false.
Logging it,
Log.e("AnimatedAngle", "$animateAngle : Rotated boolean state [$rotated]")
prints:
360.0 : Rotated boolean state [false]
360.0 : Rotated boolean state [true]
Just simply remove it and slightly modify the targetValueByState block this way, and your animation will work.
val animateAngle: Float by transition.animateFloat(
transitionSpec = {
tween(2000)
},
label = "Rotate box"
) { state ->
if (state) 360f else 0f
}

Related

Jetpack Compose animation performance issue

I'm new to Jetpack Compose, and I'm trying to rotate the home screen with animation when the menu button is tapped. It works fine 3-5 times, but suddenly it lags like crazy and I don't know why? Is this a bug, or am I doing something wrong?
var isOpen by remember { mutableStateOf(false) }
val transition = updateTransition(targetState = isOpen, "Menu")
val rotation by transition.animateFloat(
transitionSpec = { spring(0.4f, Spring.StiffnessLow) },
label = "MenuRotation",
targetValueByState = { if (it) -30f else 0f }
)
val scale by transition.animateFloat(
label = "MenuScale",
targetValueByState = { if (it) 0.9f else 1f }
)
val translateX by transition.animateFloat(
transitionSpec = { tween(400) },
label = "MenuTranslation",
targetValueByState = { if (it) 536f else 0f }
)
Box(
modifier = Modifier
.fillMaxSize()
.graphicsLayer {
cameraDistance = density * 10f
rotationY = rotation
scaleX = scale
translationX = translateX
}
) {
HomeScreen()
}
Box {
DefaultButton(
onClick = { isOpen = ! isOpen },
modifier = Modifier
.padding(16.dp)
.padding(top = 32.dp)
.shadow(blur = 8.dp, radius = 16.dp)
.size(32.dp, 32.dp),
shape = Shapes.large,
) {
Icon(
imageVector = if (isOpen) Icons.Filled.Close else Icons.Filled.Menu,
contentDescription = "Menu",
tint = Color.Black,
)
}
}
Update #1
I found this in the logcat
Skipped 52 frames! The application may be doing too much work on its main thread.
Davey! duration=1067ms; Flags=0, FrameTimelineVsyncId=2511155, IntendedVsync=4294436921331, Vsync=4294870254647, InputEventId=0, HandleInputStart=4294871069349, AnimationStart=4294871070287, PerformTraversalsStart=4294871939089, DrawStart=4294872039558, FrameDeadline=4294486921330, FrameInterval=4294870971954, FrameStartTime=41666666, SyncQueued=4294872645860, SyncStart=4295312217578, IssueDrawCommandsStart=4295312304089, SwapBuffers=4295937520703, FrameCompleted=4295944298047, DequeueBufferDuration=5729, QueueBufferDuration=166719, GpuCompleted=4295944298047, SwapBuffersCompleted=4295937862943, DisplayPresentTime=4237530536663, CommandSubmissionCompleted=4295937520703,
Update #2
The animation works flawlessly when I comment out all text components and vice versa. So what's wrong with the text components?
Please check the HomeScreen composable, component recomposition counts。
I suspect that HomeScreen reorganizes too many times。
You can just replace #Composable HomeScreen with an #Composeable Image verify。

What is the behavior of Jetpack Compose animations?

In my android project, I'm doing a simple Floating Action Button that can expand and show a list of buttons to perform different actions.
To track the current state of the FAB, I have the next enum class
enum class FabState {
Expanded,
Collapsed
}
For displaying the Floating Action Button, I have the following Composable function:
#Composable
fun MultiFloatingActionButton(
icon: ImageVector,
iconTint: Color = Color.White,
miniFabItems: List<MinFabItem>,
fabState: FabState, //The initial state of the FAB
onFabStateChanged: (FabState) -> Unit,
onItemClick: (MinFabItem) -> Unit
) {
val transition = updateTransition(targetState = fabState, label = "transition")
val rotate by transition.animateFloat(label = "rotate") {
when (it) {
FabState.Collapsed -> 0f
FabState.Expanded -> 315f
}
}
val fabScale by transition.animateFloat(label = "fabScale") {
when (it) {
FabState.Collapsed -> 0f
FabState.Expanded -> 1f
}
}
val alpha by transition.animateFloat(label = "alpha") {
when (it) {
FabState.Collapsed -> 0f
FabState.Expanded -> 1f
}
}
val shadow by transition.animateDp(label = "shadow", transitionSpec = { tween(50) }) { state ->
when (state) {
FabState.Expanded -> 2.dp
FabState.Collapsed -> 0.dp
}
}
Column(
horizontalAlignment = Alignment.End
) { // This is where I have my question, in the if condition
if (fabState == FabState.Expanded || transition.currentState == FabState.Expanded) {
miniFabItems.forEach { minFabItem ->
MinFab( //Composable for creating sub action buttons
fabItem = minFabItem,
alpha = alpha,
textShadow = shadow,
fabScale = fabScale,
onMinFabItemClick = {
onItemClick(minFabItem)
}
)
Spacer(modifier = Modifier.size(16.dp))
}
}
FloatingActionButton(
onClick = {
onFabStateChanged(
when (fabState) {
FabState.Expanded -> {
FabState.Collapsed
}
FabState.Collapsed -> {
FabState.Expanded
}
}
)
}
) {
Icon(
imageVector = icon,
tint = iconTint,
contentDescription = null,
modifier = Modifier.rotate(rotate)
)
}
}
}
The constants I defined are for animating the buttons that will show/hide depending on the FAB state.
When I first made the function, the original condition was giving me a different behavior, and playing around with all the posible conditions, I got 3 different results:
1st condition:
if (transition.currentState == FabState.Expanded) {...}
Result: animation not loading from collapsed to expanded, but it does from expanded to collapsed
2nd condition: if (fabState == FabState.Expanded) {...}
Result: animation loading from collapsed to expanded, but not from expanded to collapsed
3rd condition (the one I'm using right now):
if (fabState == FabState.Expanded || transition.currentState == FabState.Expanded) {...}
Result: animation loading in both ways
So my question is: how does every condition change the behavior of the animations?
Any help would be appreciated. Thanks in advance
fabState is updated as soon as onFabStateChanged is called and transition.currentState is updated when it ends the transition and transition.isRunning returns false
Animation only happens if the composable is present in the tree. When the condition is false in the if block, the elements are not available for animation.
condition 1 false during the enter perion which breaks the enter animation and condition 2 is false during the exit period which breaks the exit animation and both are false after exit. Therefore merging them solved your issue and also removes the composables from the tree when not wanted.
Better approach
AnimatedVisibility(
visible = fabState == FabState.Expanded,
enter = fadeIn()+ scaleIn(),
exit = fadeOut() + scaleOut(),
) {
miniFabItems.forEach { minFabItem ->
MinFab(
fabItem = minFabItem,
textShadow = 0.dp,
onMinFabItemClick = {
onItemClick(minFabItem)
}
)
Spacer(modifier = Modifier.size(16.dp))
}
}
And use graphicsLayer modifier to instead of rotate
Icon(
imageVector = Icons.Default.Add,
tint = Color.White,
contentDescription = null,
modifier = Modifier
.graphicsLayer {
this.rotationZ = rotate
}
)

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.

Restore SwipeToDismiss LazyColumn Item to it's original state?

I am able to do SwipeToDismiss but I want to restore the swiped LazyColumn item back to its original state.
I don't want to remove swiped item but want to restore it to its original state.
I am able to achieve this easily in RecyclerView by just calling notifyItemChanged() but can't figure out how to do this in LazyColumn.
Below is my code:
val dataList = remember{ mutableStateListOf<ListItem>()}
for(i in 0..100){
dataList.add(ListItem("$i", "'$i' is the item number."))
}
LazyColumn(Modifier.fillMaxSize()){
items(dataList, key = {it.id}){ item ->
val dismissState = rememberDismissState(
confirmStateChange = {
if(it == DismissedToEnd || it == DismissedToStart){
Handler(Looper.getMainLooper()).postDelayed({
//dataList.remove(item)
}, 1000)
}
true
}
)
SwipeToDismiss(
state = dismissState,
directions = setOf(StartToEnd, EndToStart),
dismissThresholds = { direction ->
FractionalThreshold(if (direction == StartToEnd || direction == EndToStart) 0.25f else 0.5f)
},
background = {
val direction = dismissState.dismissDirection ?: return#SwipeToDismiss
val color by animateColorAsState(
targetValue = when(dismissState.targetValue){
Default -> Color.LightGray
DismissedToEnd -> Color.Green
DismissedToStart -> Color.Red
}
)
val icon = when(direction){
StartToEnd -> Icons.Default.Done
EndToStart -> Icons.Default.Delete
}
val scale by animateFloatAsState(
if (dismissState.targetValue == Default) 0.8f else 1.2f
)
val alignment = when (direction) {
StartToEnd -> Alignment.CenterStart
EndToStart -> Alignment.CenterEnd
}
Box(modifier = Modifier
.fillMaxSize()
.background(color)
.padding(start = 12.dp, end = 12.dp),
contentAlignment = alignment
){
Icon(icon, contentDescription = "Icon", modifier = Modifier.scale(scale))
}
},
dismissContent = {ItemScreen(dismissState = dismissState, item = item)}
)
}
}
You can wait currentValue to become non Default and reset the state:
According to Thinking in Compose, composable function should be free of side effects - you shouldn't directly reset the state in the composable scope. For such situations you need to use one of special side effect functions, more info can be found in side-effects documentation.
Recomposition can happen many times, up to once a frame during animation, and not using side effect functions will lead to multiple calls, which can cause animation problems.
As DismissState.reset() is a suspend function, LaunchedEffect fits perfectly here: it's already running on a coroutine scope.
if (dismissState.currentValue != DismissValue.Default) {
LaunchedEffect(Unit) {
dismissState.reset()
}
}

Long press gesture in Jetpack Compose

How to get this content size reduce animation with jetpack compose when long pressed and then get normal when released(like the card in Spotify Android App).
Here is a gif showing the animation.
You can use a Transition to manage the animations between the pressed and release states.
enum class ComponentState { Pressed, Released }
var toState by remember { mutableStateOf(ComponentState.Released) }
val transition: Transition<ComponentState> = updateTransition(targetState = toState, label = "")
// Defines a float animation to scale x,y
val scalex: Float by transition.animateFloat(
transitionSpec = { spring(stiffness = 50f) }, label = ""
) { state ->
if (state == ComponentState.Pressed) 0.90f else 1f
}
val scaley: Float by transition.animateFloat(
transitionSpec = { spring(stiffness = 50f) }, label = ""
) { state ->
if (state == ComponentState.Pressed) 0.90f else 1f
}
Then you can use the PointerInputScope.detectTapGestures to detect the press gestures:
val modifier = Modifier.pointerInput(Unit) {
detectTapGestures(
onPress = {
toState = ComponentState.Pressed
tryAwaitRelease()
toState = ComponentState.Released
}
)
}
Finally apply the animation to your Composable.
For example:
Box(
modifier
.width((100 * scalex).dp)
.height((100 * scaley).dp),
contentAlignment = Alignment.Center) {
Image(
//...
modifier = Modifier.graphicsLayer{
scaleX = scalex;
scaleY = scaley
})
}

Categories

Resources