ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Follow publication

Scrollable TopAppBar with Jetpack Compose

This article is based on compose-beta02

Special thanks to Johann Blake, for pointing out a way better and build-in solution. With this answer my article gets obsolete. But I will keep it alive to be found by developers who need this 😉
The answer to this problem is nestedScrollConnection. Check his answer or the documentation.
If you wish an article about it, let me know ;)

Since Jetpack Compose reached beta status my team decided to jump right on it for our new project. We did a lot of reading, testing, failing and gained some learnings. One of the things we wanted to achieve is for the TopAppBar to scroll away while scrolling down, and to re-appear when scrolling up. I was digging into it and was surprised that there is nothing build-in.

Final result

I found a nice article about a fading AppBar, but since it was not directly what I was looking for, I only used it as an orientation.

Let’s start…

Since we have the AppBar at the top of our screen, we need a Box() layout as our parent. This will hold our AppBar and our scrollable element. In our case it will be a LazyColumn() composable, due to the fact that we need to load a lot of data in our new App. But you could of course use other composables as well.

Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(top = 56.dp),
state = scrollState
) {
//content goes here
}

ScrollableAppBar(
title = "ScrollableAppBarExample",
modifier = Modifier.align(Alignment.CenterStart)
)
}

Inside the Box() layout we will now place our list composable which will hold our content later. We pass a modifier to fill the maximum space available and PaddingValues for the contentPadding (because we are in a Box layout and the AppBar is composed above the list). Last but not least we have to add the state which handles the scrolling and updates accordingly. The scrollState in our example is a LazyListState(), because we are using a LazyColumn(). In order to remember the state during recomposition we use the function rememberLazyListState().

val scrollState = rememberLazyListState()

ScrollableAppBar composable

As mentioned earlier we cannot use the default AppBar() composable, so we have to do it ourselves. As parent we use a Surface() (to get some elevation), a Box for the height and background, and a Row() to display the title and maybe a navigation icon. Pretty straight forward.

So the first draft looks like this:

First draft of the custom AppBar

If we place this in our parent Box() layout and run the app, we will notice that nothing is going to happen on scroll. ¯\_(ツ)_/¯

Handle the scrolling

To distinguish if we are scrolling up or down (and handle the animation later) we need to calculate the current scroll position and compare it with the last one. In a normal Column you would use scrollState and it would have parameters like scrollPosition, but that would be way too easy. 😉

We already created a scrollState with rememberLazyListState(). This gives us a LazyListState, which provides parameters like initialFirstVisibleItemIndex and initialFirstVisibleItemScrollOffset. For our purpose we need the index.

Additionally we want to create our own state to determine, if we scroll up or down. Therefor we implement a simple ViewModel which holds a scrollUp LiveData object which informs the ui if something changes. Furthermore we store the lastScrollIndex in a simple property. To update these values and to trigger a recomposition we add an update function to the ViewModel.

class ExampleViewModel: ViewModel() {

private var lastScrollIndex = 0

private val _scrollUp = MutableLiveData(false)
val scrollUp: LiveData<Boolean>
get() = _scrollUp

fun updateScrollPosition(newScrollIndex: Int) {
if (newScrollIndex == lastScrollIndex) return

_scrollUp.value = newScrollIndex > lastScrollIndex
lastScrollIndex = newScrollIndex
}
}

Back to the view. We create a new state from our ViewModel LiveData and update the ViewModel state, by using the scrollStates firstVisibleItemIndex property. Then we add a new parameter and pass the scrollUpState to our ScrollableAppBar. #StateAllTheThings

val scrollState = rememberLazyListState()
val scrollUpState = viewModel.scrollUp.observeAsState()

viewModel.updateScrollPosition(scrollState.firstVisibleItemIndex)

Box(..) {
LazyColumn(state = scrollState,...) {...}
ScrollableAppBar(scrollUpState = scrollUpState...) {...}
}

Animation

Inside our AppBar composable use scrollUpState to animate the AppBar up and down by doing the following:

val position by animateFloatAsState(if (scrollUpState.value == true) -150f else 0f)

This animates the value between -150 and 0. Lastly we have to add this position to the Surface in our AppBar, by adding a graphicsLayer modifier and pass the position into the translationY parameter of the layer.

Final ScrollAbleAppBar composable

That’s it! Our AppBar should now scroll accordingly to the scroll position. 👏

Summarize, what have we done..

  1. Create a layout that holds your scrollable content and the AppBar
  2. Create scrollState via rememberLazyListState() (or other scrollStates) and pass it to the scrollable composable
  3. Create a composable for your AppBar
  4. Handle scrolling by adding states to the viewModel, observe and update them while scrolling
  5. Pass the states into the correct composabels
  6. Animate the AppBar with the help of the passed state and animateFloatAsState()

Thanks for reading and I hope you learned something new. If you have any question or suggestions, please leave a comment.

Have a nice day and stay healthy!!! 🍀

Published in ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Written by Max Hübscher

Android engineer | kotlin lover | Berlin

Responses (1)

Write a response