Jetpack Compose: component with bottom shadow only - android-jetpack-compose

I'm struggling with implementing the UI component.
I want to achive something like this:
A box with only bottom shadow.
For now I'm able to add elevation but it add's shadow in every direction.
This is my current code and it's preview:
#Composable
fun PushNotificationsDisabledInfo(onTap: () -> Unit) {
Surface(
elevation = dimensionResource(R.dimen.card_elevation_big),
shape = RoundedCornerShape(dimensionResource(R.dimen.corner_radius_large)),
modifier = Modifier
.background(
color = colorResource(
id = R.color.white
)
)
.padding(dimensionResource(R.dimen.grid_2_5x))
) {
Box(
Modifier
.clip(shape = RoundedCornerShape(dimensionResource(R.dimen.corner_radius_large)))
.background(
color = colorResource(R.color.white)
)
.clickable(
onClick = { onTap() },
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = true),
)
) {
Row(Modifier.padding(dimensionResource(R.dimen.grid_2x))) {
Image(
painter = painterResource(R.drawable.ic_error_big),
contentDescription = stringResource(R.string.empty_content_description),
modifier = Modifier.size(dimensionResource(R.dimen.grid_4x))
)
Spacer(modifier = Modifier.width(dimensionResource(R.dimen.grid_2x)))
Column {
Text(
text = stringResource(R.string.notifications_settings_push_notifications_disabled_title),
style = SiemensTextStyle.caption1,
color = colorResource(R.color.red)
)
Text(
text = stringResource(R.string.notifications_settings_push_notifications_disabled_message),
style = SiemensTextStyle.caption2,
color = colorResource(R.color.black)
)
}
}
}
}
}
Any ideas how to implement only bottom shadow using Compose?

Copy-paste the following extension & use it in your Card's Modifier:
private fun Modifier.bottomElevation(): Modifier = this.then(Modifier.drawWithContent {
val paddingPx = 8.dp.toPx()
clipRect(
left = 0f,
top = 0f,
right = size.width,
bottom = size.height + paddingPx
) {
this#drawWithContent.drawContent()
}
})

Related

How set width to drawer in jetpack compose>

I can have Navigation Drawer in JetpackCompose by using Scaffold.
But when I change the size of drawbleShape, it set fixed width as default size.
Scaffold(
drawerContent = { DrawableCompose() },
drawableShape = MaterialTheme.shapes.small
) {}
I reference this post, How to set the Scaffold Drawer Width in JetpackCompose?
But it cut child compose like,
Scaffold(
drawerContent = { DrawableCompose() },
drawableShape = customShape()
) {}
#Composable
fun customShape() = object : Shape {
override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density
): Outline {
return Outline.Rectangle(Rect(left = 0f, top = 0f, right = size.width * 2 / 3, bottom = size.height))
}
DrawerCompose
#Composable
fun DrawerCompose(
id: String
) {
val focusManager = LocalFocusManager.current
val keyboardController = LocalSoftwareKeyboardController.current
Column(modifier = Modifier.fillMaxSize()) {
OutlinedTextField(value = id, onValueChange = { onChangeEmpNo(it) }, modifier = Modifier.fillMaxWidth())
}
}
As shown in the picture above, some parts are cut off.
Is there any way to set width drawer compose width?
You should be able to solve it by adding Spacer, something like:
Scaffold(
drawerContent = {
Row {
DrawableCompose()
Spacer(Modifier.fillMaxHeight().width(48.dp))
}
},
drawerShape = customShape()
) {}
If you want to however use the right with, you might use this:
val myShape = customShape()
val widthDp = pxToDp(myShape.leftSpaceWidth!!)
Scaffold(
drawerContent = {
Row {
DrawableCompose()
Spacer(Modifier.fillMaxHeight().width(widthDp))
}
},
drawerShape = myShape
) {}
For above solution you have to add pxToDp function and adjust the customShape function:
#Composable
fun pxToDp(px: Float) = with(LocalDensity.current) { px.toDp() }
#Composable
fun customShape() = MyShape()
class MyShape : Shape {
var leftSpaceWidth: Float? = null
override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density
): Outline {
leftSpaceWidth = size.width * 1 / 3
return Outline.Rectangle(Rect(left = 0f, top = 0f, right = size.width * 2 / 3, bottom = size.height))
}
}

Disable ripple effect of BottomNavigationItem

I need a bottom navigation bar for my app. To display the items I use BottomNavigationItem:
BottomNavigation(
...
) {
...
BottomNavigationItem(
...
onClick = {onItemSelect(item)},
...
)
}
However, these come with a ripple effect that I want to disable, though I don't know how to. The BottomNavigationItem requires the attribute onClick so using the .clickable() modifier is not an option.
Edit:
This answer from Gabriele Mariotti recommends passing a MutableInteractionSource to the .clickable function, as well as null for indication. Though BottomNavigationItem accepts a MutableInteractionSource, it does not seem to accept an indication.
In this case you can provide a custom LocalRippleTheme to override the default behaviour.
Something like:
CompositionLocalProvider(LocalRippleTheme provides NoRippleTheme) {
BottomNavigation {
BottomNavigationItem(
//...
)
}
}
}
with:
private object NoRippleTheme : RippleTheme {
#Composable
override fun defaultColor() = Color.Unspecified
#Composable
override fun rippleAlpha(): RippleAlpha = RippleAlpha(0.0f,0.0f,0.0f,0.0f)
}
To bypass this, I copied the all BottomNavigationItem.kt file from material library and changed the line I wanted.
In box scope you can see indication parameter in selectable modifier. I pass this with null. However i can not change this parameter outside this class.
#Composable
fun RowScope.NoRippleEffectBottomNavigationItem(
selected: Boolean,
onClick: () -> Unit,
icon: #Composable () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
label: #Composable (() -> Unit)? = null,
alwaysShowLabel: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
selectedContentColor: Color = LocalContentColor.current,
unselectedContentColor: Color = selectedContentColor.copy(alpha = ContentAlpha.medium)
) {
val styledLabel: #Composable (() -> Unit)? = label?.let {
#Composable {
val style = MaterialTheme.typography.caption.copy(textAlign = TextAlign.Center)
ProvideTextStyle(style, content = label)
}
}
// The color of the Ripple should always the selected color, as we want to show the color
// before the item is considered selected, and hence before the new contentColor is
// provided by BottomNavigationTransition.
val ripple = rememberRipple(bounded = false, color = selectedContentColor)
Box(
modifier
.selectable(
selected = selected,
onClick = onClick,
enabled = enabled,
role = Role.Tab,
interactionSource = interactionSource,
indication = null //Changed line.
)
.weight(1f),
contentAlignment = Alignment.Center
) {
BottomNavigationTransition(
selectedContentColor,
unselectedContentColor,
selected
) { progress ->
val animationProgress = if (alwaysShowLabel) 1f else progress
BottomNavigationItemBaselineLayout(
icon = icon,
label = styledLabel,
iconPositionAnimationProgress = animationProgress
)
}
}
}
private val BottomNavigationAnimationSpec = TweenSpec<Float>(
durationMillis = 300,
easing = FastOutSlowInEasing
)
#Composable
private fun BottomNavigationTransition(
activeColor: Color,
inactiveColor: Color,
selected: Boolean,
content: #Composable (animationProgress: Float) -> Unit
) {
val animationProgress by animateFloatAsState(
targetValue = if (selected) 1f else 0f,
animationSpec = BottomNavigationAnimationSpec
)
val color = lerp(inactiveColor, activeColor, animationProgress)
CompositionLocalProvider(
LocalContentColor provides color.copy(alpha = 1f),
LocalContentAlpha provides color.alpha,
) {
content(animationProgress)
}
}
private val BottomNavigationItemHorizontalPadding = 12.dp
#Composable
private fun BottomNavigationItemBaselineLayout(
icon: #Composable () -> Unit,
label: #Composable (() -> Unit)?,
/*#FloatRange(from = 0.0, to = 1.0)*/
iconPositionAnimationProgress: Float
) {
Layout(
{
Box(Modifier.layoutId("icon")) { icon() }
if (label != null) {
Box(
Modifier
.layoutId("label")
.alpha(iconPositionAnimationProgress)
.padding(horizontal = BottomNavigationItemHorizontalPadding)
) { label() }
}
}
) { measurables, constraints ->
val iconPlaceable = measurables.first { it.layoutId == "icon" }.measure(constraints)
val labelPlaceable = label?.let {
measurables.first { it.layoutId == "label" }.measure(
// Measure with loose constraints for height as we don't want the label to take up more
// space than it needs
constraints.copy(minHeight = 0)
)
}
// If there is no label, just place the icon.
if (label == null) {
placeIcon(iconPlaceable, constraints)
} else {
placeLabelAndIcon(
labelPlaceable!!,
iconPlaceable,
constraints,
iconPositionAnimationProgress
)
}
}
}
private fun MeasureScope.placeIcon(
iconPlaceable: Placeable,
constraints: Constraints
): MeasureResult {
val height = constraints.maxHeight
val iconY = (height - iconPlaceable.height) / 2
return layout(iconPlaceable.width, height) {
iconPlaceable.placeRelative(0, iconY)
}
}
private val CombinedItemTextBaseline = 12.dp
private fun MeasureScope.placeLabelAndIcon(
labelPlaceable: Placeable,
iconPlaceable: Placeable,
constraints: Constraints,
/*#FloatRange(from = 0.0, to = 1.0)*/
iconPositionAnimationProgress: Float
): MeasureResult {
val height = constraints.maxHeight
// TODO: consider multiple lines of text here, not really supported by spec but we should
// have a better strategy than overlapping the icon and label
val baseline = labelPlaceable[LastBaseline]
val baselineOffset = CombinedItemTextBaseline.roundToPx()
// Label should be [baselineOffset] from the bottom
val labelY = height - baseline - baselineOffset
val unselectedIconY = (height - iconPlaceable.height) / 2
// Icon should be [baselineOffset] from the text baseline, which is itself
// [baselineOffset] from the bottom
val selectedIconY = height - (baselineOffset * 2) - iconPlaceable.height
val containerWidth = max(labelPlaceable.width, iconPlaceable.width)
val labelX = (containerWidth - labelPlaceable.width) / 2
val iconX = (containerWidth - iconPlaceable.width) / 2
// How far the icon needs to move between unselected and selected states
val iconDistance = unselectedIconY - selectedIconY
// When selected the icon is above the unselected position, so we will animate moving
// downwards from the selected state, so when progress is 1, the total distance is 0, and we
// are at the selected state.
val offset = (iconDistance * (1 - iconPositionAnimationProgress)).roundToInt()
return layout(containerWidth, height) {
if (iconPositionAnimationProgress != 0f) {
labelPlaceable.placeRelative(labelX, labelY + offset)
}
iconPlaceable.placeRelative(iconX, selectedIconY + offset)
}
}

Use Dialog as navigation destination with jetpack compose

A dialog can have a rather complex ui, acting more like a floating screen rather than a typical AlertDialog. Therefore it can be desired to let the dialog have its own ViewModel and being able to navigate to it. When using the jetpack compose navigation artifact the code indicates that only one composable is shown at any time inside the NavHost.
Is there a way to navigate to a dialog that is overlaid onto the current ui? This would be in line with how we can navigate to fragment dialogs. Thanks.
Aha. This is now a feature in compose navigation version 2.4.0-alpha04
From the release notes
The NavHost of the navigation-compose artifact now supports dialog
destinations in addition to composable destinations. These dialog
destinations will each be shown within a Composable Dialog, floating
above the current composable destination.
val navController = rememberNavController()
Scaffold { innerPadding ->
NavHost(navController, "home", Modifier.padding(innerPadding)) {
composable("home") {
// This content fills the area provided to the NavHost
HomeScreen()
}
dialog("detail_dialog") {
// This content will be automatically added to a Dialog() composable
// and appear above the HomeScreen or other composable destinations
DetailDialogContent()
}
}
}
Example:
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = Screen.Menu.route
) {
// some other screens here: composable(...) { ... }
dialog(
route = "exit_dialog",
dialogProperties = DialogProperties(
dismissOnBackPress = true,
dismissOnClickOutside = true,
)
) {
Box(modifier = Modifier.width(280.dp)) {
Box(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(10.dp))
.background(DialogBorder)
.padding(bottom = 3.dp)
.clip(RoundedCornerShape(10.dp))
.background(DialogBackground),
) {
Column {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(id = R.string.text_dialog_exit_title),
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
color = Color.Black
)
Text(
text = stringResource(id = R.string.text_dialog_exit_description),
fontSize = 16.sp,
fontWeight = FontWeight.Normal,
textAlign = TextAlign.Center,
color = Color.Black
)
}
Divider(color = DialogBorder)
Row(modifier = Modifier.height(IntrinsicSize.Min)) {
Box(
modifier = Modifier
.weight(1f)
.clickable {
// dismiss dialog
navController.popBackStack()
}
.padding(horizontal = 16.dp, vertical = 8.dp),
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(id = R.string.button_cancel),
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = Color.Black
)
}
Box(
modifier = Modifier
.fillMaxHeight()
.width(1.dp)
.background(DialogBorder),
)
Box(
modifier = Modifier
.weight(1f)
.clickable {
// go back to home other screen
navController.popBackStack(
route = "home_screen",
inclusive = false
)
}
.padding(horizontal = 16.dp, vertical = 8.dp),
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(id = R.string.button_ok),
fontSize = 16.sp,
fontWeight = FontWeight.Normal,
color = Color.Black
)
}
}
}
}
}
}
}
It's a feature request: https://issuetracker.google.com/issues/179608120
You can star it so perhaps we'll increase it's priority

How to add swipe behavior to the screens?

We have bottom navigation in our app and we want to add swipe behavior into our screens so that if a user swipe to right/left then s/he should be navigated into next screen.
I know that Accompanist has HorizontalPager with Tabs. But I wonder if we can achieve that behavior with bottom navigation.
As you can see in the Material Design Guidelines:
Using swipe gestures on the content area does not navigate between views.
Also:
Avoid using lateral motion to transition between views.
But, if you really want to do this, you can do the this:
fun BottomNavSwipeScreen() {
// This scope is necessary to change the tab using animation
val scope = rememberCoroutineScope()
// I'm using a list of images here
val images = listOf(R.drawable.img1, ...)
// This page state will be used by BottomAppbar and HorizontalPager
val pageState = rememberPagerState(pageCount = images.size)
val scaffoldState = rememberScaffoldState()
Scaffold(
scaffoldState = scaffoldState,
bottomBar = {
BottomAppBar(
backgroundColor = MaterialTheme.colors.primary,
content = {
for (page in images.indices) {
BottomNavigationItem(
icon = {
Icon(Icons.Filled.Home, "Page $page")
},
// here's the trick. the selected tab is based
// on HorizontalPager state.
selected = page == pageState.currentPage,
onClick = {
// When a tab is selected,
// the page is updated
scope.launch {
pageState.animateScrollToPage(page)
}
},
selectedContentColor = Color.Magenta,
unselectedContentColor = Color.LightGray,
label = { Text(text = "Page $page") }
)
}
}
)
},
) {
HorizontalPager(
state = pageState,
offscreenLimit = 2
) { page ->
Image(
painterResource(id = images[page]),
null,
modifier = Modifier
.fillMaxSize(),
contentScale = ContentScale.Crop
)
}
}
}
Here is the result:
you can achieve this by using the animation library from compose:
https://developer.android.com/jetpack/compose/animation
And using the slideIntoContainer animation you can simulate the swipe effect:
composable("route1",
enterTransition = {
slideIntoContainer(
towards = AnimatedContentScope.SlideDirection.Right,
animationSpec = tween(
durationMillis = 250,
easing = LinearEasing // interpolator
)
)
},
exitTransition = {
slideOutOfContainer(
towards = AnimatedContentScope.SlideDirection.Left,
animationSpec = tween(
durationMillis = 250,
easing = LinearEasing
)
)
}) {
Screen1()
}
composable("route2",
enterTransition = {
slideIntoContainer(
towards = AnimatedContentScope.SlideDirection.Left,
animationSpec = tween(
durationMillis = 250,
easing = LinearEasing // interpolator
)
)
},
exitTransition = {
slideOutOfContainer(
towards = AnimatedContentScope.SlideDirection.Right,
animationSpec = tween(
durationMillis = 250,
easing = LinearEasing
)
)
}) {
Screen2()
}

fillMaxHeight() inside Row

My question is related to this one: How to achieve this layout in Jetpack Compose
I have this code:
#Composable
fun TestUi() {
Row {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.background(color = Color.Yellow)
.fillMaxHeight()
) {
CircularProgressIndicator()
}
Image(imageVector = vectorResource(id = R.drawable.ic_launcher_background))
}
}
I expected to get this:
But I got this instead:
How can I get the Box to fill all the available height without affecting the height of the Row?
I know I could use a ConstraintLayout to solve this, but it seems too much for such a simple use case.
This worked for me Modifier.height(IntrinsicSize.Min)
#Composable
fun content() {
return Row(
modifier = Modifier
.height(IntrinsicSize.Min)
) {
Box(
modifier = Modifier
.width(8.dp)
.fillMaxHeight()
.background(Color.Red)
)
Column {
Text("Hello")
Text("World")
}
}
}
source: https://www.rockandnull.com/jetpack-compose-fillmaxheight-fillmaxwidth/
Have a look at the Layout Composable or Modifier. You can measure the defining element first and then provide modified constraints to the dependent element. If you want to use this as a modifier you should add a size check for the list.
Layout(content = {
Box(modifier = Modifier
.size(width = 30.dp, height = 50.dp)
.background(Color.Green))
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.background(color = Color.Yellow)
.fillMaxHeight()
) {
CircularProgressIndicator()
}
}) { measurables: List<Measurable>, constraints: Constraints ->
val heightDef = measurables[0].measure(constraints)
val other = measurables[1].measure(
constraints.copy(
maxHeight = heightDef.height,
maxWidth = constraints.maxWidth - heightDef.width)
)
layout(
width = heightDef.width + other.width,
height = heightDef.height
) {
other.placeRelative(0,0)
heightDef.placeRelative(other.width,0)
}
}
For height, instead of fillMaxHeight, just put
.height(vectorResource(id = R.drawable.ic_launcher_background).defaultHeight)
Just for reference, here's the equivalent design implemented with ConstraintLayout:
#Composable
fun TestUi() {
ConstraintLayout {
val (box, image) = createRefs()
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.background(color = Color.Yellow)
.constrainAs(box) {
height = Dimension.fillToConstraints
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
start.linkTo(parent.start)
}
) {
CircularProgressIndicator()
}
Image(
imageVector = vectorResource(id = R.drawable.ic_launcher_background),
modifier = Modifier.constrainAs(image) {
top.linkTo(parent.top)
start.linkTo(box.end)
}
)
}
}
you can use constraintlayout, the trick is in this part of the code height =Dimension.fillToConstraints
ConstraintLayout(){
val (boxRef, imgRef) = createRefs()
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.background(color = Color.Yellow)
.constrainAs(boxRef){
start.linkTo(parent.start)
end.linkTo(imgRef.start)
top.linkTo(parent.top)
height = Dimension.fillToConstraints
}
) {
CircularProgressIndicator()
}
Image(imageVector = vectorResource(id = R.drawable.ic_launcher_background),modifier=Modifier.constrainAs(imgRef){
start.linkTo(boxRef.end)
top.linkTo(parent.top)
end.linkTo(parent.end)
})
}

Resources