I have two pages A and B with white and black background colors.
I need to change the status bar color when switching between two pages.
I use rememberSystemUiController to change the status bar color, but there is flickering, jerky, and the transition is not smooth.
Blink effect YouTube
#Composable
fun APager() {
val systemUiController = rememberSystemUiController()
val background = MaterialTheme.colorScheme.background
SideEffect {
systemUiController.setStatusBarColor(background)
}
// ...
}
#Composable
fun BPager() {
val systemUiController = rememberSystemUiController()
val black = MaterialTheme.colorScheme.black
SideEffect {
systemUiController.setStatusBarColor(black)
}
// ...
}
SideEffect launches with every recomposition and your composable is probably recomposing multiple times. Use LauncedEffect instead with a key whose value is set to the currently selected page of the pager. This will prevent launching the effect multiple times. Example:
#OptIn(ExperimentalPagerApi::class)
#Composable
fun MyPager() {
val pagerState = rememberPagerState()
val systemUiController = rememberSystemUiController()
LaunchedEffect(pagerState.currentPage) {
val color = when (pagerState.currentPage) {
0 -> Color.Blue
1 -> Color.Green
2 -> Color.Red
else -> Color.Black
}
systemUiController.setStatusBarColor(color)
}
HorizontalPager(
modifier = Modifier.fillMaxSize(),
count = 10,
state = pagerState,
) { page ->
Text(
text = "Page: $page",
modifier = Modifier.fillMaxWidth()
)
}
}
Flicker due to multiple recombination,Replace SideEffect with LaunchedEffect
val black = MaterialTheme.colorScheme.black
LaunchedEffect(black){
systemUiController.setStatusBarColor(black)
}
but, You can also add a lock
var isChange by remember {
mutableStateOf(true)
}
val black = MaterialTheme.colorScheme.black
SideEffect {
if(isChange){
systemUiController.setStatusBarColor(black).apply {
isChange = false
}
}
}
I think the second method is better, and there is no problem switching pages quickly
Related
I’m trying to add swipe to dismiss to a LazyList.
The default implementation of SwipeToDismiss will lead to accidental horizontal scrolling while trying to scroll vertically leading to accidental swipe to dismiss events.
In vanilla android view world i'd set a touch listener and check against x > y touch event distance before starting swipe to dismiss. However in Jetpack Compose I don't know how.
I tried pointerInput modifier, however it disabled vertical scrolling completely.
I’m aware of dismissThresholds but that does only handle invoking the dismissState change.
#Composable
fun MyList(modifier: Modifier = Modifier, list: MutableState<List<String>>) {
val scrollState: LazyListState = rememberLazyListState()
LazyColumn(
modifier = modifier
.fillMaxWidth()
.background(MaterialTheme.colors.surface),
state = scrollState,
) {
items(
items = list.value,
key = { it },
contentType = { 0 }
) { item ->
MySwipeToDismiss(modifier = modifier, onSwipeLeft = {}, onSwipeRight = {}) {
Text(text = item)
}
}
}
}
}
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun MySwipeToDismiss(modifier: Modifier = Modifier, onSwipeLeft: () -> Unit, onSwipeRight: () -> Unit, content: #Composable () -> Unit) {
val dismissState = rememberDismissState {
when (it) {
DismissValue.Default -> return#rememberDismissState false
DismissValue.DismissedToEnd -> onSwipeRight()
DismissValue.DismissedToStart -> onSwipeLeft()
}
true
}
SwipeToDismiss(
modifier = modifier,
state = dismissState,
dismissThresholds = { FractionalThreshold(0.7f) },
background = {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Cyan)
)
}
) {
content()
}
}
Any suggestion is highly appreciated.
I am trying to figure out the right way to remember a color between re-compose, where the color is used in the parent, but calculated in a child.
var color by remember { mutableStateOf(Color.Transparent) }
Row(Modifier.height(100.dp)) {
Box(
Modifier
.width(6.dp)
.fillMaxHeight()
.background(color)
)
Column {
HorizontalPager(
count = pages.size,
state = pagerState
) { page ->
val page = pages[page]
color = page.color
}
}
}
However I am running into two issues
When the color changes when I page seem to get into an infinite re-compose.
The color seems to be a blend of the new page color and the previous page color.
Rather than having color as state, which is causing your recomposition loop. You could derive the same thing by hoisting your pagerState object and referencing the current page. Something like the following:
val pagerState = rememberPagerState()
var color = remember(pagerState.currentPage) { pages[pagerState.currentPage].color }
Row(Modifier.height(100.dp)) {
Box(
Modifier
.width(6.dp)
.fillMaxHeight()
.background(color)
)
Column {
HorizontalPager(
count = pages.size,
state = pagerState
) { page ->
}
}
}
I saw similar questions with no great solutions so I thought this might be useful.
I had a problem when I wanted to animate a button the user clicked to make it larger when pressed; not as easy as it sounds because when you get the animation working the onClick event never fires. [Because it depends on the up event I guess]
I found a way to make both the animation and the click work for a icon button and I thought it might work for other cases.
#OptIn(ExperimentalComposeUiApi::class)
#Composable
fun RoundIconButton(
modifier: Modifier = Modifier,
imageVector: ImageVector,
onClick: () -> Unit,
tint: Color = Color.Black.copy(alpha = 0.8f),
backgroundColor: Color =
MaterialTheme.colors.background,
elevation: Dp = 4.dp,
contentDescription: String
) {
val interactionSource = remember {
MutableInteractionSource() }
val isPressed by
interactionSource.collectIsPressedAsState()
val transition = updateTransition(targetState
= isPressed, label = "")
val size by transition.animateDp(label = "")
{ state ->
when(state) {
false -> 40.dp
true -> 50.dp
}
}
Card(
modifier = modifier
.padding(all = 4.dp)
.clickable(interactionSource =
interactionSource,indication =
LocalIndication.current,onClick= onClick)
.then(Modifier.size(size)),
shape = CircleShape,
backgroundColor = backgroundColor,
elevation = elevation,
) {
Icon( imageVector = imageVector,
contentDescription = contentDescription,tint
= tint)
}
}
I have a simple Light/Dark theme toggle, and I'm saving the theme to a dataStore.
After saving the theme and closing the app, the screen temporarily starts in the light mode before switching to the dark theme. Am I missing something? is it a side-effect thing I'm dealing with here? Please advise.
Here's a GIF explaining the case
https://i.stack.imgur.com/7YH7z.gif
Here's the code for the theme.kt file
private val DarkColorPalette = darkColors(
primary = White100,
)
private val LightColorPalette = lightColors(
primary = Blue100,
)
#Composable
fun MyTheme(
darkTheme: MutableState<Boolean>,
content: #Composable () -> Unit) {
val colors = if (darkTheme.value) {
DarkColorPalette
} else {
LightColorPalette
}
MaterialTheme(
colors = colors,
typography = Typography,
shapes = Shapes,
content = content
)
}
Here's the code for MainActivity.kt file
// DataStore
val Context.dataStore: DataStore<Preferences> by preferencesDataStore("settings")
// DataStore key
val USER_THEME = booleanPreferencesKey("user_theme")
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// Reading theme
val themeFlow: Flow<Boolean> = dataStore.data
.map { preferences ->
preferences[USER_THEME] ?: false
}
// Toggle and save theme
suspend fun toggleTheme() {
dataStore.edit { settings ->
val currentTheme = settings[USER_THEME] ?: false
settings[USER_THEME] = !currentTheme
}
}
super.onCreate(savedInstanceState)
setContent {
// Getting the theme from DataStore
// If the initial value is true, it works fine on dark mode but not on Light
// If the initial value is false, it works fine on light mode but not on dark
val userTheme = themeFlow.collectAsState(initial = false)
// Mutable state of the theme to be passed into "My Theme"
val themeState: MutableState<Boolean> = mutableStateOf(userTheme.value)
// Scope to toggle theme on click
val scope = rememberCoroutineScope()
// UI
MyTheme (darkTheme = themeState) {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
Image(
painter = painterResource(id = R.drawable.icon_dark),
contentDescription = "Theme Switcher",
colorFilter = ColorFilter.tint(MaterialTheme.colors.primary),
modifier = Modifier
.clickable {
scope.launch {
toggleTheme()
}
}
)
}
}
}
}
}
Edited code after Francesc's answer:
Removed the "userTheme" line and added runBlocking to this line
val themeState: MutableState<Boolean> = runBlocking { mutableStateOf(themeFlow.first())}
Toggled the theme value manually from the button:
scope.launch {
themeState.value = !themeState.value
saveTheme() // renamed it to saveTheme instead of toggleTheme
}
Your theme is read asynchronously from the store and initially it defaults to false (not dark theme), so it shows in light mode. Later when the flow emits and the mode is dark, then the UI switches to dark mode.
Due to the asynchronicity of the data store, you have 2 options:
read the theme initial value synchronously, using runBlocking
display some placeholder while you wait for the read, maybe a Box with your app logo (so like a splash screen), or just nothing, a blank canvas
The following picture shows what I want to achieve. I want the tab indicator to be a short rounded bar.
I looked up the implementation of TabRowDefaults.Indicator(), and just made my own one. I just tried to add the clip() modifier, but it didn't work. And I tried to change the order of the modifiers, but still no luck.
And here is my code implementation:
#Composable
fun TabLayout(
tabItems: List<String>,
content: #Composable () -> Unit
) {
var tabIndex by remember { mutableStateOf(0) }
Column {
ScrollableTabRow(
selectedTabIndex = tabIndex,
edgePadding = 0.dp,
backgroundColor = MaterialTheme.colors.background,
contentColor = Blue100,
indicator = { tabPositions ->
Box(
modifier = Modifier
.tabIndicatorOffset(tabPositions[tabIndex])
.height(4.dp)
.clip(RoundedCornerShape(8.dp)) // clip modifier not working
.padding(horizontal = 28.dp)
.background(color = AnkiBlue100)
)
},
divider = {},
) {
tabItems.forEachIndexed { index, item ->
Tab(
selected = tabIndex == index,
onClick = { tabIndex = index },
selectedContentColor = Blue100,
unselectedContentColor = Gray200,
text = {
Text(text = item, fontFamily = fontOutfit, fontSize = 18.sp)
}
)
}
}
Divider(
color = Gray50,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
)
content()
}
}
You applied Modifier.padding between Modifier.clip and Modifier.background, so the rounding is actually applied to the transparent padding. You need to move the padding in front of the clip, or specify the shape with the background:
.background(color = AnkiBlue100, shape = RoundedCornerShape(8.dp))
Read more about why the order of the modifiers matters in this answer