Jetpack Compose, ModalBottomSheetLayout and content height - android

In my app, I'd like to use a ModalBottomSheetLayout. In my sheet content it, I'd like to use multiple screens :
ModalBottomSheetLayout(
modifier = Modifier,
content = { /* Some content */ },
sheetContent = {
Crossfade(
modifier = Modifier,
targetState = someState
) { state ->
when (state) {
A -> FirstScreen()
B -> SecondScreen()
}
}
}
)
The issue is that if my next screen is smaller than my previous, the animation makes that the bottom sheet is falling down.
I've tried to use Modifier.animateContentSize or Modifier.wrapContent on the Crossfade, or use a AnimatedContent, but the result is always the same
Anyone knows how to resolve that issue ? Thank for your help

Related

TabRow/Tab Recomposition Issue in Compose Accompanist Pager

I was trying to create a sample Tab View in Jetpack compose, so the structure will be like
Inside a Parent TabRow we are iterating the tab title and create Tab composable.
More precise code will be like this.
#OptIn(ExperimentalPagerApi::class)
#Composable
private fun MainApp() {
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.app_name)) },
backgroundColor = MaterialTheme.colors.surface
)
},
modifier = Modifier.fillMaxSize()
) { padding ->
Column(Modifier.fillMaxSize().padding(padding)) {
val pagerState = rememberPagerState()
val coroutineScope = rememberCoroutineScope()
val tabContents = listOf(
"Home" to Icons.Filled.Home,
"Search" to Icons.Filled.Search,
"Settings" to Icons.Filled.Settings
)
HorizontalPager(
count = tabContents.size,
state = pagerState,
contentPadding = PaddingValues(horizontal = 32.dp),
modifier = Modifier
.weight(1f)
.fillMaxWidth()
) { page ->
PagerSampleItem(
page = page
)
}
TabRow(
selectedTabIndex = pagerState.currentPage,
backgroundColor = MaterialTheme.colors.surface,
contentColor = MaterialTheme.colors.onSurface,
indicator = { tabPositions ->
TabRowDefaults.Indicator(
Modifier
.pagerTabIndicatorOffset(pagerState, tabPositions)
.height(4.dp)
.background(
color = Color.Green,
shape = RectangleShape
)
)
}
) {
tabContents.forEachIndexed { index, pair: Pair<String, ImageVector> ->
Tab(
selected = pagerState.currentPage == index,
selectedContentColor = Color.Green,
unselectedContentColor = Color.Gray,
onClick = {
coroutineScope.launch {
pagerState.animateScrollToPage(index)
}
},
text = { Text(text = pair.first) },
icon = { Icon(imageVector = pair.second, contentDescription = null) }
)
}
}
}
}
}
#Composable
internal fun PagerSampleItem(
page: Int
) {
// Displays the page index
Text(
text = page.toString(),
modifier = Modifier
.padding(16.dp)
.background(MaterialTheme.colors.surface, RoundedCornerShape(4.dp))
.sizeIn(minWidth = 40.dp, minHeight = 40.dp)
.padding(8.dp)
.wrapContentSize(Alignment.Center)
)
}
And coming to my question is whenever we click on the tab item, the inner content get recompose so weirdly. Im not able to understand why it is happens.
Am attaching an image of the recomposition counts below, please take a look that too, it would be good if you guys can help me more for understand this, also for future developers.
There are two question we have to resolve in this stage
Whether it will create any performance issue, when the view getting more complex
How to resolve this recompostion issue
Thanks alot.
… whenever we click on the tab item, the
inner content get recompose so weirdly. Im not able to understand why
it is happens...
It's hard to determine what this "weirdness" is, there could be something inside the composable your'e mentioning here.
You also didn't specify what the API is, so I copied and pasted your code and integrated accompanist view pager, then I was able to run it though not on an Android Studio with a re-composition count feature.
And since your'e only concerned about the Text and the Icon parameter of the API, I think that's something out of your control. I suspect the reason why your'e getting those number of re-composition count is because your'e animating the page switching.
coroutineScope.launch {
pagerState.animateScrollToPage(index)
}
Though 'm not able to try this on another Android Studio version with the re-composition feature, I think (though I'm not sure) scrolling to another page without animation will yield less re-composition count.
coroutineScope.launch {
pagerState.scrollToPage(index)
}
If it still bothers you, the best course of action is to ask them directly, though personally I wouldn't concerned much about this as they are part of an accepted API and its just Text and Icon being re-composed many times by an animation which is also fine IMO.
Now if you have some concerns about your PagerSampleItem stability(which you have a full control), based on the provided code and screenshot, I think your'e fine.
There's actually a feature suggested from this article to check the stability of a composable, I run it and I got this report.
restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun PagerSampleItem(
stable page: Int
)
Everything about this report is within the article I linked.
Also, your Text and Icon are using String and ImageVector which is stable and immutable (marked by #Immutable) respectively.
So TLDR, IMO your code is fine, your PagerSampleItem is not re-composing in the screenshot.

Kotlin Compose global footer view

I am trying to show an Ad banner at the bottom which is globally displayed. That means that I want to stick there when I navigate throughout the app.
So far I've tried adding the NavHost and the AdView inside a Column, hoping that the navigation with the linked pages will just take the remaining space and the bottom view will stick there. However the NavHost takes the whole screen and the AdView has zero height. When I navigate the second view, the AdView is still inside the view hierarchy but still with zero height.
I've tried changing all kinds of setup, adding .weight(1f) to the Ad view, changing Column Arrangement, .fillMaxSize(), .fillMaxHeight(), .minHeight(), fixed height, etc.
I might need to look into different layout for achieving this result, but not sure in which direction to search. The Ad works well, I tried displaying without the column and is shown correctly in the center of the screen.
Another variant I've tried is to display it without column, add bottom spacing to the NavHost then position the AdView to the bottom somehow. With this variant I haven't been able to position it to bottom.
Any help would be appreciated!
Column() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = NavigationRoutes.Home
) {
composable(NavigationRoutes.Home) { HomeView(navController = navController) }
composable(
NavigationRoutes.Books,
arguments = listOf(navArgument("mode") { type = NavType.IntType })
) { backStackEntry ->
val rawMode = backStackEntry.arguments?.getInt("mode") ?: 0
var mode: ContentMode = if (rawMode == 0) {
ContentMode.READING
} else {
ContentMode.QUIZ
}
BooksView(navController = navController, mode = mode)
}
}
AndroidView(
modifier = Modifier
.fillMaxWidth()
.height(IntrinsicSize.Max)
.weight(1f),
factory = { context ->
AdView(context).apply {
setAdSize(AdSize.BANNER)
adUnitId = AdIdentifiers.GlobalBanner.adID
loadAd(AdRequest.Builder().build())
}
}
)
}
It might be the issue of any child Composable of NavHost having Modifier.fillmaxSize(). You can set a Modifier for NavHost which let's you define how much space it will cover
Column(modifier = Modifier.fillMaxSize()){
NavHost(modifier = Modifier.fillmaxWidth).weight(1f){}
AdView(modifier = Modifier
.fillMaxWidth()
.height(IntrinsicSize.Max)
)
}
You can put AndroidView in Box and give androidView height as bottom padding values in screens.
NavHost(
//
){
//
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.BottomCenter
){
AndroidView()
}

compose LazyColumn crops the content at the bottom

I have empty fragment with composable:
setContent {
Surface(
modifier = Modifier
.fillMaxWidth().fillMaxHeight().padding(bottom = 48.dp, top = 16.dp),
color = colorResource(id = R.color.usaa_white)
) {
val itemsList = (0..50).toList()
val itemsIndexedList = listOf("A", "B", "C")
LazyColumn(
) {
items(itemsList.size) {
Text("Item is $it")
}
item {
Text("Single item")
}
itemsIndexed(itemsIndexedList) { index, item ->
Text("Item at index $index is $item")
}
}
}
}
the problem is: I can only scroll the content until "Single Item" row and the rest of content is hidden. I added some padding to make sure that it wasn't bottomNavBar covering the list but it's still cropped.
Looks like the issue is caused by bottomNavBar. What's interesting is that it happens only with LazyColumn and works fine when I use Column
The fix I found is to add contentPadding to the bottom. (But hope to find better solution)
LazyColumn(contentPadding = PaddingValues(bottom = 70.dp)) { }
Use Scaffold (check documentation).
Scaffold has a generic content trailing lambda slot. The lambda receives an instance of PaddingValues that should be applied to the content root — for example, via Modifier.padding — to offset the top and bottom bars, if they exist.
setContent {
Scaffold { contentPadding ->
Box(
modifier = Modifier.padding(contentPadding)
) {
// Your code
}
}
}
Hope it helps !

Jetpack Compose Bottomsheet with empty sheet content is always expnaded

I'm trying to achieve modalBottomSheet by BottomSheetScaffold for some custom implementation.
here is my BottomSheetScaffold
BottomSheetScaffold(
sheetPeekHeight = 0.dp,
scaffoldState = bottomSheetState,
sheetBackgroundColor = Color.Transparent,
backgroundColor = Color.Transparent,
sheetElevation = 0.dp,
sheetShape = RoundedCornerShape(topStart = 36.dp, topEnd = 36.dp),
snackbarHost = snackBarHost,
sheetContent = { bottomSheet() }) {
Box(Modifier.fillMaxSize()) {
val coroutineScope = rememberCoroutineScope()
sheetContent()
Scrim(
color = Primary,
alpha = bottomSheetState.currentFraction * 0.5f,
onDismiss = {
coroutineScope.launch { bottomSheetState.bottomSheetState.collapse() }
},
visible = bottomSheetState.bottomSheetState.targetValue != BottomSheetValue.Collapsed && bottomSheetState.bottomSheetState.currentValue != BottomSheetValue.Collapsed
)
}
}
When this scaffold is called by some screen, the sheetContent() will be replaced as screen content. My problem here is when bottomSheet() is empty on that screen and thus there is no height, bottom sheet state think it is expanded while I just not put composable inside bottomSheet() and it just fill based on some condition with no default composable. Because of that the Scrim() function will be visible and when I click on it this exception will throw
java.lang.IllegalArgumentException: The target value must have an associated anchor.
It seems while sheetContent is necessary for BottomSheetScaffold there is no way to deal with empty value because BottomSheetState class that handle's swiping need anchor to get height and empty value cause's unexpected result
This is a bug introduced in latest release of compose 1.2.x. To stop showing bottom sheet at start, I only add sheetContent if data is not null(this was not possible in previous release1.1.x). Now I have another issue. The click event to expanding bottomSheet won't trigger at first try. I always need to click it twice.

How to implement BottomAppBar and BottomDrawer pattern using Android Jetpack Compose?

I'm building Android app with Jetpack Compose. Got stuck while trying to implement BottomAppBar with BottomDrawer pattern.
Bottom navigation drawers are modal drawers that are anchored to the bottom of the screen instead of the left or right edge. They are only used with bottom app bars. These drawers open upon tapping the navigation menu icon in the bottom app bar.
Description on material.io, and direct link to video.
I've tried using Scaffold, but it only supports side drawer. BottomDrawer appended to Scaffold content is displayed in content area and BottomDrawer doesn't cover BottomAppBar when open. Moving BottomDrawer after Scaffold function doesn't help either: BottomAppBar is covered by some invisible block and prevents clicking buttons.
I've also tried using BottomSheetScaffold, but it doesn't have BottomAppBar slot.
If Scaffold doesn't support this pattern, what would be correct way to implement it? Is it possible to extend Scaffold component? I fear that incorrect implementation from scratch might create issues later, when I'll try to implement navigation and snackbar.
I think the latest version of scaffold does have a bottom app bar parameter
They (Google Devs) invite you in the Jetpack Compose Layouts pathway to try adding other Material Design Components such as BottomNavigation or BottomDrawer to their respective Scaffold slots, and yet do not give you the solution.
BottomAppBar does have its own slot in Scaffold (i.e. bottomBar), but BottomDrawer does not - and seems to be designed exclusively for use with the BottomAppBar explicitly (see API documentation for the BottomDrawer).
At this point in the Jetpack Compose pathway, we've covered state hoisting, slots, modifiers, and have been thoroughly explained that from time to time we'll have to play around to see how best to stack and organize Composables - that they almost always have a naturally expressable way in which they work best that is practically intended.
Let me get us set up so that we are on the same page:
class MainActivity : ComponentActivity() {
#ExperimentalMaterialApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
LayoutsCodelabTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
LayoutsCodelab()
}
}
}
}
}
That's the main activity calling our primary/core Composable. This is just like in the codelab with the exception of the #ExperimentalMaterialApi annotation.
Next is our primary/core Composable:
#ExperimentalMaterialApi
#Composable
fun LayoutsCodelab() {
val ( gesturesEnabled, toggleGesturesEnabled ) = remember { mutableStateOf( true ) }
val scope = rememberCoroutineScope()
val drawerState = rememberBottomDrawerState( BottomDrawerValue.Closed )
// BottomDrawer has to be the true core of our layout
BottomDrawer(
gesturesEnabled = gesturesEnabled,
drawerState = drawerState,
drawerContent = {
Button(
modifier = Modifier.align( Alignment.CenterHorizontally ).padding( top = 16.dp ),
onClick = { scope.launch { drawerState.close() } },
content = { Text( "Close Drawer" ) }
)
LazyColumn {
items( 25 ) {
ListItem(
text = { Text( "Item $it" ) },
icon = {
Icon(
Icons.Default.Favorite,
contentDescription = "Localized description"
)
}
)
}
}
},
// The API describes this member as "the content of the
// rest of the UI"
content = {
// So let's place the Scaffold here
Scaffold(
topBar = {
AppBarContent()
},
//drawerContent = { BottomBar() } // <-- Will implement a side drawer
bottomBar = {
BottomBarContent(
coroutineScope = scope,
drawerState = drawerState
)
},
) {
innerPadding ->
BodyContent( Modifier.padding( innerPadding ).fillMaxHeight() )
}
}
)
}
Here, we've leveraged the Scaffold exactly as the codelab in the compose pathway suggests we should. Notice my comment that drawerContent is an auto-implementation of the side-drawer. It's a rather nifty way to bypass directly using the [respective] Composable(s) (material design's modal drawer/sheet)! However, it won't work for our BottomDrawer. I think the API is experimental for BottomDrawer, because they'll be making changes to add support for it to Composables like Scaffold in the future.
I base that on how difficult it is to use the BottomDrawer, designed for use solely with BottomAppBar, with the Scaffold - which explicitly contains a slot for BottomAppBar.
To support BottomDrawer, we have to understand that it is an underlying layout controller that wraps the entire app's UI, preventing interaction with anything but its drawerContent when the drawer is open. This requires that it encompasses Scaffold, and that requires that we delegate necessary state control - to the BottomBarContent composable which wraps our BottomAppBar implementation:
#ExperimentalMaterialApi
#Composable
fun BottomBarContent( modifier: Modifier = Modifier, coroutineScope: CoroutineScope, drawerState: BottomDrawerState ) {
BottomAppBar{
// Leading icons should typically have a high content alpha
CompositionLocalProvider( LocalContentAlpha provides ContentAlpha.high ) {
IconButton(
onClick = {
coroutineScope.launch { drawerState.open() }
}
) {
Icon( Icons.Filled.Menu, contentDescription = "Localized description" )
}
}
// The actions should be at the end of the BottomAppBar. They use the default medium
// content alpha provided by BottomAppBar
Spacer( Modifier.weight( 1f, true ) )
IconButton( onClick = { /* doSomething() */ } ) {
Icon( Icons.Filled.Favorite, contentDescription = "Localized description" )
}
IconButton( onClick = { /* doSomething() */ } ) {
Icon( Icons.Filled.Favorite, contentDescription = "Localized description" )
}
}
}
The result shows us:
The TopAppBar at top,
The BottomAppBar at bottom,
Clicking the menu icon in the BottomAppBar opens our BottomDrawer, covering the BottomAppBar and entire content space appropriately while open.
The BottomDrawer is properly hidden, until either the above referenced button click - or gesture - is utilized to open the bottom drawer.
The menu icon in the BottomAppBar opens the drawer partway.
Gesture opens the bottom drawer partway with a quick short swipe, but as far as you guide it to otherwise.
You may have to do something as shown below..
Notice how the Scaffold is called inside the BottomDrawer().
It's confusing though how the documentation says "They (BottomDrawer) are only used with bottom app bars". It made me think I have to look for a BottomDrawer() slot inside Scaffold or that I have to call BottomDrawer() inside BottomAppBar(). In both cases, I experienced weird behaviours. This is how I worked around the issue. I hope it helps someone especially if you are attempting the code lab exercise in Module 5 of Layouts in Jetpack Compose from the Jetpack Compose course.
#ExperimentalMaterialApi
#Composable
fun MyApp() {
var selectedItem by rememberSaveable { mutableStateOf(1)}
BottomDrawer(
modifier = Modifier.background(MaterialTheme.colors.onPrimary),
drawerShape = Shapes.medium,
drawerContent = {
Column(Modifier.fillMaxWidth()) {
for(i in 1..6) {
when (i) {
1 -> Row(modifier = Modifier.clickable { }.padding(16.dp)){
Icon(imageVector = Icons.Rounded.Inbox, contentDescription = null)
Text(text = "Inbox")
}
2 -> Row(modifier = Modifier.clickable { }.padding(16.dp)){
Icon(imageVector = Icons.Rounded.Outbox, contentDescription = null)
Text(text = "Outbox")
}
3 -> Row(modifier = Modifier.clickable { }.padding(16.dp)){
Icon(imageVector = Icons.Rounded.Archive, contentDescription = null)
Text(text = "Archive")
}
}
}
}
},
gesturesEnabled = true
) {
Scaffold(
topBar = {
TopAppBar(
title = {
Text(text = "Learning Compose Layouts" )
},
actions = {
IconButton(onClick = { /*TODO*/ }) {
Icon(Icons.Filled.Favorite, contentDescription = null)
}
}
)
},
bottomBar = { BottomAppBar(cutoutShape = CircleShape, contentPadding = PaddingValues(0.dp)) {
for (item in 1..4) {
BottomNavigationItem(
modifier = Modifier.clipToBounds(),
selected = selectedItem == item ,
onClick = { selectedItem = item },
icon = {
when (item) {
1 -> { Icon(Icons.Rounded.MusicNote, contentDescription = null) }
2 -> { Icon(Icons.Rounded.BookmarkAdd, contentDescription = null) }
3 -> { Icon(Icons.Rounded.SportsBasketball, contentDescription = null) }
4 -> { Icon(Icons.Rounded.ShoppingCart, contentDescription = null) }
}
}
)
}
}
}
) { innerPadding -> BodyContent(
Modifier
.padding(innerPadding)
.padding(8.dp))
}
}
}

Categories

Resources