Okay, so this is a bit of a specific issue, but hopefully someone understands what's happening:
I'm using a Jetpack Compose Material 3 CenterAlignedTopAppBar, with TopAppBarDefaults.pinnedScrollBehavior to make it so that the color of the appbar changes when I scroll down on nested content.
It works! In most cases. However, one of my screens has a large text field, that when clicked causes the content to scroll down by itself (to focus on the text field)—and that seems to confuse the appbar, which doesn't change color. It will only change color if I manually scroll up and down.
Relevant code:
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
Scaffold(
contentWindowInsets = EmptyWindowInsets,
modifier = Modifier
.fillMaxHeight()
.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
// just a wrapper around CenterAlignTopAppBar
StandardTopAppBar(scrollBehavior = scrollBehavior)
},
content = { innerPadding ->
// I've also tried with LazyColumn and see the same behavior
Column(
Modifier
.padding(innerPadding)
.padding(start = 10.dp, end = 10.dp)
.fillMaxHeight()
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
tl;dr; manually scrolling changes the appbar color, but scrolling caused by clicking into a text field that scrolls into view does not. Any idea how I could fix this?
Probably not quite what you are looking for unfortunately, but I had a similar problem. My TopAppBar color was also stuck, but when navigating between different screens. I fixed it by assigning the TopAppBar a different scrollBehaviour depending on the screen.
val scrollBehavior1 = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val scrollBehavior2 = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
In TopAppBar arguments:
scrollBehavior =
when (currentRoute) {
Screens.ExampleScreen1.route -> scrollBehavior1
Screens.ExampleScreen2.route -> scrollBehavior2
else -> null
}
Hopefully this helps you in one way or another.
In the XML-based UI, FAB had a field to override the custom scroll behavior. Since there is no such parameter in the function in Compose FAB implementation, it became a little unclear how to implement the hide on scroll behavior.
I use Scaffold with the nestedScroll modifier in order for my СollapsingAppBar to work. I assumed at first that it was necessary to create an object inherited from the NestedScrollConnection interface. And also connect it with the nestedScroll modifier to Scaffold. But unfortunately, as I understand it, it is impossible to connect several scrollBehavior objects to one Scaffold at once.
val scrollBehavior = TopAppBarScrollBehavior()
val scrollBehavior1 = FloatingActionButtonScrollBehavior()
Scaffold(
modifier = Modifier
.nestedScroll(scrollBehavior.nestedScrollConnection)
//this one won't work
.nestedScroll(scrollBehavior1.nestedScrollConnection)
...
According to nestedScroll documentation it takes 2 arguments
fun Modifier.nestedScroll(
connection: NestedScrollConnection,
dispatcher: NestedScrollDispatcher? = null
): Modifier
NestedScrollDispatcher is needed if a component is able to receive and react to the drag/fling events. I think you have to implement NestedScrollDispatcher to coordinate FAB's visibility with scroll.
I was building a calendar app in SwiftUI and decided to move it to the Jetpack Compose to have a version for Android as well. For the SwiftUI version, I was using a ZStack group to overlay the event cards on the time headings. Is there any way to overlay two divs in Kotlin like in SwiftUI?
Well, you can use a Box and set the z-Index using zIndex modifier.
Box {
Box(
Modifier
.size(100.dp)
.background(Color.Red)
.zIndex(1f)
)
Box(
Modifier
.size(100.dp)
.background(Color.Green)
.zIndex(2f)
)
Box(
Modifier
.size(100.dp)
.background(Color.Blue)
.zIndex(3f)
)
}
The highest zIndex will be in front of the others. If you don't define a zIndex, the last one will be closest one.
I have a composable representing list of results:
#Composable
fun ResultsList(results: List<Pair<Square, Boolean>>) {
val coroutineScope = rememberCoroutineScope()
val listState = rememberLazyListState()
LazyRow(state = listState) {
items(results) { result ->
ResultsItem(result.first, result.second)
coroutineScope.launch {
listState.animateScrollToItem(results.size)
}
}
}
}
Expected behaviour: The list smoothly scrolls to the last item whenever a new item is added
Actual behaviour: All is good, but whenever I manually scroll fast through the list, it is also automatically put on the bottom. Also, the scrolling is not smooth.
Your code gives the following error:
Calls to launch should happen inside a LaunchedEffect and not composition
You should not ignore it by calling the side effect directly from the Composable function. See side effects documentation for more details.
You can use LaunchedEffect instead (as this error suggests). By providing results.size as a key, you guarantee that this will be called only once when your list size changes:
#Composable
fun ResultsList(results: List<Pair<Square, Boolean>>) {
val listState = rememberLazyListState()
LaunchedEffect(results.size) {
listState.animateScrollToItem(results.size)
}
LazyRow(state = listState) {
items(results) { result ->
ResultsItem(result.first, result.second)
}
}
}
Philip's solution will work for you. However, I'm posting this to ensure that you understand why
A.) The scroll was not smooth
B.) The list gets scrolled to the bottom when you scroll through it fast enough.
Explanation for A.)
It is because you are using animateScollTo. I've experienced issues with this method if called too often,
Explanation for this lies in how Lazy scrollers handle their children internally. You see, Lazy scrollers, as you might know, are meant to display only a small window of a large dataset to the user. To achieve this, it uses internal caching. So, the items on the screen, AND a couple of items to the top and bottom of the current window are cached.
Now, since in your code, you are making a call to animateScrollTo(size) inside the Composable's body (the items scope), the code will essentially be executed upon every composition.
Hence, on every recomposition, there is an active clash between the animateScrollTo method, and the users touch input. When the user scrolls past in a not-so-fast manner, this is what happens - user presses down, gently scrolls, then lifts up the finger. Now, remember this, for as long as the finger is actually pressed down, they animateScrollTo will seem to have no effect (because the user is actively holding a position on the scroller, so it won't be scrolled past it by the system). Hence, while the user is scrolling, some items ahead of the list are cached, but the animateScrollTo does not work. Then, because the motion is slow enough, the distance the scroller travels because of inertia is not a problem, since the list already has enough cached items to show for the distance. That also explains the second problem.
B.)
When you are scrolling through the list FAST enough, the exact same thing as the above case (the slow-scroll) happens. Only, this time the inertia carries the list too forward for the scroller to be handled based on the internal cache, and hence there is active recomposition. However, now since there is no active user input (they have lifted their finger off the screen), it actually does animate to the bottom, since their is no clash here for the animateScrollTo method.
For as long as your finger is pressed, no matter how fast you scroll, it won't scroll to the bottom (test that!)
Now to the solution of the actual problem. Philip your answer is brilliant. The only thing is that it might not work if the developer has an item remove implementation as well. Since only the size of the list is monitored, it will scroll to end when an item is added OR deleted. To counteract that, we would actually need some sort of reference value. So, either you can implement something of your own to provide you with a Boolean variable that actually confirms whether an item has been ADDED, or you could just use something like this.
#Composable
fun ResultsList(results: List<Pair<Square, Boolean>>) {
//Right here, create a variable to remember the current size
val currentSize by rememberSaveable { mutableStateOf (results.size) }
//Now, extract a Boolean to be used as a key for LaunchedEffect
var isItemAdded by mutableStateO(results.size > currentSize)
LaunchedEffect (isItemAdded){ //Won't be called upon item deletion
if(isItemAdded){
listState.animateScrollToItem(results.size)
currentSize = results.size
}
}
val listState = rememberLazyListState()
LazyRow(state = listState) {
items(results) { result ->
ResultsItem(result.first, result.second)
}
}
}
This should ensure the proper behaviour. Of course, let me know if there is anything else, happy to help.
Pretty obvious. Why are you calling:
listState.animateScrollToItem(results.size) inside your LazyList? Of course you're going to get extremely bad performance. You shouldn't be messing around with scrolling when items are being rendered. Get rid of this line of code.
Here is the code snippet:
Box(
Modifier
.size(48.dp, 48.dp)
.background(Color.Transparent)
.align(Alignment.CenterEnd)
)
{
Box(
Modifier
.size(height = 35.636363.dp, width = 41.818181.dp)
.align(Alignment.CenterEnd)
.clip(RoundedCornerShape(topStart = 20.dp, bottomStart = 20.dp))
.clickable(onClick = {showDialogFragment.value = true})
.background(Color.Red)
)
}
Whenever i click on parent box (the one which is transparent) it activates click event of its only child. Why so? Current behavior meets my needs, but i dont understand why it happens.
According to Material Guidelines, the minimum touch size is 48.dp.
Since 1.1.0-alpha03, when you add touch detection to a view with a size smaller than this value, tracking will work on the enlarged frame.
Added minimum touch target size to ViewConfiguration for use in semantics and pointer input to ensure accessibility.
Just out of curiosity, you can check the extended touch padding for the current view using pointerInput modifier, which lies under all touch processing in Compose:
.pointerInput(Unit) {
println("$extendedTouchPadding")
}