My app needs ModalDrawer & ModalBottomSheetLayout at the same time.
I searched but couldn't find the officially recommended way to accomplish it.
So I just put ModalDrawer inside ModalBottomSheetLayout. Please check the below code.
#Composable
fun MainScreen()
{
ModalBottomSheetLayout(
sheetState = recipeViewModel.bottomSheetState,
sheetContent = { #AnotherComposable },
sheetShape = RoundedCornerShape(topStart = 50.dp, topEnd = 50.dp)
) {
// "Rest-Content Area"
Column(
modifier = Modifier
.padding(10.dp)
.fillMaxWidth()
) {
Buttons()
}
// Drawer
ModalDrawerSample()
}
}
ModalDrawerSample() working fine without any issue.
However, the UIs in the "Rest-Content Area" is not working. For example, button clicking it not working at all.
When we press the button, the color of the button area should be changed (gray like colored) but it's not.
Is there any official document that recommend the best practice or any other solution for it?
Related
I have BottomDialog Fragment which consists only of Composable (Row(title) and LazyColumn).
The first issue I faced was when you scroll your list down and then you try to scroll up list won't scroll and the dialog starts to minimize. This is solved with
modifier = Modifier.nestedScroll(rememberNestedScrollInteropConnection())
But now user can't minimize a dialog when he tries to do it by touching a title. And there is my question, How to solve this?
Minimum reproducible code
During creating this example I found that I can maximize dialog when touching the title, also I can start moving the action going to the top with my finger (to start to expand it) and then move the finger to the bottom of the screen, in this way the dialog will be dismissed, but still can't minimize it in a non-tricky way.
ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
Theme {
Column(modifier = Modifier.nestedScroll(rememberNestedScrollInteropConnection())) {
Row {
Text(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
text = "Title"
)
}
LazyColumn(
Modifier
.weight(1f)
.fillMaxWidth()
) {
items(100) {
Text(
text = "Item $it",
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
)
}
}
}
}
}
}
Please do not propose BottomSheetScaffold. Because I want to create a standardized bottom dialog. And to use it I will need just pass a list of items and a Compose function for one item.
IMO BottomSheetScaffold shouldn't be released at all, cause it was designed "not well". Just imagine earlier before Jetpack Compose you write your code around the bottom dialog, nice layering. No.
As a temp decision. I just think of LazyColumn works properly so I need to wrap my header to the LazyColumn.
So I created this function. And Just pass here any Composable
#Composable
fun TrickyLazyColumn(content: #Composable () -> Unit) {
LazyColumn {
items(
items = listOf(1),
itemContent = {
content.invoke()
})
}
}
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.
I want to display a sticky bar at the bottom of the screen to show dynamic or static data similar to the bars in the image below. Can this be achieved with BottomSheetScaffold on screen load without user interaction? If not, how else can this be implemented?
EDIT: I want to achieve this in Jetpack Compose.
This UI collection might be a good starting point for you to see which elements/classes you need. It looks like you're depicting a BottomSheet element? Maybe an AndroidSweetSheet element - Both of these look like they'd suit your purpose. You don't need the animations or can turn off the resizable, if you want them static. I'd start there with that huge collection of curated UI elements/widgets anyway, see if those two, or perhaps some other of the many UI element better fits your purpose.
For example, this is the AndroidSweetSheet: (1)
And heres the BottomSheet (2):
Let the page load (lots of example images so may take a little while depending on your connection/CPU) but theres several that look like what you want. May have extras that you can turn off, such as the resizable nature of that AndroidSweetSheet, you can have it appear rather than slide up & scroll
Just having a look through that UI elements page (for my own app purposes) I noticed this one 3 which might suit your purposes - imagine having a star pop up instead of the play icon, and when the user presses the star, it reveals your "Go premium" panel instead of the music player widget:
OP commented that they're specifically looking for jetpack compose - what about this bottom-sheet?
Although this example looks very bare-bones, the structure looks pretty close to what you're describing in yout text/images:
You'd probably have the pop-up controlled so that it pops-up when you want it to, instead of the user clicking a button.
Edits: some more examples
This code tutorial also suggests building a bottom sheet using jetpack compose.
I'd think you'd probably want your "go premium" box to be modal, and this page does just that, again with jetpack compose - looks prettier, too!
You can consider this one where you can utilize LaunchedEffect with a Unit key to show the bottom sheet immediately on first composition, and closing it using an Icon placed at the end of the gray "header".
All these codes are copy-and-paste-able so you can run it with no issues.
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun MyScreenAutoShowBottomSheet() {
val contentBackground = Color(0xFFa593b8)
val sheetPeekHeight = 40.dp
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = BottomSheetState(BottomSheetValue.Collapsed)
)
LaunchedEffect(key1 = Unit){
bottomSheetScaffoldState.bottomSheetState.expand()
}
val coroutineScope = rememberCoroutineScope()
BottomSheetScaffold(
scaffoldState = bottomSheetScaffoldState,
sheetBackgroundColor = contentBackground,
sheetElevation = 0.dp,
sheetPeekHeight = sheetPeekHeight,
sheetContent = {
Column(
modifier = Modifier
.padding(top = sheetPeekHeight)
.wrapContentHeight()
.fillMaxWidth()
.background(Color.DarkGray)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
modifier = Modifier.padding(8.dp),
text = "Go Premium"
)
Column(
modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.End
) {
Icon(
modifier = Modifier
.clickable {
coroutineScope.launch {
bottomSheetScaffoldState.bottomSheetState.collapse()
}
},
imageVector = Icons.Default.Add,
contentDescription = null
)
}
}
Box(
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.background(Color(0xFF4fc992))
)
}
},
floatingActionButton = {
FloatingActionButton(
backgroundColor = Color(0xFF4a6ebd),
shape = CircleShape,
onClick = {},
) {
Icon(imageVector = Icons.Filled.Add, contentDescription = "icon")
}
}
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(contentBackground)
)
}
}
A simpler solution without the use of sheets is to use a Box composable with the last element having a Modifier.align(Alignment.BottomCenter) modifier:
Box(
modifier = Modifier.fillMaxSize()
) {
//Main screen contents
Column() {...}
...
Row (
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.height(50.dp)
.background(Color(0xFF4fc992))
.padding(10.dp),
verticalAlignment = Alignment.CenterVertically
) {
//Bottom bar contents
}
}
I am struggling with the jetpack compose LazyColumn and the stickyHeader functionality. Basically the static view works well, but once I start scrolling, the items would go over the sticky headers, the scrolling starts a weird behaviour and the last item would never be visible as the scrolling always bounces back.
Here's how it looks like:
Here's the composable:
#OptIn(ExperimentalFoundationApi::class)
#Composable
fun CollectionsScreen(
collectionsLive: LiveData<List<CollectionsView>>,
onCollectionChanged: (ICalCollection) -> Unit
/* some more hoisted functions left out for simplicity */
) {
val list by collectionsLive.observeAsState(emptyList())
val grouped = list.groupBy { it.accountName ?: it.accountType ?: "Account" }
LazyColumn(
modifier = Modifier.padding(8.dp)
) {
item {
Text(
stringResource(id = R.string.collections_info),
textAlign = TextAlign.Center,
modifier = Modifier.padding(bottom = 16.dp)
)
}
grouped.forEach { (account, collectionsInAccount) ->
stickyHeader {
Text(
account,
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(
top = 16.dp,
start = 8.dp,
end = 16.dp,
bottom = 8.dp
)
)
}
items(
items = collectionsInAccount,
key = { collection -> collection.collectionId }
) { collection ->
CollectionCard(
collection = collection,
allCollections = list,
onCollectionChanged = onCollectionChanged,
/* some more hoisted functions left out for simplicity */
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp)
.animateItemPlacement()
.combinedClickable(
//onClick = { onCollectionClicked(collection) }
)
)
}
}
}
}
I am really not sure what is causing this issue as the code itself is pretty straightforward from the example provided in the documentation. Only the CollectionCard itself is a more complex structure.
I have also tried removing the header text (the first item) and removed the Modifier.animateItemPlacement() for the card, but with no difference, the problem stays the same...
The composable itself is used in a Compose View within a Fragment, but there is no nested scrolling.
Do you have any idea what could cause this strange behaviour? Or might this be a bug when using cards within the LazyColumn with sticky headers?
UPDATE:
It seems like the problem nothing to do with the stickyHeader, but somehow with the LazyColumn. If I replace the "stickyHeader" just with "item", the problem still persists... Only when I replace the lazyColumn with a column it would work. But I assume that there must be a solution for this problem...
Setting the stickyHeader background color will help.
stickyHeader {
Text(
"text",
modifier = Modifier.padding(
top = 16.dp,
start = 8.dp,
end = 16.dp,
bottom = 8.dp
)
.background(colorResource(id = R.color.white))
)
}
I don't know if you solved it yet, but try to fillMaxWidth and set the background. This code worked for me.
Text(
account,
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
modifier = Modifier
.padding(
top = 16.dp,
start = 8.dp,
end = 16.dp,
bottom = 8.dp
)
.fillMaxWidth()
.background(MaterialTheme.colors.background)
)
In general, if you are using Material or Material3 theming, you can wrap your stickyHeader content in a Surface to automatically make it non-transparent with your theme's standard (or customized) coloring scheme. Surface lets you raise the stickyHeader above your table's other contents.
stickyHeader {
Surface(Modifier.fillParentMaxWidth()) {
Text("Header")
}
}
You can customize the Surface at your heart's desire.
I'd create another issue for the bounciness problem, it looks like a separate concern.
Just provide a background for the sticky header.
I was playing around with Jetpack Compose TextField and I found one strange behaviour with Keyboard.
If my TextField is around bottom of the screen and I open keyboard, the TextField is remain hidden behind the keyboard.
I tried some solutions as well.
Modifying android:windowSoftInputMode="adjustPan" and android:windowSoftInputMode="adjustResize"
If I use adjustPan, sometimes TextField is lifted up with the Keyboard but sometimes it does not.
Here is the code and images of what is happening.
Ok I did something that worked for me, I clarify that I have only been learning compose for a few weeks so don't expect much from me, I simply did something that may not be the right way but it is functional for me and may be useful to anyone.
What I did was create a main class that inherits the ComponentActivity() with all the toys.
class Login : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
HealthAtHansClientTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
LoginLayout()
}
}
}
}
}
After that I imported my #Composable function which contained my layout with the LoginLayout, but the important part is this:
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.padding(bottom = 100.dp, end = 40.dp, start = 40.dp)
.verticalScroll(scrollState),
verticalArrangement = Arrangement.SpaceAround,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 30.dp)
) {
Text(text = "Hola !", style = TextStyle(fontSize = 40.sp))
}
Row(horizontalArrangement = Arrangement.Center) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
TextField(
maxLines = 1,
modifier = Modifier.fillMaxWidth(),
value = documentNumber,
onValueChange = { documentNumber = it })
Spacer(modifier = Modifier.height(100.dp))
TextField(
maxLines = 1,
modifier = Modifier.fillMaxWidth(),
value = privateCode,
onValueChange = { privateCode = it })
}
}
}
Note that the space between the Textfields is 100 this was just to test that it is functional.
and finally declare the Login class in my main activity in the following way, since the Mainactivity came by default but it was only for this test, the idea is only that they realize the line that I added
And voilà that was all now my TextField is not hidden, I think it would be very feasible if you have extensive forms you create your "activity" in the manifest and add the line android:windowSoftInputMode="adjustResize"
that way the problem is over.
I hope someone with more skill and knowledge can do something more efficient, for now it works for me.
If there is already a more efficient solution for that, the comment is appreciated, if my solution is stupid, I would appreciate feedback to learn.
One other thing that happens is that if you run the emulator with 'DefaultPreview', none of the ways I try to do it work.
If you run the emulation of your application in App, it works without problem.
But I wanted to go further, so I compiled this example in a release apk version where it is supposed to look like it should and it works perfectly as you can see in the image.
Update at October 2022: The following code does the trick:
class SampleActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
WindowCompat.setDecorFitsSystemWindows(window, false)
super.onCreate(savedInstanceState)
setContent {
LazyColumn(
contentPadding = WindowInsets.navigationBars.asPaddingValues()
) {
// items
}
}
}
}
Setting Activity to fit system windows and also using contentPadding is key here.