The library I'm using: "com.google.accompanist:accompanist-placeholder-material:0.23.1"
I want to display a placeholder in the place of (or over) a component when it's in the loading state.
I do the following for a Text:
MaterialTheme() {
var placeholderVisible by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
while (true) {
delay(1000)
placeholderVisible = !placeholderVisible
}
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.border(1.dp, Color.Red)
.padding(16.dp)
) {
Text(
modifier = Modifier
.then(
if (placeholderVisible) {
Modifier.height(28.dp).width(62.dp)
} else {
Modifier
}
)
.placeholder(
visible = placeholderVisible,
highlight = PlaceholderHighlight.shimmer()
),
text = if (placeholderVisible) "" else "Hello"
)
}
}
}
And I get this:
I want instead that no matter how big I set the placeholder's height or width, it will not participate in any way in the measuring process and, if I want to, to be able to draw itself even over other components (in this case let's say the red border).
As an effect of what I want, the box with red border will always have the dimension as if that Modifier.height(28.dp).width(62.dp) is not there.
I know I can draw outside a component's borders using drawWithContent, specifying the size of a rectangle or a circle (or whatever) to be component's size + x.dp.toPx() (or something like that). But how do I do this with Modifier.placeholder?
Ideally, I would need something like Modifier.placeholder(height = 28.dp, width = 62.dp)
So, with or without this ideal Modifier, the UI should never change (except, of course, the shimmer box that may be present or not).
I think I can pull this off by modifying the source code of this Modifier, but I hope I won't need to turn to that.
Just replace your Text() with below code, maybe conditional Modifier is the issue in above code!
Text(
modifier = Modifier
.size(width = 62.dp, height = 28.dp)
.placeholder(
visible = placeholderVisible,
highlight = PlaceholderHighlight.shimmer()
),
text = if (placeholderVisible) "" else "Hello",
textAlign = TextAlign.Center
)
Related
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'm creating a component in Jetpack Compose and realized that when I'm making a Composable inside a Box it's possible that this component assumes 2 maximum fill possibilities: Modifier.fillMaxSize() and Modifier.matchParentSize(). As shown below:
Box(
modifier = modifier // This modifier is received by parameter of another composable function
) {
Canvas(modifier = Modifier.matchParentSize()) {
// Using match parent size
}
}
and
Box(
modifier = modifier // This modifier is received by parameter of another composable function
) {
Canvas(modifier = Modifier.fillMaxSize()) {
// Using fill max size
}
}
What is the practical difference between these 2 modes? And why can't I set Modifier.matchParentSize() to a Column or a Row?
From official doc
Modifier.fillMaxSize modifier, which makes an element occupy all available space, will take part in defining the size of the Box.
So it specifies the size of the element.
But if you use Modifier.matchParentSize() in an element inside of a box it has nothing to do with specifying the size of the box.
The size of the box will be measured by other children element of the box. Then the element with the Modifier.matchParentSize() will match and occupy that size.
You can't use .matchParentSize() in Row or Column because this modifier is part of the BoxScope interface. So it works only with boxscope.
For example, if you use fillMaxSize with something like the below.
Box() {
Text(
modifier = Modifier
.fillMaxSize()
.background(Color.Green),
text = ""
)
Text(
modifier = Modifier
.size(100.dp)
.background(Color.Blue),
text = "Hello",
)
}
You will get this. It will fill the entire screen because of that .fillMaxSize() modifier in the first child.
But if you use this
Box() {
Text(
modifier = Modifier
.matchParentSize()
.background(Green),
text = ""
)
Text(
modifier = Modifier
.size(100.dp),
text = "Hello",
)
}
It will take only 100.dp for the Hello text and then the green background will fill that 100.dp because of that .matchParentSize() modifier in the first child.
I could have used Box instead of Text but more Box can make it confused.
I am trying to center the content inside a Text of Jetpack Compose, but I'm not succeeding. The code seems fine to me (I'm very new to Compose though). The code:
Row(
modifier = Modifier
.padding(8.dp)
.height(30.dp)
.fillMaxHeight(),
) {
IconButton(..) {..}
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "Some text",
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxHeight()
.border(1.dp, Color.Black, RoundedCornerShape(20))
.width(120.dp)
.background(color = Color.White, RoundedCornerShape(20))
.align(Alignment.CenterVertically)
)
Spacer(modifier = Modifier.width(4.dp))
IconButton(..) {..}
}
This yields the following result:
Your Text takes full height according to your modifier, in this case .align(Alignment.Center) won't help you.
You can place your Text inside a Box and center it there:
Row(
modifier = Modifier
.padding(8.dp)
.height(30.dp)
.fillMaxHeight()
) {
IconButton({ }) { Text("hello ") }
Spacer(modifier = Modifier.width(4.dp))
Box(
Modifier
.fillMaxHeight()
.border(1.dp, Color.Black, RoundedCornerShape(20))
.width(120.dp)
.background(color = Color.White, RoundedCornerShape(20))
) {
Text(
text = "Some text",
textAlign = TextAlign.Center,
modifier = Modifier
.align(Alignment.Center)
)
}
Spacer(modifier = Modifier.width(4.dp))
IconButton({ }) { Text("hello ") }
}
You can solve this with adding a padding to your text, but in this case you can't specify Row height explicitly, and need to let it wrap content size:
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(8.dp)
) {
IconButton({ }) { Text("hello ") }
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "Some text",
textAlign = TextAlign.Center,
modifier = Modifier
.border(1.dp, Color.Black, RoundedCornerShape(20))
.width(120.dp)
.background(color = Color.White, RoundedCornerShape(20))
.padding(vertical = 10.dp)
)
Spacer(modifier = Modifier.width(4.dp))
IconButton({ }) { Text("hello ") }
}
Putting it on a column and centering both ways can also work:
Column(
modifier = Modifier
.fillMaxHeight()
.border(1.dp, Color.Black, RoundedCornerShape(20))
.width(120.dp)
.background(color = Color.White, RoundedCornerShape(20)),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "Some text")
}
First of all, Compose is different than Android Views in many ways. There is no 'C' in 'View', but 'Composable' STARTS with a 'C'. Ok, so now that the basics are clear, you see the behaviour that you are observing is indeed the expected one. Here's why
Composables, just like views, have bounds (the four corner points separating it from the rest of the environment), which we can modify using Modifiers. Composables including text to be rendered, such as the Text and all the TextFields follow the same basic principle. If I have made my text very large (say a quarter to the maxHeight), then I must have done so because I assume my text value to be placed in is large, or else there is no point of doing that, right? Hence, when you create a Text with dimensions larger than required by the value, it should be treated as a Composable 'ready' to hold the maximum capacity it is composed to.
Ok so if you were to change your text value in such a way so as to center the text, what would you do? You would add Line-Breaks (Press Enter Multiple Times) until the text looks vertically in the middle, but that's just lame. You did not need to specify bounds so large if your value was smaller than would fill the Comp. Let's say a person can modify this value. Every time the value changes, the space from the edges needs to be calculated based on the length of the string + font size + font family + font style. If there is a faster approach, why would a person not take that? You are provided with a golden Modifier called as wrapContentHeight. Now, this will wrap the Composable itself around the text (asynchronously), limiting the amount of space to the required. Ok now that the bounds are set, you can position the Composable literally anywhere you wish to on the screen by specifying the co-ordinates. For the desired behaviour, there's the handy verticalAlignemnt parameter provided with some Composables, or something to that effect, which will automatically and asynchronously do the job for you.
You may as well ask why the horizontal alignment is provided despite having the same effect, I think it is because of the convention that people had been used to since the early typewriting days. The vertical one might be inefficient from a memory and/or performance perspective.
However, if for some bizarre reason you are sworn not to use external Composables for content Alignment, then I guess it is not such a big deal to implement your own version of the aligner. It is just that vertical space is divided into 'lines' of fixed width, and at any given time, a text value can be displayed only on a specific line. Hence, if the height of the Composable is not perfectly an integral multiple of the line-height, it would be tricky to place the block (actually it would be impossible using the pre-built stuff). In your case for example, the Composable does not seem out two line-heights. Now to center, you would have to place it at 1.5 the line height which is something Text cannot achieve, so the line break implementation would fail here. Also the alignment will not be perfect if the height is an even multiple of the line-height.
It's just messed up.
How to create BottomNavigation with one of the item is larger than the parent, but without using floatingActionButton. For example like this:
I tried to do that by wrapping the icon with Box but it get cut like this:
Then i try to separate that one button and use constraintLayout to position it, but the constraintLayout cover the screen like this. Even when i color it using Color.Transparent, it always feels like Color.White (i dont know why Color.Transparent never work for me). In this picture i give it Red color for clarity reason.
So how to do this kind of bottomNavBar without having to create heavy-custom-composable?
Update: so i try to make the code based on MARSK and Dharman comment (thanks btw). This is what i
BoxWithConstraints(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.background(Color.Transparent)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(56.dp)
.background(Color.White)
.align(Alignment.BottomCenter)
)
Row(
modifier = Modifier
.zIndex(56.dp.value)
.fillMaxWidth()
.selectableGroup(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
items.forEach { item ->
val selected = item == currentSection
BottomNavigationItem(
modifier = Modifier
.align(Alignment.Bottom)
.then(
Modifier.height(
if (item == HomeSection.SCAN) 84.dp else 56.dp
)
),
selected = selected,
icon = {
if (item == HomeSection.SCAN) {
ScanButton(navController = navController, visible = true)
} else {
ImageBottomBar(
icon = if (selected) item.iconOnSelected else item.icon,
description = stringResource(id = item.title)
)
}
},
label = {
Text(
text = stringResource(item.title),
color = if (selected) Color(0xFF361DC0) else LocalContentColor.current.copy(
alpha = LocalContentAlpha.current
),
style = TextStyle(
fontFamily = RavierFont,
fontWeight = if (selected) FontWeight.Bold else FontWeight.Normal,
fontSize = 12.sp,
lineHeight = 18.sp,
),
maxLines = 1,
)
},
onClick = {
if (item.route != currentRoute && item != HomeSection.SCAN) {
navController.navigate(item.route) {
launchSingleTop = true
restoreState = true
popUpTo(findStartDestination(navController.graph).id) {
saveState = true
}
}
}
}
)
}
}
}
It works in preview, but doesn't work when i try in app.
This one in the preview, the transparent working as expected:
And this is when i try to launch it, the transparent doesnt work:
Note: I assign that to bottomBar of Scaffold so i could access the navigation component. Is it the cause that Transparent Color doesnt work?
Update 2: so the inner paddingValues that makes the transparent doesnt work. I fixed it by set the padding bottom manually:
PaddingValues(
start = paddingValues.calculateStartPadding(
layoutDirection = LayoutDirection.Ltr
),
end = paddingValues.calculateEndPadding(
layoutDirection = LayoutDirection.Ltr
),
top = paddingValues.calculateTopPadding(),
bottom = SPACE_X7,
)
Custom Composable are not heavy, really.
Anyway, try this:-
Create a Container of MaxWidth (maybe a BoxWithConstraints or something), keep its background transparent, set the height to wrap content. Create the tabs as usual, but keeping the bigger tab's icon size bigger explicitly using Modifier.size(Bigger Size).
After you have this setup, add another container inside this container with white background, covering a specific height of the original container. Let's say 60%
Now set the z-index of all the icons and tabs to higher than the z-index of this lastly added container. Use Modifier.zIndex for this. And viola, you have your Composable ready.
In order to set a specific percentage height of the inner container, you will need access to the height of the original container. Use BoxWithConstraints for that, or just implement a simple custom Layout Composable
I want to explicitely retrieve the value of the fillMaxSize().
Suppose i have:
Box(Modifier
.fillMaxSize()
.background(Color.Yellow))
{
var size = ?
Box(Modifier
.size(someSize)
.background(Color.Blue))
{Text("Test")}
I want to change the size of my second Box multiple times (will probably stem from some viewmodel) and then reset it to maxSize.
How can i do that, I don't know any 'getMaxSize()'-method?
If you really need the raw size value, you can use the following code:
var size by remember { mutableStateOf(IntSize.Zero) }
Box(Modifier
.fillMaxSize()
.background(Color.Yellow)
.onSizeChanged {
size = it
}
) {
Box(
Modifier
.then(
with(LocalDensity.current) {
Modifier.size(
width = size.width.toDp(),
height = size.height.toDp(),
)
}
)
.background(Color.Blue)
) { Text("Test") }
}
But note, that is't not optimal in terms of performance: this view gets rendered two times. First time second box gets size zero, then the onSizeChanged block gets called, and then view gets rendered for the second time.
Be especially careful if using remember in top level views, because changing state will trigger full view stack re-render. Usually you want split your screen into views with states, so changing one view state only will re-render this view.
Also you can use BoxWithConstraints where you can get maxWidth/maxHeight inside the BoxWithConstraintsScope: it's much less code and a little better on performance.
BoxWithConstraints(
Modifier
.fillMaxSize()
.background(Color.Yellow)
) {
Box(
Modifier
.size(
width = maxWidth,
height = maxHeight,
)
.background(Color.Blue)
) { Text("Test") }
}
But usually if you wanna indicate size dependencies, using modifiers without direct knowing the size should be enough. It's more "Compose" way of writing code and more optimized one.
So if you wanna you second box be same size as the first one, just use .fillMaxSize() on it too. If you wanna it to be some part of the parent, you can add fraction param. To make second box size be half size of the first one, you do:
Box(
Modifier
.fillMaxSize(fraction = 0.5f)
) { Text("Test") }
If you wanna different parts for width/height:
Box(
Modifier
.fillMaxWidth(fraction = 0.3f)
.fillMaxHeight(fraction = 0.7f)
) { Text("Test") }
In your first Box you can use the onGloballyPositioned modifier to get the size.
It is called with the final LayoutCoordinates of the Layout when the global position of the content may have changed.
Then use coordinates.size to get the size of the first Box.
var size by remember { mutableStateOf(Size.Zero)}
Box(
Modifier
.fillMaxSize()
.background(Color.Yellow)
.onGloballyPositioned { coordinates ->
size = coordinates.size.toSize()
}
Box(){ /*....*/ }
)
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
var parentSize by remember {
mutableStateOf(Size.Zero)
}
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
.background(Color.Green)
) {
Box(
modifier = Modifier
.size(100.dp)
.align(Alignment.Center)
.background(Color.Red)
.onGloballyPositioned {
//here u can access the parent layout coordinate size
parentSize = it.parentLayoutCoordinates?.size?.toSize()?: Size.Zero
}
) {
Column(Modifier.fillMaxSize()) {
Text(text = "parent size = $parentSize")
}
}
}
}