How to Reset the state of compose views animating - android

I am building an onboarding fragment that gives users tips for each screen. there are multiple pages of a few lines:
Page 1
this icon does this
that icon does that
Button: Next
Page 2
this icon does this
that icon does that
Button: Finish
I want each view on the page to fade in progressively down the screen.
Then with a new page i want the items to reset and all fade in again from the top down.
I have tried using AnimatedVisibility but the problem is that elements keep their state so the fade effect doesnt play on the second page. Possibly AnimatedVisibility isnt the right choice for what i want to do?
So this is the code for a line. I want to reset the state object on each recomposition.
Or if someone has a better suggetion on how to do it - then that would also be excellent.
#Composable
private fun Line(line: ActionResources, modifier: Modifier) {
val state = remember {
MutableTransitionState(false).apply {
// Start the animation immediately.
targetState = true
}
}
AnimatedVisibility(state,
enter = fadeIn(animationSpec = tween(durationMillis = 1000)),
exit = fadeOut(animationSpec = tween(durationMillis = 1000))
) {
Row(
modifier = modifier
.padding(16.dp)
) {
val color = line.color?.let { colorResource(it) } ?: MaterialTheme.colors.onSurface
line.icon?.also {
Icon(
painter = painterResource(it),
tint = color,
contentDescription = stringResource(id = R.string.menu_search),
modifier = Modifier.padding(end = 8.dp).size(24.dp)
)
}
line.label?.also {
Text(
modifier = Modifier,
style = MaterialTheme.typography.body1,
color = color,
text = it
)
}
}
}
}

I can't compile your code especially ActionResources, but based on this
I want to reset the state object on each recomposition.
I can only suggest supplying a key to your remember {...} using the the line parameter.
val state = remember (line) {
MutableTransitionState(false).apply {
// Start the animation immediately.
targetState = true
}
}
I'm not sure if this would solve your problem, but if you want this state to be re-calculated assuming the line parameter will always be different on succeeding re-compositions, then using it as remember {...}'s key will guarantee a re-calculation.

Related

Color is not changed correctly after state change

So I'm trying to do a simple app that changes the color to red or green and goes back to black if a price fluctuates, my currently implementation is this
#Composable
fun LaunchingComposable() {
var coinPrice by remember {
mutableStateOf(2000.30)
}
CoinHeader(modifier = Modifier.padding(horizontal = 8.dp),
"http://myicon.com/image.png",
coinPrice
)
LaunchedEffect(Unit) {
delay(3000)
coinPrice = 2000.40
delay(3000)
coinPrice = 2000.20
delay(3000)
coinPrice = 2000.10
delay(3000)
coinPrice = 2000.20
}
}
...
#Composable
fun CoinHeader(modifier: Modifier, coinImageUrl: String, currentPrice: Double) {
val baseColor = remember { Animatable(Black) }
val previousPrice = remember {
currentPrice
}
Row(
modifier = modifier,
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
AsyncImage(
modifier = Modifier
.width(30.dp)
.height(30.dp)
.padding(end = 4.dp)
.data(coinImageUrl)
.build()
)
LaunchedEffect(currentPrice) {
if (previousPrice < currentPrice) {
baseColor.animateTo(Red, animationSpec = tween(1000))
} else {
baseColor.animateTo(Green, animationSpec = tween(1000))
}
baseColor.animateTo(Black, animationSpec = tween(1000))
}
Text(text = currentPrice.toPrice(), color = baseColor.targetValue)
}
}
In my last composable I'm expecting the values to change to Green - Red - Red - Green
I need to always store previous value of my coinPrice in order to compare it, do a fade animation with the color and then come back to the black color.
Currently this is my output
The problems are 2
Fade in - out color from red to black or green to black not happening
Seems like it always compare with the first value
Can anyone explain to me why if I recompose after coinPrice has been changed, the value of previous is not set correctly ?
Your current implementation of previousPrice is really just original price because it is never changed. You never set it to a new value, so it forever holds the first currentPrice ever received. Your remember call doesn't even have a key, so it will never recompute it, but even if you did, there would be no way to compute it to be the previous value instead of the current one.
I think you will have to use a mutable wrapper class around the remembered previous price so you can actually change it. An array may suffice, or you could write a specific data class to wrap a var.
Something like this:
val rememberedPreviousPrice = remember { arrayOf(currentPrice) }
val previousPrice = rememberedPreviousPrice[0]
rememberedPreviousPrice[0] = currentPrice
Secondly, you're using baseColor.targetValue instead of baseColor.value, so it's not using the animated value, but jumping right to the final ("target") color.
There could be other problems in your code. I'm not sure because I haven't done much with LaunchedEffects or animations in Compose yet myself.
By the way, you should not use Double for currency. Use BigDecimal instead. Read here and here.

TabRow/Tab Recomposition Issue in Compose Accompanist Pager

I was trying to create a sample Tab View in Jetpack compose, so the structure will be like
Inside a Parent TabRow we are iterating the tab title and create Tab composable.
More precise code will be like this.
#OptIn(ExperimentalPagerApi::class)
#Composable
private fun MainApp() {
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.app_name)) },
backgroundColor = MaterialTheme.colors.surface
)
},
modifier = Modifier.fillMaxSize()
) { padding ->
Column(Modifier.fillMaxSize().padding(padding)) {
val pagerState = rememberPagerState()
val coroutineScope = rememberCoroutineScope()
val tabContents = listOf(
"Home" to Icons.Filled.Home,
"Search" to Icons.Filled.Search,
"Settings" to Icons.Filled.Settings
)
HorizontalPager(
count = tabContents.size,
state = pagerState,
contentPadding = PaddingValues(horizontal = 32.dp),
modifier = Modifier
.weight(1f)
.fillMaxWidth()
) { page ->
PagerSampleItem(
page = page
)
}
TabRow(
selectedTabIndex = pagerState.currentPage,
backgroundColor = MaterialTheme.colors.surface,
contentColor = MaterialTheme.colors.onSurface,
indicator = { tabPositions ->
TabRowDefaults.Indicator(
Modifier
.pagerTabIndicatorOffset(pagerState, tabPositions)
.height(4.dp)
.background(
color = Color.Green,
shape = RectangleShape
)
)
}
) {
tabContents.forEachIndexed { index, pair: Pair<String, ImageVector> ->
Tab(
selected = pagerState.currentPage == index,
selectedContentColor = Color.Green,
unselectedContentColor = Color.Gray,
onClick = {
coroutineScope.launch {
pagerState.animateScrollToPage(index)
}
},
text = { Text(text = pair.first) },
icon = { Icon(imageVector = pair.second, contentDescription = null) }
)
}
}
}
}
}
#Composable
internal fun PagerSampleItem(
page: Int
) {
// Displays the page index
Text(
text = page.toString(),
modifier = Modifier
.padding(16.dp)
.background(MaterialTheme.colors.surface, RoundedCornerShape(4.dp))
.sizeIn(minWidth = 40.dp, minHeight = 40.dp)
.padding(8.dp)
.wrapContentSize(Alignment.Center)
)
}
And coming to my question is whenever we click on the tab item, the inner content get recompose so weirdly. Im not able to understand why it is happens.
Am attaching an image of the recomposition counts below, please take a look that too, it would be good if you guys can help me more for understand this, also for future developers.
There are two question we have to resolve in this stage
Whether it will create any performance issue, when the view getting more complex
How to resolve this recompostion issue
Thanks alot.
… whenever we click on the tab item, the
inner content get recompose so weirdly. Im not able to understand why
it is happens...
It's hard to determine what this "weirdness" is, there could be something inside the composable your'e mentioning here.
You also didn't specify what the API is, so I copied and pasted your code and integrated accompanist view pager, then I was able to run it though not on an Android Studio with a re-composition count feature.
And since your'e only concerned about the Text and the Icon parameter of the API, I think that's something out of your control. I suspect the reason why your'e getting those number of re-composition count is because your'e animating the page switching.
coroutineScope.launch {
pagerState.animateScrollToPage(index)
}
Though 'm not able to try this on another Android Studio version with the re-composition feature, I think (though I'm not sure) scrolling to another page without animation will yield less re-composition count.
coroutineScope.launch {
pagerState.scrollToPage(index)
}
If it still bothers you, the best course of action is to ask them directly, though personally I wouldn't concerned much about this as they are part of an accepted API and its just Text and Icon being re-composed many times by an animation which is also fine IMO.
Now if you have some concerns about your PagerSampleItem stability(which you have a full control), based on the provided code and screenshot, I think your'e fine.
There's actually a feature suggested from this article to check the stability of a composable, I run it and I got this report.
restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun PagerSampleItem(
stable page: Int
)
Everything about this report is within the article I linked.
Also, your Text and Icon are using String and ImageVector which is stable and immutable (marked by #Immutable) respectively.
So TLDR, IMO your code is fine, your PagerSampleItem is not re-composing in the screenshot.

Compose : advanceTimeBy doesn't work with animation

I've two Boxes and one Button. Clicking on the Button would toggle a flag and it triggers an AnimatedVisibility animation on these Boxes.
Code
#Composable
fun TestBox() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
var flag by remember { mutableStateOf(false) }
AnimatedVisibility(
visible = !flag,
enter = slideInHorizontally(animationSpec = tween(3000)) { it },
exit = slideOutHorizontally(animationSpec = tween(3000)) { -it }
) {
// Red box
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Red)
.testTag("red_box"),
) {}
}
AnimatedVisibility(
visible = flag,
enter = slideInHorizontally(animationSpec = tween(3000)) { it },
exit = slideOutHorizontally(animationSpec = tween(3000)) { -it }
) {
// Red box
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Green)
.testTag("green_box"),
) {}
}
Button(onClick = { flag = !flag }) {
Text(text = "TOGGLE")
}
}
}
Output
Now I want to write a test to check if two boxes are visible in the middle of the transition. So I wrote a test like this
class BoxAnimationTest {
#get:Rule
val composeRule = createComposeRule()
#Before
fun beforeAll() {
composeRule.setContent {
TestBox()
}
}
#Test
fun firstTest() {
with(composeRule) {
mainClock.autoAdvance = false
onNodeWithTag("red_box").assertExists() // red box visible
onNodeWithTag("green_box").assertDoesNotExist() // green box shouldn't be visible
onNodeWithText("TOGGLE").performClick() // clicking toggle button
mainClock.advanceTimeBy(1500) // and advance time to half of total duration (3000ms)
onNodeWithTag("green_box").assertExists() // now both green and
onNodeWithTag("red_box").assertExists() // [FAILED] red should be visible
mainClock.advanceTimeBy(1500) // finishing the animation
onNodeWithTag("green_box") // now green should be visible
onNodeWithTag("red_box").assertDoesNotExist() // but red shouldn't be
}
}
}
But it fails at onNodeWithTag("red_box").assertExists() (2nd).
java.lang.AssertionError: Failed: assertExists.
Reason: Expected exactly '1' node but could not find any node that satisfies: (TestTag = 'red_box')
Any idea why?
Some initial investigation showed that the animations from
AnimatedVisibility were never added to the Transition, because
measure() was not called and slide animation is initialized during
measure. As a result, we ended up having an empty Transition that
finished right away. That's why the test failed. Note that adding
advanceTimeByFrame() before advanceTimeBy(1500) seems to allow the
test to pass. This might be useful for narrowing down the cause.
Source - KotlingLang Slack : https://kotlinlang.slack.com/archives/CJLTWPH7S/p1644036648038349?thread_ts=1643956469.612919&cid=CJLTWPH7S
Issue Tracker : https://issuetracker.google.com/issues/217880227

Jetpack Compose animation finished listener not called

I'm struggling to use Jetpack Compose Animation to achieve a (supposedly simple) effect:
in the case of an error, a control's background color should flash red, and after a short delay then fade back to normal (transparent).
My current approach is to model this with a boolean state shouldFlash which is set to true when an error occurs, and is set back to false when the animation completes. Unfortunately it seems the finishedListener passed to animateColorAsState is never called. I attached a debugger and also added a log statement to verify this.
What am I doing wrong?
Sample (button triggers error):
#Composable
fun FlashingBackground() {
Column(modifier = Modifier.size(200.dp)) {
var shouldFlash by remember { mutableStateOf(false) }
var text by remember { mutableStateOf("Initial") }
FlashingText(flashing = shouldFlash, text = text) {
shouldFlash = false
text = "Animation done"
}
Button(onClick = {
shouldFlash = true
}) {
Text(text = "Flash")
}
}
}
#Composable
fun FlashingText(flashing: Boolean, text: String, flashFinished: () -> Unit) {
if (flashing) {
val flashColor by animateColorAsState(
targetValue = Color.Red,
finishedListener = { _ -> flashFinished() }
)
Text(text = text, color = Color.White, modifier = Modifier.background(flashColor))
} else {
Text(text = text)
}
}
Compose version: 1.0.5 (latest stable at time of writing), also in 1.1.0-beta02
Anyway, to understand this, you need to know how the animateColorAsState works internally.
It relies on recompositions - is the core idea.
Every time a color changes, a recomposition is triggered, which results in the updated color value being reflected on the screen. Now, what you are doing is just using conditional statements to DISPLAY DIFFERENT COMPOSABLES. Now, one Composable is actually referring to the animating value, that is, the one inside your if block (when flashing is true). On the other hand, the else block Composable is just a regular text which does not reference it. That is why you need to remove the conditional. Anyway, because after removing the conditional, what remains is only a single text, I thought it would be a waste to create a whole new Composable out of it, which is why I removed that Composable altogether and pasted the Text inside your main Composable. It helps to keep things simpler enough. Other than this, the answer by #Rafiul does work, but there is not really a need for a Composable like that, so I would still recommend using this answer instead, so that the code is easier to read.
ORIGINAL ANSWER:
Try moving the animator outside the Child Composable
#Composable
fun FlashingBackground() {
Column(modifier = Modifier.size(200.dp)) {
var shouldFlash by remember { mutableStateOf(false) }
var text by remember { mutableStateOf("Initial") }
val flashFinished: (Color) -> Unit = {
shouldFlash = false
text = "Animation done"
}
val flashColor by animateColorAsState(
targetValue = if (shouldFlash) Color.Red else Color.White,
finishedListener = flashFinished
)
//FlashingText(flashing = shouldFlash, text = text) -> You don't need this
Text(text = text, color = Color.White, modifier = Modifier.background(flashColor))
Button(onClick = {
shouldFlash = true
}) {
Text(text = "Flash")
}
}
}
Change your code like this.
FlashingBackground
#Composable
fun FlashingBackground() {
Column(modifier = Modifier.size(200.dp)) {
var shouldFlash by remember { mutableStateOf(false) }
var text by remember { mutableStateOf("Initial") }
FlashingText(flashing = shouldFlash, text = text) {
shouldFlash = false
text = "Animation done"
}
Button(onClick = {
shouldFlash = true
}) {
Text(text = "Flash")
}
}
}
FlashingText
#Composable
fun FlashingText(flashing: Boolean, text: String, flashFinished: () -> Unit) {
val flashColor by animateColorAsState(
targetValue = if(flashing) Color.Red else Color.White,
finishedListener = { _ -> flashFinished() }
)
Text(text = text, color = Color.White, modifier = Modifier.background(flashColor))
}
Edited:
The problem with your code is you are initializing animateColorAsState when you are clicking the Flash button and making shouldFlash = true. So for the first time, it just initializes the animateColorAsState, doesn't run the animation. So there will be no finishedListener call as well. Since finishedListener isn't executed, shouldFlash stays to true. So from the next call shouldFlash is already true there will be no state change. That's why from the subsequent button click, it doesn't recompose the FlashingText anymore. You can put some log in your method you won't see FlashingText after the first button click.
Keep in mind: targetValue = Color.Red will not do anything. target value should be either a state or like this condition if(flashing) Color.Red because you need to change the state to start the animation.
#Phillip's answer is also right. But I don't see any extra advantage in moving the animator outside the Child Composable if you use it like the above.

dynamically change textDecoration on clickableText android compose

I have a large number of texts in a row, and I would like to make every one of them change text decoration on press
(so the user can notice which text/tag is already selected)
(unselected: TextDecoration.None, selected: TextDecoration: Underlined)
(user can press selected text to unselect it)
var tagsSelected = mutableListOf<String>()
...
Text(text = "tech",
Modifier.clickable {
if (tagsSelected.contains("tech")) {
tagsSelected.remove("tech")
// RemoveTextDecoration ?
} else {
tagsSelected.add("tech")
// AddTextDecoration ?
}
}.padding(5.dp))
...
I've tried using variables (not a good idea cause it would require a lot of them), using an mutable array of boolean values (later observed as states) and none of that has brought results for me,
any amount of help will be appreciated,
thanks :)
You're creating a new mutableListOf on each recomposition. That's why new values are not getting saved. Check out how you should store state in compose.
rememberSaveable will save your state even after screen rotation(unlike remember), and mutableStateListOf is a variation of mutable list which will notify Compose about updates. I you need to save state even when you leave the screen and come back, check out about view models.
Also you can move your add/remove logic into extension so your code will look cleaner:
fun <E> MutableList<E>.addOrRemove(element: E) {
if (!add(element)) {
remove(element)
}
}
Final variant:
val tagsSelected = rememberSaveable { mutableStateListOf<String>() }
Text(
text = "tech",
modifier = Modifier
.clickable {
tagsSelected.addOrRemove("tech")
}
.padding(5.dp)
)
If you have many Text items which looks the same, you can repeat them using forEach:
val tagsSelected = rememberSaveable { mutableStateListOf<String>() }
val items = listOf(
"tech1",
"tech2",
"tech3"
)
items.forEach { item ->
Text(
text = item,
modifier = Modifier
.clickable {
tagsSelected.addOrRemove(item)
}
.padding(5.dp)
)
}
If you need to use selection state only to change text decoration, you can easily move it to an other composable and create a local variable:
#Composable
fun ClickableDecorationText(
text: String,
) {
var selected by rememberSaveable { mutableStateOf(false) }
Text(
text = text,
textDecoration = if(selected) TextDecoration.Underline else TextDecoration.None,
modifier = Modifier
.clickable {
selected = !selected
}
.padding(5.dp)
)
}

Categories

Resources