Recently I've been trying Material3 combined with Jetpack Compose, and I need a drawer along with a scaffold in one of my screen. It's sad that Scaffold in material3 hasn't support drawer yet, but luckily ModalNavigationDrawer can be found as a substitute. However, using ModalNavigationDrawer, the drawer's content is always covering the whole screen when drawer is opened, and I can't find any parameter to set it's width to a proper value, e.g. half of the screen width. Is there any way I can solve this problem?
My compose version is 1.2.0-beta02 and my material3 version is 1.0.0-alpha12.
Use a ModalDrawerSheet in the drawerContent and then set its modifier to the desired width:
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
ModalDrawerSheet(
modifier = Modifier
.width(300.dp) // or your desired width
.fillMaxHeight()
) {
...Your Drawer Content...
}
}
)
Code sample on the Material3 developer's page
and the ModalDrawerSheet docs
From the source code of ModalNavigationDrawer, the max-width is set in modifier and the drawer content's column is set to fill max size so by default it will take NavigationDrawerTokens.ContainerWidth's value as width.
But a work-around exists here, what you can do is you need to set drawerContainerColor as Color.Transparent and put another column or box inside the drawerContent with your required size.
You can use requiredWidth modifier to fix the required width or you can use sizeIn modifier according to min and max-width.
ModalNavigationDrawer(
drawerContent = {
Column(
modifier = Modifier
.requiredWidth(250.dp)
.fillMaxHeight()
.background(Color.White, RoundedCornerShape(topEnd = 16.dp, bottomEnd = 16.dp))
) {
//Drawer Content
}
},
drawerContainerColor = Color.Transparent
) {
//Main Content
}
Update with click to close the drawer on tap of the background -
If you want to have the ripple effect on Spacer's clickable just remove the interactionSource and indication parameters.
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope()
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
Row(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier
.requiredWidth(250.dp)
.fillMaxHeight()
.background(
Color.White,
RoundedCornerShape(topEnd = 16.dp, bottomEnd = 16.dp)
)
) {
//Drawer Content
}
Spacer(
modifier = Modifier
.fillMaxSize()
.clickable(
interactionSource = MutableInteractionSource(),
indication = null
) {
scope.launch {
if (drawerState.isOpen) {
drawerState.close()
}
}
},
)
}
},
drawerContainerColor = Color.Transparent
) {
//Main Content
}
I have tweaked #Priyank-Jain's answer a little bit to clean up the elevation left behind by the original drawer. Remove the elevation from the original draw and add it to the newly created one
val scope = rememberCoroutineScope()
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
Row(modifier = Modifier.fillMaxWidth()) {
Surface(
modifier = Modifier
.requiredWidth(320.dp)
.fillMaxHeight(),
color = Color.White,
shape = RoundedCornerShape(topEnd = 16.dp, bottomEnd = 16.dp),
elevation = 16.dp
) {
//Drawer Content
}
Spacer(
modifier = Modifier
.fillMaxSize()
.clickable(
interactionSource = MutableInteractionSource(),
indication = null
) {
scope.launch {
if (drawerState.isOpen) {
drawerState.close()
}
}
},
)
}
},
drawerContainerColor = Color.Transparent,
drawerElevation = 0.dp,
) {
//Main Content
}
Related
Recently, I try to migrate to Jetpack compose.
So, I want to show 'card' where has 'indicator' while loading
but I can't change 'indicator' size in 'SubcomposeAsyncImage'
#Composable
private fun SponsorDialogContent(
imgUrl: String,
) {
Card(
modifier = Modifier
.width(300.dp)
.wrapContentHeight(),
elevation = CardDefaults.cardElevation(0.dp),
shape = RoundedCornerShape(10.dp)
) {
Box() {
SubcomposeAsyncImage(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.heightIn(min = 200.dp),
model = imgUrl,
contentDescription = null,
loading = {
CircularProgressIndicator(modifier = Modifier.size(30.dp).align(Alignment.Center), progress = 1f)
},
alignment = Alignment.Center,
contentScale = ContentScale.FillWidth
)
CircularProgressIndicator(1f, Modifier.width(30.dp).align(Alignment.Center))
}
}
}
So. I have try the code below
#Composable
private fun SponsorDialogContent(
imgUrl: String,
) {
Card(
modifier = Modifier
.width(300.dp)
.wrapContentHeight(),
elevation = CardDefaults.cardElevation(0.dp),
shape = RoundedCornerShape(10.dp)
) {
Box() {
SubcomposeAsyncImage(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.heightIn(min = 200.dp),
model = imgUrl,
contentDescription = null,
loading = {
Box(contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
},
alignment = Alignment.Center,
contentScale = ContentScale.FillWidth
)
}
}
}
Then, works well, but I don't know why
Does anyone know why?
I tried to find how to work this code but failed
As #Jan Bina mentioned it has propagateMinConstraints = true which means it forces minimum constraints to its direct child only. I explained in this answer how it effects with Surface because Surface is a Box with propagateMinConstraints = true. You can try out these with Box and you will see the same outcomes
Surface(
modifier = Modifier.size(200.dp),
onClick = {}) {
Column(
modifier = Modifier
.size(50.dp)
.background(Color.Red, RoundedCornerShape(6.dp))
) {}
}
Spacer(modifier = Modifier.height(20.dp))
Surface(
modifier = Modifier.size(200.dp),
onClick = {}) {
Column(
modifier = Modifier
.size(50.dp)
.background(Color.Red, RoundedCornerShape(6.dp))
) {
Column(
modifier = Modifier
.size(50.dp)
.background(Color.Green, RoundedCornerShape(6.dp))
) {}
}
}
Spacer(modifier = Modifier.height(20.dp))
Box(
modifier = Modifier.size(200.dp)
) {
Column(
modifier = Modifier
.size(50.dp)
.background(Color.Red, RoundedCornerShape(6.dp))
) {
Column(
modifier = Modifier
.size(50.dp)
.background(Color.Green, RoundedCornerShape(6.dp))
) {}
}
}
In first example on Surface forces Column to have 200.dp size even though it has Modifier.size(50.dp).
In second example Box inside Column has 50.dp size because it's not a direct descendant of Surface.
In third example if we replace Surface(Box with propagateMinConstraints true) with Box it allows direct descendant to use its own constraints or dimensions.
And when you set Modifier.fillMaxWidth you set Constraints as minWidth = parent width, maxWidth = parentWidth
I answered about Constraint types here
If you browse the source code of SubcomposeAsyncImage, you will get to the point where you can see this:
BoxWithConstraints(
modifier = modifier, // this is the modifier you passed to SubcomposeAsyncImage
contentAlignment = alignment,
propagateMinConstraints = true,
) {
// image or your loading content based on state
}
So the behavior you describe is because of your modifier that has heightIn(min = 200.dp) and your progress indicator that ends up being direct child of this Box which has propagateMinConstraints = true.
Is there a way to set height of elements in a Scaffold to be exactly the same? I want both the top bar and the content to occupy half of the screen's height
Scaffold(
topBar = {
MyLargeTopAppBar(modifier = Modifier.height(1f))
},
content = {
LazyColumn(modifier = Modifier.padding(it).height(1f)) {
...
}
},
containerColor = MaterialTheme.colorScheme.background
)
Arthur Kasparian's suggestion
You can skip using a scaffold if none of its other elements are needed, the layout then simply becomes a column like so:
Column(
modifier = Modifier.fillMaxSize()
) {
TopAppBar(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {}
Content(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
)
}
The height of the content body is determined in part by the topBar body height, and the weight setter is inaccessible in those scopes, so I don't think it's possible. However what you can do is simply include the top bar in the content:
val configuration = LocalConfiguration.current
val screenHeight = configuration.screenHeightDp.dp
val size = screenHeight / 2
Scaffold(
content = {
Column {
TopAppBar(
modifier = Modifier
.height(size)
.weight(1f)
.fillMaxWidth()
) {
Text("$size")
}
Box(
modifier = Modifier
.height(size)
.weight(1f)
.fillMaxWidth()
.background(Color.Red)
) {
Text("$size")
}
}
},
)
You get something like this:
I have comment box and need to put icon on specific position ( bottom right ). I need to make something like position absolute where my icon button need to be bottom right inside comment box. Here is image what I am trying to achieve. Any help or idea?
You can do it by using Modifier.offset{} and putting your Icon inside a Box with Modifier.align(Alignment.BottomEnd)
#Composable
private fun Test() {
Column(modifier = Modifier.padding(10.dp)) {
Box(
modifier = Modifier
.width(200.dp)
.background(Color.LightGray.copy(alpha = .5f), RoundedCornerShape(8.dp))
.padding(4.dp)
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
Text("Title", fontSize = 20.sp)
Text("Comment")
}
val offsetInPx = with(LocalDensity.current) {
16.dp.roundToPx()
}
Icon(
imageVector = Icons.Default.Settings,
contentDescription = null,
modifier = Modifier
.offset {
IntOffset(-offsetInPx, offsetInPx)
}
.shadow(2.dp, RoundedCornerShape(40))
.background(Color.White)
.padding(horizontal = 10.dp, vertical = 4.dp)
.size(30.dp)
.align(Alignment.BottomEnd),
tint = Color.LightGray
)
}
Spacer(modifier = Modifier.height(4.dp))
Text("Reply", color = Color.Blue)
}
}
I have developed my bottom sheet in Compose.
The goal is to have the top of the bottom sheet (where the picture is) to be transparent to see the background picture.
The rest of the bottom sheet with the text will have a background
Here is what I came up with:
#ExperimentalMaterialApi
#Composable
fun EntryFragment() {
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = rememberBottomSheetState(BottomSheetValue.Collapsed)
)
BottomSheetScaffold(
scaffoldState = bottomSheetScaffoldState,
sheetContent = {
Box(
Modifier
.fillMaxWidth()
.fillMaxHeight().background(Color.Transparent)
) {
Column(Modifier.background(Color.Transparent)) {
Image(
painter = painterResource(R.drawable.main_screen_picture),
contentDescription = "Picture",
/*contentScale = ContentScale.Crop, */
modifier = Modifier.width(150.dp).height(150.dp)
)
Box(
Modifier
.fillMaxWidth()
.fillMaxHeight()
.background(Color.Blue)
){
Text(text = "Hello from sheet")
}
}
}
}, sheetPeekHeight = 600.dp
, modifier = Modifier.background(Color.Green)
) {
//Under the bottom sheet
Image(
painter = painterResource(R.drawable.main_screen_background),
contentDescription = "Background",
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxWidth()
)
}
}
I have this issue where there is a white/black (depending on day/night) background put next to my picture.
Apparently it comes from the column, but everything has a transparent background.
I put a green background in my bottom sheet, and it is not shown, so the issue must come from above.
Where this issue comes from?
It's controlled by sheetBackgroundColor parameter of BottomSheetScaffold. As the next step you probably gonna need to disable shadows too:
sheetBackgroundColor = Color.Transparent,
sheetElevation = 0.dp,
Try to put your background as a first Modifier because the order matters. For example:
Box(
Modifier
.background(Color.Transparent)
.fillMaxWidth()
.fillMaxHeight()
)
There is no ripple effect when I click on MyBox() I've added MyTheme(){} to main #Composable screen but it doesn't work. Is something missing?
#Composable
private MyBox(onClickInvoked: () -> Unit) {
MyAppTheme(isSystemInDarkTheme()) {
Box(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.clip(RoundedCornerShape(10.dp))
.background(MaterialTheme.colors.onBackground)
.clickable(onClick = { onClickInvoked.invoke() })
.padding(horizontal = 10.dp, vertical = 15.dp)
) {
Text(
text = "My text",
modifier = Modifier
.align(Alignment.CenterStart)
.padding(end = 95.dp)
.wrapContentWidth()
.wrapContentHeight(),
color = MaterialTheme.colors.primary
)
Image(
painter = painterResource(R.drawable.icon),
modifier = Modifier
.align(Alignment.CenterEnd)
.padding(end = 20.dp)
.size(60.dp)
)
}
}
}
With compose 1.0.5, I see the default indication after set clickable is
LocalIndication.current. And LocalIndication.current is PlatformRipple. Therefore, after you set clickable to your Box, it will have ripple effect.
In your case, I think the ripple effect won't display because your Box background is too dark (normally MaterialTheme.colors.onBackground is black on Light theme)
I think you can change the ripple effect color to make it easy to see.
Surface(
onClick = { onClickInvoked.invoke() },
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(10.dp),
color = MaterialTheme.colors.onBackground, // normally it is black on Light theme
indication = rememberRipple(color = Color.White) // color for your ripple, you can use other suitable MaterialTheme.colors for your case to support Light/Dark mode
) {
Box(modifier = Modifier.padding(horizontal = 10.dp, vertical = 15.dp)) {
// your Box content
...
}
}
There is a Modifier.indication but after testing I see it not working with Modifier.clickable so I use Surface
I don't really know if there is a problem with your theme. I try to pass the material theme, and the ripples will display normally
MaterialTheme() {
Box(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.clip(RoundedCornerShape(10.dp))
.background(MaterialTheme.colors.secondary)
.clickable(onClick = { t = "My text" })
.padding(horizontal = 10.dp, vertical = 15.dp)
) {
Text(
text = t,
modifier = Modifier
.align(Alignment.CenterStart)
.padding(end = 95.dp)
.wrapContentWidth()
.wrapContentHeight(),
color = MaterialTheme.colors.primary
)
Image(
painter = painterResource(R.drawable.ic_launcher_foreground),
modifier = Modifier
.align(Alignment.CenterEnd)
.padding(end = 20.dp)
.size(60.dp),
contentDescription = ""
)
}
}