Is there any way to remove this padding from the BottomNavigationItem?
Image
If I have very large text, I have to use ResponsiveText to manage this, but that's not what I intended. What I need is that it doesn't have that side padding/margin, both on the left and on the right, in order to occupy as much space as possible.
My code:
#Composable
fun BottomNavBar(
backStackEntryState: State<NavBackStackEntry?>,
navController: NavController,
bottomNavItems: List<NavigationItem>
) {
BottomNavigation(
backgroundColor = DarkGray.copy(alpha = 0.6f),
elevation = Dimen0,
modifier = Modifier
.padding(Dimen10, Dimen20, Dimen10, Dimen20)
.clip(RoundedCornerShape(Dimen13, Dimen13, Dimen13, Dimen13))
) {
bottomNavItems.forEach { item ->
val isSelected = item.route == backStackEntryState.value?.destination?.route
BottomNavigationItem(
icon = {
Icon(
painter = painterResource(id = item.icon.orZero()),
contentDescription = stringResource(id = item.title)
)
},
label = {
ResponsiveText(
text = stringResource(id = item.title),
textStyle = TextStyle14,
maxLines = 1
)
},
selectedContentColor = Color.White,
unselectedContentColor = Color.White,
alwaysShowLabel = true,
selected = isSelected,
onClick = {
navController.navigate(item.route) {
navController.graph.startDestinationRoute?.let { route ->
popUpTo(route = route) {
saveState = true
}
}
launchSingleTop = true
restoreState = true
}
},
modifier = if (isSelected) {
Modifier
.clip(RoundedCornerShape(Dimen13, Dimen13, Dimen13, Dimen13))
.background(color = DarkGray)
} else {
Modifier.background(color = Color.Unspecified)
}
)
}
}
}
Apparently this is currently (I am using compose version '1.2.0-rc03') not possible when using BottomNavigation, as there is padding set for each element in these lines:
.padding(horizontal = BottomNavigationItemHorizontalPadding)
Here is what is said about this value:
/**
* Padding at the start and end of a [BottomNavigationItem]
*/
private val BottomNavigationItemHorizontalPadding = 12.dp
[Solution]
Just copy BottomNavigation from androidx and remov this line:
.padding(horizontal = BottomNavigationItemHorizontalPadding)
However, it is necessary that the first and last elements still have padding, so add the innerHorizontalPaddings parameter to the your CustomBottomNavigation constructor
There are a few more changes, you can see the full code of my CustomBottomNavigation here
Example of usage:
CustomBottomNavigation(
...,
innerHorizontalPaddings = 12.dp
) {
items.forEach { item ->
BottomNavigationItem(
icon = {
Icon(...)
},
label = {
Text(
...
softWrap = false,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(horizontal = 2.dp)
)
},
...
)
}
}
Another solution is to wrap the label in a BoxWithConstraints and draw outside of it:
BottomNavigationItem(
label = {
/**
* Because of [BottomNavigationItemHorizontalPadding] (12.dp), we need to
* think (and draw) outside the box.
*/
BoxWithConstraints {
Text(
modifier = Modifier
.wrapContentWidth(unbounded = true)
.requiredWidth(maxWidth + 24.dp), // 24.dp = the padding * 2
text = "Centered text and clipped at the end if too long",
softWrap = false,
textAlign = TextAlign.Center
)
}
},
...
)
To get a little bit of padding, you can set requiredWidth(maxWidth + 18.dp).
With this solution, you don't need to copy the enire BottomNavigation :)
Related
I'm using BottomSheetScaffold for BottomSheet.
I followed some sample and it worked.
but when I click outside the BottomSheet, the view's click event is called behind the sheet.
I want to prevent the view click when the Sheet is expanded.
Is there any way?
here is my code
val bottomSheetState = rememberBottomSheetState(
initialValue = BottomSheetValue.Collapsed,
)
val scaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = bottomSheetState,
)
val coroutineScope = rememberCoroutineScope()
val purchaseButtonHeight = 76
val closedSheetHeaderHeight = 50
val sheetHeaderRadius = 24
BottomSheetScaffold(
modifier = Modifier.pointerInput(Unit) {
detectTapGestures(
onTap = {
println("TAG : detectTapGestures ${it}")
coroutineScope.launch {
if (bottomSheetState.isCollapsed) {
bottomSheetState.expand()
} else {
bottomSheetState.collapse()
}
}
})
},
scaffoldState = scaffoldState,
sheetShape = RoundedCornerShape(
topStart = sheetHeaderRadius.dp,
topEnd = sheetHeaderRadius.dp
),
sheetPeekHeight = (purchaseButtonHeight + closedSheetHeaderHeight).dp,
sheetContent = {
Box(
modifier = Modifier
.wrapContentHeight()
.padding(horizontal = 16.dp),
contentAlignment = Alignment.BottomCenter
) {
Column {
for (i in 0 until 5) {
Text(
text = "Item ${i}",
modifier = Modifier
.height(50.dp)
.fillMaxWidth(),
textAlign = TextAlign.Center
)
}
Spacer(modifier = Modifier.height(purchaseButtonHeight.dp))
}
}
})
I'm using ScalingLazyColumn with a very long Text inside as follows:
#Preview(device = Devices.WEAR_OS_SMALL_ROUND, showSystemUi = true)
#Composable
fun Test(modifier: Modifier = Modifier) {
val scalingLazyState = remember { ScalingLazyListState() }
val focusRequester = remember { FocusRequester() }
Scaffold(
modifier = modifier,
positionIndicator = { PositionIndicator(scalingLazyListState = scalingLazyState) }
) {
ScalingLazyColumn(
modifier = Modifier.scrollableColumn(focusRequester, scalingLazyState),
state = scalingLazyState,
) {
item {
Text(
longText,
Modifier
.padding(top = 20.dp, start = 16.dp, end = 16.dp, bottom = 48.dp),
textAlign = TextAlign.Center,
)
}
}
}
}
val longText =
"Take the plunge\n" +
"\n" +
"commit oneself to a course of action about which one is nervous.\n" +
"\n" +
"\"she wondered whether to enter for the race, but decided to take the plunge\"\n" +
"\"They're finally taking the plunge and getting married.\"\n" +
"\n" +
"\n" +
"plunge:\n" +
"jump or dive quickly and energetically.\n" +
"\"our daughters whooped as they plunged into the sea\"\n"
But for some reason when I launch the app the focus goes to the bottom of the text, instead of the beginning, which looks like a bug. I've tried playing with different parameters of ScalingLazyColumn (anchorType, autoCentering, scalingParams) to no avail.
Any idea how to fix it and make the ScalingLazyColumn focus on the beginning of the first element when I launch the app?
Switching off autoCentering is an option, but I would try and avoid it in most cases as it will will make handling getting the padding right on different devices sizes more difficult and often results in being able to over scroll the list items either at the beginning or the end.
I am not sure exactly what you want to achieve when you say that you want the focus to be on the start of the first item but the following should give you what you need.
Set the state initial item to 0
Set the anchor type to ScalingLazyListAnchorType.ItemStart
Remove top padding from your item
Apply an offset to the state initialItem initialCenterItemScrollOffset to shift the start of you item up a little.
Optionally adjust the autoCentering to make sure that the limit of the scrolling matches the initial position selected in the state
#Preview(device = Devices.WEAR_OS_SMALL_ROUND, showSystemUi = true)
#Composable
fun SingleItemSLCWithLongText(modifier: Modifier = Modifier) {
val scalingLazyState = remember { ScalingLazyListState(initialCenterItemIndex = 0, initialCenterItemScrollOffset = 80) }
val focusRequester = remember { FocusRequester() }
Scaffold(
modifier = modifier.background(Color.Black),
positionIndicator = { PositionIndicator(scalingLazyListState = scalingLazyState) }
) {
ScalingLazyColumn(
modifier = Modifier.scrollableColumn(focusRequester, scalingLazyState),
autoCentering = AutoCenteringParams(itemIndex = 0, itemOffset = 80),
state = scalingLazyState,
anchorType = ScalingLazyListAnchorType.ItemStart
) {
item {
Text(
longText,
Modifier
.padding(start = 16.dp, end = 16.dp, bottom = 48.dp),
textAlign = TextAlign.Center,
)
}
}
}
}
Here is a screenshot of how the screen initially looks
Initial screen
This test activity let's you play with all the params to see starting position
https://github.com/google/horologist/blob/a1241ff25b7008f7c1337f4425b98d14ce30d96d/sample/src/main/java/com/google/android/horologist/scratch/ScratchActivity.kt
After a few hours of frustration I finally found a solution.
If you read the documentation for ScalingLazyColumn it says:
"If the developer wants custom control over position and spacing they
can switch off autoCentering and provide contentPadding."
So all you need to do is to just add autoCentering = null in ScalingLazyColumn.
This is a working code where the focus will be in the beginning of the Text:
val scalingLazyState = remember { ScalingLazyListState() }
val focusRequester = remember { FocusRequester() }
Scaffold(
modifier = modifier,
positionIndicator = { PositionIndicator(scalingLazyListState = scalingLazyState) }
) {
ScalingLazyColumn(
modifier = Modifier.scrollableColumn(focusRequester, scalingLazyState),
state = scalingLazyState,
autoCentering = null,
) {
item {
Text(
longText,
Modifier
.padding(top = 20.dp, start = 16.dp, end = 16.dp, bottom = 48.dp),
textAlign = TextAlign.Center,
)
}
}
}
ScalingLazyListState defaults to the center of the second item (index 1). You can tell it to instead start in the first item and even jn the ScalingLazyColumn parameters use the start of items.
val scalingLazyState = remember { ScalingLazyListState(initialCenterItemIndex = 0) }
val focusRequester = remember { FocusRequester() }
Scaffold(
modifier = Modifier,
positionIndicator = { PositionIndicator(scalingLazyListState = scalingLazyState) }
) {
ScalingLazyColumn(
modifier = Modifier.scrollableColumn(focusRequester, scalingLazyState),
state = scalingLazyState,
anchorType = ScalingLazyListAnchorType.ItemStart
) {
item {
Text(
longText,
Modifier
.padding(top = 20.dp, start = 16.dp, end = 16.dp, bottom = 48.dp),
textAlign = TextAlign.Center,
)
}
}
}
I have a lazy column in which there is a list of cards in which there is a row and two columns in it, the height of the first column is much larger than the second. The second column has text with a different background made using a surface. The fillMaxHeight modifiers do not work. Already tried to do it by other means:
1)
Row(
modifier = Modifier
.height(IntrinsicSize.Min)
However, there is a problem, this only works with static components, and this is a lazy line in my columns, and the composition starts to crash
2) Get the height in this way.
//Save height
val heightIs = remember {
mutableStateOf(0.dp)
}
//Get the context
val localDensity = LocalDensity.current
Column(modifier = Modifier.fillMaxWidth(0.7f).onSizeChanged{cord->
heightIs.value = with(localDensity){
cord.height.toDp()
}
The problem with this solution is that when there is no lazy row, it doesn't properly distribute the height.
Short code:
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 2.dp, vertical = 4.dp), elevation = 4.dp
) {
Row(modifier = Modifier.height(IntrinsicSize.Min)) {
Column(Modifier.fillMaxWidth(0.7f)) {
Surface(
color = MaterialTheme.colors.primaryVariant,
modifier = Modifier.fillMaxWidth(),
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = historyOrderItem.id.toString(),
style = MaterialTheme.typography.h6,
modifier = Modifier.padding(4.dp)
)
Text(
text = historyOrderItem.status,
style = MaterialTheme.typography.h6,
modifier = Modifier.padding(4.dp)
)
}
}
LazyRow(modifier = modifier.fillMaxWidth()) {
items(items = historyOrderItem.items, key = { it.itemId }) {
ItemDoneOrderShort(it)
}
}
Spacer(modifier = Modifier.weight(1f))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = { onCopy(historyOrderItem) }) {
Text(
text = "ПОВТОРИТЬ",
style = MaterialTheme.typography.h6,
textAlign = TextAlign.Right,
maxLines = 1,
)
}
}
}
Surface(color = MaterialTheme.colors.colorAccent) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = "Оплатить",
textAlign = TextAlign.Center,
style = MaterialTheme.typography.h5,
)
}
}
}
}
I'm trying to use ConstraintLayout in Compose. I want the second Composable to be constrained at the end of the first Composable. The first composable can exist or not. In each case, I would like to have different margins. Thus, I use goneMargin but this seems to not be respected. Do I need to do something else?
ConstraintLayout {
val (firstItemRef, secondItemRef) = createRefs()
if (isFirstItemVisible) {
Text(
text = "First",
modifier = Modifier.constrainAs(firstItemRef) {
top.linkTo(anchor = parent.top)
start.linkTo(anchor = parent.start)
}
)
}
Text(
text = "Second",
modifier = Modifier.constrainAs(secondItemRef) {
top.linkTo(anchor = parent.top)
start.linkTo(anchor = firstItemRef.end, margin = 8.dp, goneMargin = 16.dp)
}
)
}
As a workaround, we could do something like that, but this seems to counterfeit the purpose of goneMargin
Text(
text = "Second",
modifier = Modifier.constrainAs(secondItemRef) {
val margin = if (isFirstItemVisible) 8.dp else 16.dp
val end = if (isFirstItemVisible) firstItemRef.end else parent.end
top.linkTo(anchor = parent.top)
start.linkTo(anchor = end, margin = margin)
}
)
You have to use the visibility property in ConstrainScope like this:
ConstraintLayout(Modifier.fillMaxSize()) {
val (firstItemRef, secondItemRef) = createRefs()
Text(
text = "First",
modifier = Modifier.constrainAs(firstItemRef) {
top.linkTo(anchor = parent.top)
start.linkTo(anchor = parent.start)
// >> This is what you want <<<
visibility = if (isFirstItemVisible) Visibility.Visible else Visibility.Gone
}
)
Text(
text = "Second",
modifier = Modifier.constrainAs(secondItemRef) {
top.linkTo(anchor = parent.top)
start.linkTo(anchor = firstItemRef.end, margin = 8.dp, goneMargin = 16.dp)
}
)
}
It is usually possible to assign different shapes to a composable using a modifier, but this is not done in this composable.
I want the part marked in the image to be a circle
You can see the code I wrote below
#Composable
fun StandardCheckbox(
text: String = "",
checked: Boolean,
onCheckedChange: ((Boolean) -> Unit)?,
) {
Row(
Modifier.padding(horizontal = SpaceMedium)
) {
Checkbox(
modifier = Modifier
.clip(CircleShape),
checked = checked,
onCheckedChange = onCheckedChange,
enabled = true,
colors = CheckboxDefaults.colors(
checkedColor = MaterialTheme.colors.primary,
checkmarkColor = MaterialTheme.colors.onPrimary,
uncheckedColor = MaterialTheme.colors.onBackground.copy(alpha = 0.3f)
)
)
Spacer(modifier = Modifier.width(SpaceSmall))
Text(
text = text,
color = MaterialTheme.colors.primary,
modifier = Modifier.clickable {
if (onCheckedChange != null) {
onCheckedChange(!checked)
}
}
)
}
}
In order to achieve a circular checkbox with a native experience, and retain the body color and click ripple effect, and keep it simple, IconButton is the best choice.
#Composable
fun CircleCheckbox(selected: Boolean, enabled: Boolean = true, onChecked: () -> Unit) {
val color = MaterialTheme.colorScheme
val imageVector = if (selected) Icons.Filled.CheckCircle else Icons.Outlined.Circle
val tint = if (selected) color.primary.copy(alpha = 0.8f) else color.white.copy(alpha = 0.8f)
val background = if (selected) color.white else Color.Transparent
IconButton(onClick = { onChecked() },
modifier = Modifier.offset(x = 4.dp, y = 4.dp),
enabled = enabled) {
Icon(imageVector = imageVector, tint = tint,
modifier = Modifier.background(background, shape = CircleShape),
contentDescription = "checkbox")
}
}
The code below is from CheckboxImpl composable
Canvas(modifier.wrapContentSize(Alignment.Center).requiredSize(CheckboxSize)) {
val strokeWidthPx = floor(StrokeWidth.toPx())
drawBox(
boxColor = boxColor,
borderColor = borderColor,
radius = RadiusSize.toPx(),
strokeWidth = strokeWidthPx
)
drawCheck(
checkColor = checkColor,
checkFraction = checkDrawFraction,
crossCenterGravitation = checkCenterGravitationShiftFraction,
strokeWidthPx = strokeWidthPx,
drawingCache = checkCache
)
}
drawBox will always draw a rounded rectangle. It can't be be customised.
To implement the circular checkbox you need to write a custom Composable and draw Circle instead of Rectangle. You can use RadioButton and CheckboxImpl composable as reference.