Persistant dynamick color - android

I'm trying to create a clickable card that changed its color after it is clicked.
I have (code relevant to the question is documented as "This is relevant to the question") :
val selectedCardColor by remember {
mutableStateOf(Color(0xFF00FFFF))
}
val unselectedCardColor by remember {
mutableStateOf(Color(0xAA007777))
}
// This is relevant to the question
var hmsToDecColor by remember {
mutableStateOf(selectedCardColor)
}
// This is relevant to the question
var decToHmsColor by remember {
mutableStateOf(unselectedCardColor)
}
...
Card(
modifier = Modifier
.weight(0.5f)
.padding(1.dp)
.clickable {
// This is relevant to the question
hmsToDecColor = unselectedCardColor
decToHmsColor = selectedCardColor
onClick(true)
},
border = BorderStroke(1.dp, Color.Black),
elevation = 4.dp
) {
Text(
text = str1,
textAlign = TextAlign.Center,
color = MaterialTheme.colors.onSecondary,
modifier = Modifier
.background(hmsToDecColor) // This is relevant to the question
.padding(20.dp)
)
}
(There is another one that toggles the other way).
Everything works as planned. When I hit the card the color changes but... When I rotate the phone the colors are back to the default as if nothing was done.
I can not figure out why the "remember" does not remember...
I understand that everything is recomposed but expected the "remember" to persist.
I assume that the variables do remember but the fresh recomposition ignores the remembered value and uses the coded ones. If this is the case, how do I overcome this?

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.

How to Reset the state of compose views animating

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.

Jetpack Compose - Setting dimensions for Accompanist placeholder

The library I'm using: "com.google.accompanist:accompanist-placeholder-material:0.23.1"
I want to display a placeholder in the place of (or over) a component when it's in the loading state.
I do the following for a Text:
MaterialTheme() {
var placeholderVisible by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
while (true) {
delay(1000)
placeholderVisible = !placeholderVisible
}
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.border(1.dp, Color.Red)
.padding(16.dp)
) {
Text(
modifier = Modifier
.then(
if (placeholderVisible) {
Modifier.height(28.dp).width(62.dp)
} else {
Modifier
}
)
.placeholder(
visible = placeholderVisible,
highlight = PlaceholderHighlight.shimmer()
),
text = if (placeholderVisible) "" else "Hello"
)
}
}
}
And I get this:
I want instead that no matter how big I set the placeholder's height or width, it will not participate in any way in the measuring process and, if I want to, to be able to draw itself even over other components (in this case let's say the red border).
As an effect of what I want, the box with red border will always have the dimension as if that Modifier.height(28.dp).width(62.dp) is not there.
I know I can draw outside a component's borders using drawWithContent, specifying the size of a rectangle or a circle (or whatever) to be component's size + x.dp.toPx() (or something like that). But how do I do this with Modifier.placeholder?
Ideally, I would need something like Modifier.placeholder(height = 28.dp, width = 62.dp)
So, with or without this ideal Modifier, the UI should never change (except, of course, the shimmer box that may be present or not).
I think I can pull this off by modifying the source code of this Modifier, but I hope I won't need to turn to that.
Just replace your Text() with below code, maybe conditional Modifier is the issue in above code!
Text(
modifier = Modifier
.size(width = 62.dp, height = 28.dp)
.placeholder(
visible = placeholderVisible,
highlight = PlaceholderHighlight.shimmer()
),
text = if (placeholderVisible) "" else "Hello",
textAlign = TextAlign.Center
)

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