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))
}
}
Related
I have an animateDpAsState(..), whenever this animation is triggered it changes the Modifier.size(value) of an Image(...) thus causing recomposition.
Is there a way to skip composition phase for this specific scenario? Allowing an image to change its size?
You can do it using Modifier.drawWithContent, Modifier.drawBeheind or using Canvas which is a Spacer with Modifier.drawBehind. Modifiers with lambda trigger Layout, Layout->Draw or Draw phases skipping Composition as in this answer.
The snippet below changes size with animation and if you want size changes to be applied from center you can add translate either
#Composable
private fun ImageSizeAnimationSample() {
val painter = painterResource(id = R.drawable.landscape1)
var enabled by remember { mutableStateOf(true) }
val sizeDp by animateDpAsState(if (enabled) 200.dp else 0.dp)
val density = LocalDensity.current
val context = LocalContext.current
SideEffect {
println("🔥 Composing...")
Toast.makeText(context, "Composing...", Toast.LENGTH_SHORT).show()
}
Canvas(modifier = Modifier.size(200.dp)) {
val dimension = density.run { sizeDp.toPx() }
with(painter) {
draw(size = Size(dimension, dimension))
}
}
Button(onClick = { enabled = !enabled }) {
Text("Enabled: $enabled")
}
}
With translation
Canvas(modifier = Modifier.size(200.dp)) {
val dimension = density.run { sizeDp.toPx() }
with(painter) {
translate(left = (size.width - dimension) / 2, top = (size.height - dimension) / 2) {
draw(size = Size(dimension, dimension))
}
}
}
In these examples only one recomposition is triggered for animation because
val sizeDp by animateDpAsState(if (enabled) 200.dp else 0.dp)
reads enabled value but you can handle animations with Animatable which won't trigger any recomposition either.
#Composable
private fun ImageSizeAnimationWithAnimatableSample() {
val painter = painterResource(id = R.drawable.landscape1)
val animatable = remember { Animatable(0f) }
val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current
SideEffect {
println("🔥 Composing...")
Toast.makeText(context, "Composing...", Toast.LENGTH_SHORT).show()
}
Canvas(modifier = Modifier.size(200.dp)) {
with(painter) {
val dimension = size.width * animatable.value
translate(left = (size.width - dimension) / 2, top = (size.height - dimension) / 2) {
draw(size = Size(dimension, dimension))
}
}
}
Button(onClick = {
coroutineScope.launch {
val value = animatable.value
if(value == 1f){
animatable.animateTo(0f)
}else {
animatable.animateTo(1f)
}
}
}) {
Text("Animate")
}
}
I found the solution! To skip recomposition but still affect things around the layout, you can do it do it in layout phase, otherwise move it to Draw phase!
Apply this Modifier to the Image(...) modifier parameter.
Modifier.layout { measurable, constraints ->
val size = animationSize.toPx() // getting the size of the current animation
val placeable = measurable.measure(Constraints.fixed(size.toInt(), size.toInt())) // setting the actual constraints of the image
// Set the layout with the same width and height of the image.
// Inside the layout we will place the image. This layout function is like a "box"
layout(placeable.width,placeable.height) {
// And then we will place the image inside the "box"
placeable.placeRelative(0, 0)
}
}
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()
}
})
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)
}
}
The following is my code snippet. I pass in editClick that adds a data class object into chargingViewModel.contractSelfPay, which is observed as itemList state. When I click the icon, I can tell itemList state receives update by having more edit icons that are spaced evenly. However, BasicGrid Row's height is not stretched with Intrinsic.Min.
If I remove IntrinsicSize.Min, even though row's height is stretched, dividers no longer can fillMaxHeight as well as icon columns. without Intrinsic.Min
#Composable
fun ContractSelfPay(chargingViewModel: ChargingViewModel, editClick: () -> Unit = {}) {
val itemList by chargingViewModel.contractSelfPay.observeAsState()
val composeList: List<#Composable () -> Unit> = itemList?.map {
#Composable {
Row {
TempFunc { StyledText(text = it.itemTitle) }
TempFunc { StyledText(text = it.originalPrice.toString()) }
TempFunc { StyledText(text = it.selfPay.toString(), color = self_pay_blue) }
TempFunc { StyledText(text = it.count.toString()) }
TempFunc { StyledText(text = (it.selfPay * it.count).toString()) }
}
}
} ?: listOf()
val total = itemList?.map { (it.selfPay.toInt() * it.count.toInt()) }?.sum() ?: 0
BasicGrid("全自費", composeList, total = total.toString(), editClick = editClick)
}
#Composable
fun BasicGrid(
gridTitle: String,
itemList: List<#Composable () -> Unit>,
total: String = "0",
editClick: () -> Unit = {}
) {
Row(modifier = Modifier.height(IntrinsicSize.Min), verticalAlignment = Alignment.CenterVertically) {
StyledTextBold(text = gridTitle, modifier = Modifier.weight(15f).wrapContentWidth())
VerticalDivider()
Column(
modifier = Modifier.weight(60f)
) {
itemList.forEachIndexed { index, compose ->
compose()
if (index != itemList.size - 1)
HorizontalDivider()
}
if (itemList.isEmpty())
StyledText(text = "尚未有任何紀錄", modifier = Modifier.weight(1f).wrapContentSize())
}
VerticalDivider()
StyledTextBold(text = total, modifier = Modifier.weight(15f).wrapContentWidth())
VerticalDivider()
Column(
modifier = Modifier
.weight(10f)
.fillMaxHeight(),
verticalArrangement = Arrangement.SpaceEvenly
) {
itemList.forEachIndexed { index, detail ->
Image(
painter = painterResource(R.drawable.icon_mode_edit),
contentDescription = "",
modifier = Modifier
.align(Alignment.CenterHorizontally)
.clickable { editClick() },
)
if (itemList.isNotEmpty() && index != itemList.size - 1)
HorizontalDivider()
}
}
}
}
I have created issue here https://issuetracker.google.com/issues/217910352. Hopefully it gets solved.
One of the work-arounds I could think of is keeping track of height and removing IntrinsicSize.Min.
As in:
// _key_ is something that causes change of the height of the row
var height by remember(_key_) { mutableStateOf(0) }
Row(Modifier.onSizeChanged { height = it.height }) {
VerticalDivider(Modifier.height(height))
}
In your case I suppose key would be size of itemList.
Thank you Majkeee. It's been a while. The way I fixed it at the time was with custom layout modifier. Not sure if it still works today though.
fun Modifier.expandHeight() = this.then(layout { measurable, constraints ->
val placeable =
measurable.measure(constraints.copy(maxHeight = Constraints.Infinity))
layout(placeable.width, placeable.height) {
placeable.placeRelative(0, 0)
}
})
and to use it you can do
Column(modifier = Modifier.expandHeight())
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()
}