I'm looking forward to a way to implement a Collapsing effect while scrolling on LazyColumn list. I have been checking the docs but I didn't found nothing related. How can I implement it?
At the moment I'm using a BottomNavigation setted inside my Scaffold and I can add the inner paddings to the screen coming from the scaffold content lambda. But I didn't find any kind of scrollable state or something close to it.
You can use the nestedScroll modifier.
Something like:
val bottomBarHeight = 48.dp
val bottomBarHeightPx = with(LocalDensity.current) { bottomBarHeight.roundToPx().toFloat() }
val bottomBarOffsetHeightPx = remember { mutableStateOf(0f) }
// connection to the nested scroll system and listen to the scroll
// happening inside child LazyColumn
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.y
val newOffset = bottomBarOffsetHeightPx.value + delta
bottomBarOffsetHeightPx.value = newOffset.coerceIn(-bottomBarHeightPx, 0f)
return Offset.Zero
}
}
}
and then apply the nestedScroll to the Scaffold:
Scaffold(
Modifier.nestedScroll(nestedScrollConnection),
scaffoldState = scaffoldState,
//..
bottomBar = {
BottomAppBar(modifier = Modifier
.height(bottomBarHeight)
.offset { IntOffset(x = 0, y = -bottomBarOffsetHeightPx.value.roundToInt()) }) {
IconButton(
onClick = {
coroutineScope.launch { scaffoldState.drawerState.open() }
}
) {
Icon(Icons.Filled.Menu, contentDescription = "Localized description")
}
}
},
content = { innerPadding ->
LazyColumn(contentPadding = innerPadding) {
items(count = 100) {
Box(
Modifier
.fillMaxWidth()
.height(50.dp)
.background(colors[it % colors.size])
)
}
}
}
)
Related
I'm new to Jetpack Compose and I'm not quite sure how to do what I need. In the screen below, I want to scroll the whole screen and not just the list at the bottom and when the scroll reaches the end of the list below, it still applies the paging library and goes to get more elements. I managed to get the Paging Library to work and the scroll in the list below too, but I can't make the rest of the page elements scroll as well - this is because only the list has scroll and not the rest of the page. Whenever I'm trying to do that, I get the following crash:
Vertically scrollable component was measured with an infinity maximum height constraints, which is disallowed. One of the common reasons is nesting layouts like LazyColumn and Column(Modifier.verticalScroll()). If you want to add a header before the list of items please add a header as a separate item() before the main items() inside the LazyColumn scope. There are could be other reasons for this to happen: your ComposeView was added into a LinearLayout with some weight, you applied Modifier.wrapContentSize(unbounded = true) or wrote a custom layout. Please try to remove the source of infinite constraints in the hierarchy above the scrolling container.
and I don't really know why.
I leave you the code below and two screenshots: the first is the current state, where I can only scroll through the list. The second is what I intend, which is to scroll the entire page.
#Edit: I was able to implement all screen scroll with fixed height on the children lazy column, but that is not what I want.
#Composable
#ExperimentalFoundationApi
private fun MainActivityLayout(navController: NavHostController) {
LazyColumn(
modifier = Modifier
.paint(
painter = painterResource(id = R.drawable.main_background),
contentScale = ContentScale.FillBounds
)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
item {
HeightSpacer(Dimen40)
Image(
painter = painterResource(id = R.drawable.ic_clearjobs_logo_2x),
contentDescription = null
)
HeightSpacer(Dimen47)
Navigation(navController = navController)
}
}
}
#Composable
#ExperimentalFoundationApi
fun JobOpeningsScreen(viewModel: JobOpeningsViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsState()
Column {
ClearJobsScreenTitle(
lightTitle = stringResource(id = R.string.job_openings_light_title),
boldTitle = stringResource(id = R.string.job_openings_bold_title)
)
HeightSpacer(Dimen60)
Row {
CategoryButton()
WidthSpacer(Dimen2)
OrderByButton()
}
HeightSpacer(Dimen30)
SearchTextField()
HeightSpacer(Dimen60)
when (uiState) {
is BaseViewState.Data -> JobOpeningsContent(
viewState = uiState.cast<BaseViewState.Data<JobOpeningsViewState>>().value
)
is BaseViewState.Loading -> {
LoadingView()
}
else -> {}
}
LaunchedEffect(key1 = viewModel, block = {
viewModel.onTriggerEvent(JobOpeningsEvent.LoadJobOffers)
})
}
}
#Composable
fun JobOpeningsContent(viewState: JobOpeningsViewState) {
val pagingItems = rememberFlowWithLifecycle(viewState.pagedData).collectAsLazyPagingItems()
SwipeRefresh(
state = rememberSwipeRefreshState(
isRefreshing = pagingItems.loadState.refresh == LoadState.Loading
),
onRefresh = { pagingItems.refresh() },
indicator = { state, trigger ->
SwipeRefreshIndicator(
state = state,
refreshTriggerDistance = trigger,
scale = true
)
},
content = {
LazyColumn(
modifier = Modifier.width(Dimen320),
verticalArrangement = Arrangement.spacedBy(Dimen30)
) {
items(pagingItems.itemCount) { index ->
pagingItems[index]?.let {
JobOpeningsRow(dto = it)
}
}
if (pagingItems.loadState.append == LoadState.Loading) {
item {
Box(
Modifier
.padding(24.dp)
) {
CircularProgressIndicator(Modifier.align(Alignment.Center))
}
}
}
}
}
)
}
I found the solution to this problem, although it is not 100% and in terms of code it is not as good as I would like.
The error speaks for itself, we can't have infinite vertical scroll, Jetpack Compose doesn't allow it. I had the option of putting a fixed height on the Lazy Column of my list, but it wasn't what I wanted and it didn't work properly. The solution was to put everything inside a single LazyColumn and remove the Column from MainActivity, using a Box element and contentAlignment. I leave you below the final code that I used to solve the problem.
MainScreen function that before was MainActivityLayout function:
#Preview
#Composable
#ExperimentalFoundationApi
fun MainScreen() {
val navController = rememberNavController()
val topLevelDestinations = listOf(
NavigationItem.JobOpenings,
NavigationItem.Profile,
NavigationItem.About
)
val isTopLevelDestination =
navController
.currentBackStackEntryAsState()
.value
?.destination
?.route in topLevelDestinations.map { it.route }
val backStackEntryState = navController.currentBackStackEntryAsState()
Scaffold(
bottomBar = {
if (isTopLevelDestination) {
BottomNavBar(
navController = navController,
backStackEntryState = backStackEntryState,
bottomNavItems = topLevelDestinations
)
}
}
) {
Box(
modifier = Modifier
.paint(
painter = painterResource(id = R.drawable.main_background),
contentScale = ContentScale.FillBounds
)
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Navigation(navController = navController)
}
}
}
New JobOpenings fun that is mixed with old JobOpeningsContent function:
#Composable
#ExperimentalFoundationApi
fun JobOpeningsScreen(viewModel: JobOpeningsViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsState()
when (uiState) {
is BaseViewState.Data -> {
val pagedData = uiState.cast<BaseViewState.Data<JobOpeningsViewState>>().value.pagedData
val pagingItems = rememberFlowWithLifecycle(pagedData).collectAsLazyPagingItems()
SwipeRefresh(
state = rememberSwipeRefreshState(
isRefreshing = pagingItems.loadState.refresh == LoadState.Loading
),
onRefresh = { pagingItems.refresh() },
indicator = { state, trigger ->
SwipeRefreshIndicator(
state = state,
refreshTriggerDistance = trigger,
scale = true
)
},
content = {
LazyColumn(
modifier = Modifier
.width(Dimen320),
verticalArrangement = Arrangement.spacedBy(Dimen30)
) {
item {
ScreenHeader(
lightTitle = stringResource(id = R.string.job_openings_light_title),
boldTitle = stringResource(id = R.string.job_openings_bold_title)
)
HeightSpacer(Dimen60)
Row {
CategoryButton()
WidthSpacer(Dimen2)
OrderByButton()
}
HeightSpacer(Dimen30)
SearchTextField()
HeightSpacer(Dimen60)
}
items(pagingItems.itemCount) { index ->
pagingItems[index]?.let {
JobOpeningsRow(dto = it)
}
}
if (pagingItems.loadState.append == LoadState.Loading) {
item {
Box(Modifier.padding(Dimen24)) {
CircularProgressIndicator(Modifier.align(Alignment.Center))
}
}
}
}
}
)
}
is BaseViewState.Loading -> LoadingView()
else -> {}
}
LaunchedEffect(key1 = viewModel, block = {
viewModel.onTriggerEvent(JobOpeningsEvent.LoadJobOffers)
})
}
#ExperimentalFoundationApi
#Preview
#Composable
fun JobOpenings() {
JobOpeningsScreen()
}
Problems that I found with this solution:
LoadingView appears at the top of the screen instead at the top of the list.
If anyone has any suggestion to improve this, I am open to it. This works perfectly with Paging Library + Swipe Refresh (Accompanist) and full page scroll.
How to do this Scroll hide fab button in Jetpack Compose with transaction
Like this I need it:
You need to listen to the scroll state and apply AnimatedVisibiltiy. Here is an example using LazyColumn with LazyListState (you could also use Column with ScrollState)
#Composable
fun Screen() {
val listState = rememberLazyListState()
val fabVisibility by derivedStateOf {
listState.firstVisibleItemIndex == 0
}
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(
Modifier.fillMaxSize(),
state = listState,
) {
items(count = 100, key = { it.toString() }) {
Text(modifier = Modifier.fillMaxWidth(),
text = "Hello $it!")
}
}
AddPaymentFab(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 40.dp),
isVisibleBecauseOfScrolling = fabVisibility
)
}
}
#Composable
private fun AddPaymentFab(
modifier: Modifier,
isVisibleBecauseOfScrolling: Boolean,
) {
val density = LocalDensity.current
AnimatedVisibility(
modifier = modifier,
visible = isVisibleBecauseOfScrolling,
enter = slideInVertically {
with(density) { 40.dp.roundToPx() }
} + fadeIn(),
exit = fadeOut(
animationSpec = keyframes {
this.durationMillis = 120
}
)
) {
ExtendedFloatingActionButton(
text = { Text(text = "Add Payment") },
onClick = { },
icon = { Icon(Icons.Filled.Add, "Add Payment") }
)
}
}
It may be late, but after struggling with this issue for a while, I was able to find the right solution from the Animation Codelab sourcecode.
The difference between this and the previous answer is that in this way, as soon as the page is scrolled up, the Fab is displayed and there is no need to reach the first item of the page to display the Fab.
step one: getting an instance of the lazyListState class inside LazyColumn
val lazyListState = rememberLazyListState()
Step two: Creating a top level variable to hold the scroll state so that recompositions do not change the state value unintentionally.
var isScrollingUp by mutableStateOf(false)
Step three: just copy this composable Extension Function inside the file
#Composable
private fun LazyListState.isScrollingUp(): Boolean {
var previousIndex by remember(this) { mutableStateOf(firstVisibleItemIndex) }
var previousScrollOffset by remember(this) { mutableStateOf(firstVisibleItemScrollOffset) }
return remember(this) {
derivedStateOf {
if (previousIndex != firstVisibleItemIndex) {
previousIndex > firstVisibleItemIndex
} else {
previousScrollOffset >= firstVisibleItemScrollOffset
}.also {
previousIndex = firstVisibleItemIndex
previousScrollOffset = firstVisibleItemScrollOffset
}
}
}.value
}
Step four: Open an AnimatedVisibility block and pass the isScrollingUp variable as its first parameter. And finally, making the Fab and placing it inside the AnimatedVisibility
AnimatedVisibility(visible = isScrollingUp) {
FloatingActionButton(onClick = { /*TODO*/ }) {
// your code
}
}
have fun!
I wanted to create and add gestures on top app bar some thing similar to below screenshot using jetpack compose:
I am able to create collapsible top bar using the below android docs link:
documentation link but not able to do gestures to expand and collapse along with change in layout using compose. Below is the code I have tried for collapsible toolbar.
val toolbarHeight = 48.dp
val toolbarHeightPx = with(LocalDensity.current) { toolbarHeight.roundToPx().toFloat() }
// our offset to collapse toolbar
val toolbarOffsetHeightPx =
remember { mutableStateOf(0f) }
// now, let's create connection to the nested scroll system and listen to the scroll
// happening inside child LazyColumn
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
// try to consume before LazyColumn to collapse toolbar if needed, hence pre-scroll
val delta = available.y
val newOffset = toolbarOffsetHeightPx.value + delta
toolbarOffsetHeightPx.value = newOffset.coerceIn(-toolbarHeightPx, 0f)
// here's the catch: let's pretend we consumed 0 in any case, since we want
// LazyColumn to scroll anyway for good UX
// We're basically watching scroll without taking it
return Offset.Zero
}
}
}
And below is the gesture link which I want to implement in topbvar
topbar gesture video
Please help me with the links. Thanks!
If you are looking to implement collapsing toolbar like below where the title will animate based on collapsing state this code reference might help you. You need to build a custom layout for it.
#Composable
fun CollapsingTopBar(
modifier: Modifier = Modifier,
collapseFactor: Float = 1f, // A value from (0-1) where 0 means fully expanded
content: #Composable () -> Unit
) {
val map = mutableMapOf<Placeable, Int>()
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
map.clear()
val placeables = mutableListOf<Placeable>()
measurables.map { measurable ->
when (measurable.layoutId) {
BACK_ID -> measurable.measure(constraints)
SHARE_ID -> measurable.measure(constraints)
TITLE_ID ->
measurable.measure(Constraints.fixedWidth(constraints.maxWidth
- (collapseFactor * (placeables.first().width * 2)).toInt()))
else -> throw IllegalStateException("Id Not found")
}.also { placeable ->
map[placeable] = measurable.layoutId as Int
placeables.add(placeable)
}
}
// Set the size of the layout as big as it can
layout(constraints.maxWidth, constraints.maxHeight) {
placeables.forEach { placeable ->
when (map[placeable]) {
BACK_ID -> placeable.placeRelative(0, 0)
SHARE_ID -> placeable.run {
placeRelative(constraints.maxWidth - width, 0)
}
TITLE_ID -> placeable.run {
val widthOffset = (placeables[0].width * collapseFactor).roundToInt()
val heightOffset = (placeables.first().height - placeable.height) / 2
placeRelative(
widthOffset,
if (collapseFactor == 1f) heightOffset else constraints.maxHeight - height
)
}
}
}
}
}
}
object CollapsingTopBar {
const val BACK_ID = 1001
const val SHARE_ID = 1002
const val TITLE_ID = 1003
const val COLLAPSE_FACTOR = 0.6f
}
#Composable
fun TopBar(
modifier: Modifier = Modifier,
currentHeight: Int,
title: String,
onBack: () -> Unit,
shareShow: () -> Unit
) {
Box(
modifier = modifier.height(currentHeight.dp)
) {
CollapsingTopBar(
collapseFactor = // calculate collapseFactor based on max and min height of the toolbar,
modifier = Modifier
.statusBarsPadding()
) {
Icon(
modifier = Modifier
.wrapContentWidth()
.layoutId(CollapsingTopBar.BACK_ID)
.clickable { onBack() }
.padding(16.dp),
imageVector = Icons.Filled.ArrowBack,
tint = MaterialTheme.colors.onPrimary,
contentDescription = stringResource(id = R.string.text_back)
)
Icon(
modifier = Modifier
.wrapContentSize()
.layoutId(CollapsingTopBar.SHARE_ID)
.clickable { }
.padding(16.dp),
imageVector = Icons.Filled.Share,
tint = MaterialTheme.colors.onPrimary,
contentDescription = stringResource(id = R.string.title_share)
)
Text(
modifier = Modifier
.layoutId(CollapsingTopBar.TITLE_ID)
.wrapContentHeight()
.padding(horizontal = 16.dp),
text = title,
style = MaterialTheme.typography.h4.copy(color = MaterialTheme.colors.onPrimary),
overflow = TextOverflow.Ellipsis
)
}
}
}
Sample reference from Google
I have a column which has many items; based on the scroll, I want to show/hide the Floating action button, in case the scroll is down, hide it and in case the scroll is up, show it.
My code is working partially, but the scrolling is buggy. Below is the code. Need help.
Column(
Modifier
.background(color = colorResource(id = R.color.background_color))
.fillMaxWidth(1f)
.verticalScroll(scrollState)
.scrollable(
orientation = Orientation.Vertical,
state = rememberScrollableState {
offset.value = it
coroutineScope.launch {
scrollState.scrollBy(-it)
}
it
},
)
) { // 10-20 items }
Based on the offset value (whether positive/negative), I am maintaining the visibility of FAB.
You can use the nestedScroll modifier.
Something like:
val fabHeight = 72.dp //FabSize+Padding
val fabHeightPx = with(LocalDensity.current) { fabHeight.roundToPx().toFloat() }
val fabOffsetHeightPx = remember { mutableStateOf(0f) }
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.y
val newOffset = fabOffsetHeightPx.value + delta
fabOffsetHeightPx.value = newOffset.coerceIn(-fabHeightPx, 0f)
return Offset.Zero
}
}
}
Since composable supports nested scrolling just apply it to the Scaffold:
Scaffold(
Modifier.nestedScroll(nestedScrollConnection),
scaffoldState = scaffoldState,
//..
floatingActionButton = {
FloatingActionButton(
modifier = Modifier
.offset { IntOffset(x = 0, y = -fabOffsetHeightPx.value.roundToInt()) },
onClick = {}
) {
Icon(Icons.Filled.Add,"")
}
},
content = { innerPadding ->
Column(
Modifier
.fillMaxWidth(1f)
.verticalScroll(rememberScrollState())
) {
//....your code
}
}
)
It can work with a Column with verticalScroll and also with a LazyColumn.
I want to implement a screen which can show two different bottom sheets.
Since ModalBottomSheetLayout only has a slot for one sheet I decided to change the sheetContent of the ModalBottomSheetLayout dynamically using a selected state when I want to show either of the two sheets (full code).
val sheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
val (selected, setSelected) = remember(calculation = { mutableStateOf(0) })
ModalBottomSheetLayout(sheetState = sheetState, sheetContent = {
when (selected) {
0 -> Layout1()
1 -> Layout2()
}
}) {
Content(sheetState = sheetState, setSelected = setSelected)
}
This works fine for very similar sheets, but as soon as you add more complexity to either of the two sheet layouts the sheet will not show when the button is pressed for the first time, it will only show after the button is pressed twice as you can see here:
Here you can find a reproducible example
I had a similar usecase, where I needed to show 2-3 stacked bottomsheets.
I ended up copying large part of Compose BottomSheet and added the desired behavior:
enum class BottomSheetValue { SHOWING, HIDDEN }
#Composable
fun BottomSheet(
parentHeight: Int,
topOffset: Dp = 0.dp,
fillMaxHeight: Boolean = false,
sheetState: SwipeableState<BottomSheetValue>,
shape: Shape = bottomSheetShape,
backgroundColor: Color = MaterialTheme.colors.background,
contentColor: Color = contentColorFor(backgroundColor),
elevation: Dp = 0.dp,
content: #Composable () -> Unit
) {
val topOffsetPx = with(LocalDensity.current) { topOffset.roundToPx() }
var bottomSheetHeight by remember { mutableStateOf(parentHeight.toFloat())}
val scrollConnection = sheetState.PreUpPostDownNestedScrollConnection
BottomSheetLayout(
maxHeight = parentHeight - topOffsetPx,
fillMaxHeight = fillMaxHeight
) {
val swipeable = Modifier.swipeable(
state = sheetState,
anchors = mapOf(
parentHeight.toFloat() to BottomSheetValue.HIDDEN,
parentHeight - bottomSheetHeight to BottomSheetValue.SHOWING
),
orientation = Orientation.Vertical,
resistance = null
)
Surface(
shape = shape,
color = backgroundColor,
contentColor = contentColor,
elevation = elevation,
modifier = Modifier
.nestedScroll(scrollConnection)
.offset { IntOffset(0, sheetState.offset.value.roundToInt()) }
.then(swipeable)
.onGloballyPositioned {
bottomSheetHeight = it.size.height.toFloat()
},
) {
content()
}
}
}
#Composable
private fun BottomSheetLayout(
maxHeight: Int,
fillMaxHeight: Boolean,
content: #Composable () -> Unit
) {
Layout(content = content) { measurables, constraints ->
val sheetConstraints =
if (fillMaxHeight) {
constraints.copy(minHeight = maxHeight, maxHeight = maxHeight)
} else {
constraints.copy(maxHeight = maxHeight)
}
val placeable = measurables.first().measure(sheetConstraints)
layout(placeable.width, placeable.height) {
placeable.placeRelative(0, 0)
}
}
}
TopOffset e.g. allows to place the bottomSheet below the AppBar:
BoxWithConstraints {
BottomSheet(
parentHeight = constraints.maxHeight,
topOffset = with(LocalDensity.current) {56.toDp()}
fillMaxHeight = true,
sheetState = yourSheetState,
) {
content()
}
}
I wanted to implement the same thing and because of the big soln, I wrote a post on dev.to that solves this problem, Here is the link
I implemented it like this. It looks pretty simple, but I still could not figure out how to pass the argument to "mutableStateOf ()" directly, I had to create a variable "content"
fun Screen() {
val bottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
val scope = rememberCoroutineScope()
val content: #Composable (() -> Unit) = { Text("NULL") }
var customSheetContent by remember { mutableStateOf(content) }
ModalBottomSheetLayout(
sheetState = bottomSheetState,
sheetContent = {
customSheetContent()
}
) {
Column {
Button(
onClick = {
customSheetContent = { SomeComposable1() }
scope.launch { bottomSheetState.show() }
}) {
Text("First Button")
}
Button(
onClick = {
customSheetContent = { SomeComposable2() }
scope.launch { bottomSheetState.show() }
}) {
Text("Second Button")
}
}
}
I just tried your code. I am not sure but looks like when you click first time, since selected state changes, Content function tries to recompose itself and it somehow blocks sheetState. Because i can see that when i click first time, bottom sheet shows up a little and disappears immediately. But second time i click same button, since selected state doesnt change, sheetState works properly.