Question
How can I achieve something like a wiggle animation with jetpack compose when a boolean is changing?
From my understanding the library only supports transition animations or infinite animations. However in my case no composable object is actually changing their target value in anyway. In the examples that I found one always needs to change the target value, to actually see an animation. Like this
var isError by remember { mutableStateOf(false) }
val offset = animateIntAsState(if (isError) $targetValue else 0)
// Then have something that uses the offset and add a button that is changing the state of isError
However I don't want the targetValue to be different from what it initially was. I just want to see a keyframes animation for example.
To achieve this you can use the finishedListener parameter that comes with all animateXAsState functions to switch the state which triggers the animation.
Example below which adds one line to your code snippet:
var animationState by remember { mutableStateOf(false) }
val xOffset: Dp by animateDpAsState(
targetValue = if (animationState) 16.dp else 0.dp,
finishedListener = { animationState = false } // Add this
)
Related
I have a LazyColumn containing ToggleButtonGroups that I have created myself. My issue can be reproduced by these three steps:
Select "YES" on every ToggleButtonGroup
Rotate the screen and then scroll to the bottom
Rotate the screen back. Now, the topmost three to four ToggleButtonGroups are reset.
The problem does not appear if I don't scroll after rotating. So by rotation alone, the state is saved properly, as I would have expected it by using rememberSaveable.
The code is provided below:
LazyColumn() {
items(characteristics) {characteristic: Characteristic ->
ToggleButtonGroup(
defaultIndex = 2,
values = listOf(Pair("YES", -1), Pair("NO", 1), Pair("TBD", 0)),
onToggleChange = { newValue: Int ->
characteristic.value = newValue
}
)
}
}
The Composable named ToggleButtonGroup is seen below:
#Composable
fun ToggleButtonGroup(defaultIndex: Int, values: List<Pair<String, Int>>, onToggleChange: (newValue: Int) -> Unit) {
var selectedIndex by rememberSaveable {
mutableStateOf( defaultIndex.coerceIn(0, values.size - 1) )
}
Row(
modifier = Modifier.wrapContentHeight()
) {
values.forEachIndexed { index: Int, value: Pair<String, Int> ->
FilledTonalButton(
colors = if (selectedIndex == index) ButtonDefaults.buttonColors() else ButtonDefaults.filledTonalButtonColors(),
onClick = { selectedIndex = index; onToggleChange(value.second) },
shape = RectangleShape
) {
Text(text = value.first)
}
}
}
}
And the characteristics data is coming from my ViewModel:
data class Characteristic(val title: String, val weight: Int, var value: Int)
var characteristics by mutableStateOf(listOf<Characteristic>())
Thank you for any efforts!
It seems like this is an intended behaviour. There is an issue open on the Google Issue Tracker which describes a similar problem. The issue was marked as "intended behaviour", as this mechanism of releasing the state of non-visible items at rotation was introduced with this commit:
Save the states only for the currently visible items of lazy layouts
When we save the state of the screen in cases like navigation to other screen or activity rotation we were saving the state for all items which were ever visible in the lazy layouts like LazyColumn. We want to save state while user is scrolling so they not lose it when they scroll back, however when this screen is not active anymore keeping the states for all items is not efficient as we easily can fill up the whole available limit provided by the Bundle. Instead it could be reasonable to only save states for the items which were visible when the screen state save happens.
I am trying to add scrolling behaviour to a Column by setting verticalScroll(state = rememberScrollState()) modifier.
I checked out some examples in the official compose-jb repository, and it seems that that is the right way to do it, yet in my case the content is not scrollable.
Here the full code:
#Composable
#Preview
fun App() {
MaterialTheme {
// add scroll behaviour
val stateVertical = rememberScrollState(0)
Column(modifier = Modifier.verticalScroll(state = stateVertical)) {
repeat(100){
Text("item: $it")
}
}
}
}
fun main() = application {
Window(onCloseRequest = ::exitApplication) {
App()
}
}
Any ideas why it does not work in my case?
The Column is populated with 100 Text items, more than enough to exceed the default window height.
It actually works!
For some reason I was trying to use click and drag... which lead me to confusion.
I tried to develop a simple custom layout just like the documentation
#Composable
fun MyBasicColumn(
modifier: Modifier = Modifier,
content: #Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// Don't constrain child views further, measure them with given constraints
// List of measured children
val placeables = measurables.map { measurable ->
// Measure each children
measurable.measure(constraints)
}
// Set the size of the layout as big as it can
layout(constraints.maxWidth, constraints.maxHeight) {
// Track the y co-ord we have placed children up to
var yPosition = 0
// Place children in the parent layout
placeables.forEach { placeable ->
// Position item on the screen
placeable.placeRelative(x = 0, y = yPosition)
// Record the y co-ord placed up to
yPosition += placeable.height
}
}
}
}
it works fine when I know exact number of items
but what about lazy items?
there is nothing in documentation about how can I develop a LazyCustomLayout
You don't exactly have to know how many items are in the Layout, since even for dynamic lists, there's always a 'current number of items' which can be computed. Let's say you download a list of texts from a server, and then intend to use this Layout to render those. Even in that case, while the server may vary the length of the list, i.e., the list is dynamic in size, you would presumably have a LiveData object keeping track of the list items. From there, you can easily use the collectAsState() method inside a Composable, or the observeAsState() method tied to a LifecycleOwner to convert it into the Compose-compatible MutableState<T> variable. Hence, whenever the LiveData notifies a new value (addition, or deletion), the MutableState<T> variable will also be updated to reflect those values. This, you can use inside the said Layout, which is also a Composable and hence, will update along-side the server-values, in real-time.
The thing is, no matter how you get your list, in order to show it on-screen, or use it anywhere in your app, you would always have a list object, which can be exploited using Compose's declarative and reactive nature.
I'm probably missing something very obvious, but anyway. Say, I want to animate a background color like this: Grey -> Red -> Grey. However the function animateColorAsState does not seem to allow such a transition. What is the canonical way in Android Compose to make it happen?
Here's how you can do it with Animatable:
val color = remember { Animatable(Color.Gray) }
LaunchedEffect(Unit) {
color.animateTo(Color.Red, animationSpec = tween(1000))
color.animateTo(Color.Gray, animationSpec = tween(1000))
}
Box(Modifier.fillMaxSize().background(color.value))
I'm building an expandable Composable which would be expanded when clicked.
This would be implemented by using the AnimatedVisibility which works perfectly.
Code for the visibility animation:
AnimatedVisibility(
visible = isExpanded,
) {
// Content removed.
}
The problem I'm currently facing is that this is located in a vertical scrollable column and it should scroll to the expanded content when clicked next to expanding it.
As I read this would be done by using the BringIntoViewRequester as in the code snippet below:
var isExpanded by remember { mutableStateOf(false) }
val intoViewRequester = remember { BringIntoViewRequester() }
ClickableComposable(modifier = Modifier.clickable {
isExpanded = !isExpanded
if(isExpanded) {
coroutineScope.launch {
// delay(200)
intoViewRequester.bringIntoView(rect = null)
}
}
})
AnimatedVisibility(
modifier = Modifier.bringIntoViewRequester(intoViewRequester),
visible = isExpanded,
) {
// Content removed.
}
The code above works with the delay but that's not a perfect interaction for the user. To first see the content expanding and afterwards see the page scroll. The ideal situation would be that it would happen at the same time, however the content is not yet measured in any way. By removing the delay it does not work as the content is not yet visible.
Is there anything in Compose to do the expanding and scrolling to at the same time?