Scrolling LazyColumn items makes ScaffoldBottomSheet collapses - android-jetpack-compose

I am going to implement a screen with jetpack compose containing a ScaffoldBottomSheet and a LazyColumn inside the bottom sheet's content. I also want the bottom sheet's height to be fixed and users can't collapse it. In order to do that I disable the sheet gestures and give the height of 600.dp for the content of the bottom sheet. But when I scroll items of the lazy column the bottom sheet scrolls down and finally collapses.
Here are my codes:
#ExperimentalMaterialApi
#Composable
fun TestScreen(
testViewModel: TestViewModel = hiltViewModel()
) {
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = rememberBottomSheetState(
BottomSheetValue.Expanded
)
)
BottomSheetScaffold(
scaffoldState = bottomSheetScaffoldState,
sheetContent = {
Box(modifier = Modifier.fillMaxWidth().height(600.dp)) {
LazyColumn() {
items(20) {
Text(text = "this is for test", modifier = Modifier
.padding(start = 20.dp, top = 20.dp)
.fillMaxWidth()
.height(50.dp))
}
}
}
},
sheetPeekHeight = 0.dp,
sheetShape = RoundedCornerShape(topEnd = 52.dp, topStart = 52.dp),
backgroundColor = AppColor.ThemeColor.BACKGROUND,
sheetGesturesEnabled = false
) {
}
}

Related

Wrap content of `ScrollableTabRow` composable so it goes past the screen width

I want to make a ScrollableTabRow that goes past the width of the screen to make it clearer that there are more tabs to scroll through. It should look something like this:
When scrolled all the way to the left:
When scrolled somewhere in the middle:
When scrolled all the way to the right:
However, I cannot achieve it using the Material component ScrollableTabRow because the ScrollableTabRow is filling the remaining width of the screen, instead of fully wrapping its content.
When scrolled all the way to the left:
When scrolled all the way to the right:
Here is my code using the ScrollableTabRow composable:
Note: 1.unit is equal to 4.dp
Row {
HorizontalSpacer(width = 4.unit)
ScrollableTabRow(
modifier = Modifier.clip(CircleShape).wrapContentSize(),
selectedTabIndex = pagerState.currentPage,
indicator = { tabPositions ->
SpecsheetTabIndicator(tabPositions = tabPositions, pagerState = pagerState)
},
edgePadding = 0.dp,
divider = {},
containerColor = MaterialTheme.colorScheme.primaryContainer,
) {
for ((index, tab) in tabs.withIndex()) {
val textColor by animateColorAsState(
targetValue = when (pagerState.currentPage) {
index -> MaterialTheme.colorScheme.onPrimary
else -> MaterialTheme.colorScheme.onPrimaryContainer
},
)
Tab(
modifier = Modifier
.zIndex(6f)
.clip(CircleShape),
selected = pagerState.currentPage == index,
onClick = {
coroutineScope.launch {
pagerState.animateScrollToPage(index)
}
},
text = {
Text(
text = tab.text(),
color = textColor,
)
},
)
}
}
HorizontalSpacer(width = 4.unit)
}
Also, as you can see in my code above, the Row is not really scrolling because I am not sure how to implement nested scrolling. Adding a horizontalScroll modifier to the Row makes the app crash because of the unhandled nested scrolling.
I have achieved the behavior I wanted as demonstrated in the first three images using regular rows:
Row(
modifier = Modifier.horizontalScroll(scrollState),
) {
HorizontalSpacer(width = 4.unit)
Row(
modifier = Modifier
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primaryContainer)
) {
for ((index, tab) in tabs.withIndex()) {
val containerColor by animateColorAsState(
targetValue = when (pagerState.currentPage) {
index -> MaterialTheme.colorScheme.primary
else -> Color.Transparent
},
)
val textColor by animateColorAsState(
targetValue = when (pagerState.currentPage) {
index -> MaterialTheme.colorScheme.onPrimary
else -> MaterialTheme.colorScheme.onPrimaryContainer
},
)
Surface(
modifier = Modifier.clip(CircleShape),
color = containerColor,
) {
Tab(
modifier = Modifier
.zIndex(6f)
.clip(CircleShape)
.defaultMinSize(minWidth = 24.unit),
selected = pagerState.currentPage == index,
onClick = {
coroutineScope.launch {
pagerState.animateScrollToPage(index)
}
},
text = {
Text(
text = tab.text(),
color = textColor,
)
},
)
}
}
}
HorizontalSpacer(width = 4.unit)
}
However, I don't know how to reimplement a lot of the behavior that are built into the ScrollableTabRow like the proper rendering of the indicator and the proper scrolling of the tab row to the selected tab when the selected tab is not visible. Can anyone help me make this work for me?

swipe to dismiss in lazy list - accidental horizontal scrolling during vertical scrolling in jetpack compose

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.

How to make custom tab indicator to be a rounded bar in Jetpack Compose

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

Keyboard does not open if the TextField inside LazyColumn is in the bottom of the screen

If you focus a TextField near the bottom of the screen, keyboard appears for a moment and then immediately hides. But if you focus some TextField from above, keyboard opens as usual and you can scroll to the bottom. This happens if windowSoftInputMode is set to adjustResize. If none is set, then the behaviour goes to i would say random (keyboard may overlap content, also it may push toolbar and etc.). Perhaps i am simply doing all this wrong, i dunno. I wonder if anyone faced this issue before and could give me a hand.
Sample code (for simplicity i hold mutable states right here, not using ViewModel):
#Composable
fun Greeting() {
Scaffold(topBar = {
TopAppBar(title = { Text(text = "Some title") })
}) {
val focusManager = LocalFocusManager.current
LazyColumn(
contentPadding = PaddingValues(all = 16.dp),
verticalArrangement = Arrangement.spacedBy(space = 16.dp)
) {
items(count = 20) { index ->
val (value, onValueChange) = rememberSaveable { mutableStateOf("Some value $index") }
TextField(
value = value,
onValueChange = onValueChange,
modifier = Modifier.fillMaxWidth(),
label = { Text(text = "Some label $index") },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
keyboardActions = KeyboardActions(onNext = {
if (!focusManager.moveFocus(FocusDirection.Down))
focusManager.clearFocus()
}),
singleLine = true
)
}
}
}
}
Compose version 1.0.5

android compose lazy column separate by weight

Is it possible to do weights in Jetpack Compose with lazy column?
I'd like to set it menu item is weighted as 1/n (n = number of menus) of a layout, and the other takes up the remaining 1/n, also.
I want to list it at the same height as the number of menus.
MenuList
#Composable
fun MenuList(
loading: Boolean,
menus: List<Menu>,
onNavigateToMenuDetailScreen: (String) -> Unit
) {
Box(modifier = Modifier
.background(color = MaterialTheme.colors.surface)
.fillMaxSize()) {
Column(modifier = Modifier.fillMaxSize()) {
if (loading && menus.isEmpty()) {
LoadingShimmer(imageHeight = 800.dp)
}
else if (menus.isEmpty()) {
NothingHere()
}
else {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.weight(1F)
) {
itemsIndexed(
items = menus
) { index, menu ->
MenuCard(
menu = menu,
onClick = {
}
)
}
}
}
}
}
}
MenuCard
#Composable
fun MenuCard(
menu: Menu,
modifier: Modifier = Modifier,
onClick: () -> Unit,
) {
Card(
shape = MaterialTheme.shapes.small,
modifier = Modifier
.padding(
bottom = 6.dp,
top = 6.dp
)
.fillMaxWidth()
.clickable(onClick = onClick),
elevation = 8.dp
) {
Column {
Text(
text = menu.name,
fontSize = 30.sp,
modifier = Modifier
.fillMaxWidth()
.wrapContentWidth(Alignment.CenterHorizontally)
.wrapContentHeight(Alignment.CenterVertically),
style = MaterialTheme.typography.h3
)
}
}
}
In summary, MenuCards are created as many as the number of menu on the MenuList screen, and I hope the height of each MenuCard will be 1/n.
(n = number of menu)
Like, when number of menu is 8,
Just same height to each menu.
Replace your LazyColumn code with BoxWithConstraints and regular Column.
BoxWithConstraints for getting the minHeight or in this case you can say screen height.
Change to something like the below.
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
val height = minHeight/menus.size
Column(
modifier = Modifier
.fillMaxSize()
) {
menus.forEachIndexed { index, menu ->
MenuCard(
modifier = Modifier.height(height),
menu = menu,
onClick = {
}
)
}
}
}
MenuCard
#Composable
fun MenuCard(
menu: Menu,
modifier: Modifier = Modifier,
onClick: () -> Unit,
) {
Card(
shape = MaterialTheme.shapes.small,
modifier = modifier
.padding(
bottom = 6.dp,
top = 6.dp
)
.fillMaxWidth()
.clickable(onClick = onClick),
elevation = 8.dp
) {
Column(
modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = menu.name,
fontSize = 30.sp,
style = MaterialTheme.typography.h3
)
}
}
}
You will get this
3 items and 6 items
If all you want is a way to divide the screen height in equal proportions to all the n items, you can simply go for a column.
val items = ... //I assume you have access to this
val screenHeight = LocalConfiguration.current.screenHeightDp
Column{
items.forEach{
MenuCard(modifier = Modifier.height(1f / items.size * screenHeight))
}
}
Column and LazyColumn are fundamentally different. LazyColumn's sole purpose is to deal with large datasets by loading and caching only a small window off that set. Now, if there is nothing to be scrolled, LazyColumn is useless, in fact, it is worse.
EDIT:
You could use BoxWithConstraints to get the screen dimensions as well, but it, in this context, would be more of a workaround than a solution. It adds an extra Composable and decreases the readability of the code too, so for getting the screen dimensions, always use the API that was specifically built for the job.

Resources