Overview
Hello folks,
To start any front-end technology the layout is an essential topic. If you are new to jetpack compose then it’s good to start with layouts.The main purpose of this article is to provide information about layouts in Jetpack Compose.
Modifier
Before we start about layouts, you should know about “Modifiers” in jetpack compose.Modifiers can be used to modify certain aspects of a Composable. To set them, a Composable needs to accept a modifier as a parameter. Our output depends on the order of modifiers.
@Composable fun ModifierExample() { Text("Hello World", Modifier .background(color = Color.Cyan) .border(4.dp, Color.Blue) .padding(15.dp) .border(2.dp, Color.Green) .padding(30.dp) .border(2.dp, Color.Red) .padding(80.dp) .border(2.dp, Color.Black) .padding(4.dp) ) }
Output:
From the above example, it first applies Cyan background then it applies border, and then it applies padding so we can say that the order of the modifier is “Left to Right”.
Column Layout
@Composable inline fun Column( modifier: Modifier = Modifier, verticalArrangement: Arrangement.Vertical = Arrangement.Top, horizontalAlignment: Alignment.Horizontal = Alignment.Start, content: @Composable ColumnScope.() -> Unit )
If you are familiar with XML in android, Column layout is the same as Linear Layout with vertical orientation.
Column is an inline composable function that accepts parameters like modifier, verticalArrangement, horizontalArrangement and content
verticalArrangement: used for arranging children vertically
horizontalArrangement: used for aligning children horizontally
@Composable fun RowColumnDemo(modifier: Modifier) { Column( modifier = Modifier.padding(all = 8.dp), horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.Center ) { Text(text = "Yudiz", style = MaterialTheme.typography.h6) Text("Mobile App Development", style = TextStyle(color = Color.Gray)) } }
Output:
Row Layout
@Composable inline fun Row( modifier: Modifier = Modifier, horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, verticalAlignment: Alignment.Vertical = Alignment.Top, content: @Composable RowScope.() -> Unit )
If you are familiar with XML in android, Row layout is the same as Linear Layout with horizontal orientation
Row is an inline composable function that accepts parameters like modifier, horizontalArrangement, verticalAlignment and content
horizontalArrangement: used for arranging children horizontally
verticalAlignment: used to aligning children vertically
Example: Now, I want to add a circular image on the left side of the column which I made before.
@Composable fun RowColumnDemo(modifier: Modifier) { Row(modifier = Modifier.padding(all = 8.dp)) { Image( modifier = Modifier .size(60.dp) .clip(CircleShape), painter = painterResource(R.drawable.ic_launcher_background), contentDescription = "Contact profile picture" ) //column layout Column( modifier = Modifier.padding(all = 8.dp), horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.Center ) { Text(text = "Yudiz",style = MaterialTheme.typography.h6) Text("Mobile App Development",style = TextStyle(color = Color.Gray)) } } }
Output:
Box Layout
@Composable inline fun Box( modifier: Modifier = Modifier, contentAlignment: Alignment = Alignment.TopStart, propagateMinConstraints: Boolean = false, content: @Composable BoxScope.() -> Unit )
When you have to work with the stack or you want to put a view on top of another then you should use Box Layout. Box Layout is similar to the frame layout in android’s XML, Box is also an inline composable function that accepts parameters like modifier, contentAlignment and content
Modifier: used for modifying the layout
contentAlignment: used for aligning the view
Let’s say I want to achieve output like the below image
@Composable fun ImageCard( painter: Painter, modifier: Modifier = Modifier ) { Box(modifier = modifier) { Image( modifier = Modifier .fillMaxSize() .clip(CircleShape), painter = painter, contentDescription = null, contentScale = ContentScale.Crop, ) Card( modifier = Modifier .clip(CircleShape) .background(Color.Gray) .align(Alignment.BottomEnd), elevation = 8.dp ) { Icon( painter = painterResource(id = R.drawable.ic_baseline_photo_camera_24), modifier = Modifier .padding(4.dp), contentDescription = null ) } } }
Then add ImageCard function in your preview’s composable function
@Preview(showBackground = true) @Composable fun DefaultPreview2() { MyApplicationTheme { ImageCard( painter = painterResource(id = R.drawable.android), modifier = Modifier .size(100.dp) .padding(4.dp) ) } }
Constraint Layout
Constraint Layout is useful when implementing larger layouts with more complicated alignment requirements. ConstraintLayout is only recommended when you have a complex and large layout because compose can efficiently handle deep layouts hierarchies.
Before starting an example of constraint layout, you should know how to create references and how to link elements with each other.
- References are created using createRefs() or createRefFor().
- Constraints are provided using a constraintAs() modifier that takes references as a parameter and then you can specify constraints in the body.
- linkTo() is used for linking your view
- You can define your constraints directly in View’s modifier using constraintAs() or you can first create your constraintSet and define constraints in the constraintSet block and use layout id in the modifier.
Example:
@Composable fun ConstraintDemo() { val constraints = ConstraintSet { val image = createRefFor("image") val circle_iv = createRefFor("circle_iv") val outLineButton = createRefFor("outline_btn") val btn = createRefFor("btn") constrain(image) { top.linkTo(parent.top) start.linkTo(parent.start) end.linkTo(parent.end) width = Dimension.fillToConstraints height = Dimension.value(250.dp) } constrain(circle_iv) { start.linkTo(parent.start) end.linkTo(parent.end) top.linkTo(image.bottom) bottom.linkTo(image.bottom) width = Dimension.value(100.dp) height = Dimension.value(100.dp) } constrain(outLineButton) { end.linkTo(parent.end) top.linkTo(image.bottom) } constrain(btn) { start.linkTo(parent.start) top.linkTo(image.bottom) } } ConstraintLayout(constraints, modifier = Modifier.fillMaxSize()) { Image( painter = painterResource(id = R.drawable.background), contentDescription = null, modifier = Modifier.layoutId("image"), contentScale = ContentScale.Crop ) ImageCard( painter = painterResource(id = R.drawable.dog), modifier = Modifier .size(60.dp) .layoutId("circle_iv"), ) OutlineBtn( text = "Hello!!", modifier = Modifier .layoutId("outline_btn") .offset(x = (-10).dp, y = 10.dp) ) Btn( text = "Hii!!", modifier = Modifier .layoutId("btn") .offset(x = (10).dp, y = 10.dp) ) } }
Output:
Custom Layout
When we have certain requirements oftentimes default layouts can make us crazy & ends up increasing complexity, for such situations they need a specialised handcrafted solution like custom layout & that’s when this tool comes really handy, We can make a custom layout using the Layout Composable function. This function takes parameters like content, modifier, and measurePolicy.
@Composable inline fun Layout( content: @Composable () -> Unit, modifier: Modifier = Modifier, measurePolicy: MeasurePolicy )
Content: we need to pass child view
measurePolicy: the policy defining the measurement and positioning of the layout.
In custom layout, first, we have to measure our children. You can measure children only once.
Example: Let’s say I want to achieve output like the below image
First, we make a composable function, and in this function, we have to invoke the Layout composable function and pass parameters : content and modifier. I passed a list of images in the content. And I passed lambda parameters : measurables, constraints that are a part of measurablePolicy.
@Composable fun StaggeredGrid(numberOfColumns:Int,images: List<Int>, modifier: Modifier = Modifier) { Layout(content = { images.subList(0, images.size).forEach { Image( painter = painterResource(id = it), modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(4.dp)) .padding(4.dp), contentScale = ContentScale.Crop, contentDescription = "pictures" ) } }, modifier = modifier) { measurables, constraints -> ... }
measurables are a list of Measurable that is used to measure the layout with constraints and that returns a Placeable
Placeable is an abstract class that is used to store our child’s width and height etc. Now let’s understand how a staggered grid works. From the above image you can see the pattern, this method compares the height of columns & whichever column’s height is smaller images get placed there, and so on.
Now how can we find shortest column.
private fun shortestColumn(columnHeights: IntArray):Int { var minHeight = Int.MAX_VALUE var column = 0 columnHeights.forEachIndexed { index, height -> if (height < minHeight){ minHeight = height column = index } } return column }
Continue in StaggerGrid function
Layout(content = { images.subList(0, images.size).forEach { Image( painter = painterResource(id = it), modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(4.dp)) .padding(4.dp), contentScale = ContentScale.Crop, contentDescription = "Contact profile picture" ) } }, modifier = modifier) { measurables, constraints -> //use for find width of column val columnWidth = (constraints.maxWidth) / numberOfColumns //this will use for track my each column's total height val columnHeight = IntArray(numberOfColumns){0} val imageConstraints = constraints.copy( maxWidth = columnWidth // List of measured children val placeables = measurables.map { // column is index of column val column = shortestColumn(columnHeights = columnHeight) val placable = it.measure(imageConstraints) // add height for measure total height columnHeight[column] += placable.height placable }
Now we have a list of measured children. Using them we can place our views in the layout but before this, we need to calculate the width and height of the layout.
@Composable fun StaggeredGrid(numberOfColumns: Int, images: List<Int>, modifier: Modifier = Modifier) { Layout(content = { images.subList(0, images.size).forEach { Image( painter = painterResource(id = it), modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(4.dp)) .padding(4.dp), contentScale = ContentScale.Crop, contentDescription = "Contact profile picture" ) } }, modifier = modifier) { measurables, constraints -> ... val height = columnHeight.maxOrNull() ?.coerceIn(constraints.minHeight, constraints.maxHeight) ?: constraints.minHeight layout(constraints.maxWidth, height) { // Keep track of the current Y position for each column val columnYPointers = IntArray(numberOfColumns) { 0 } placeables.forEach { placeable -> // Determine which column to place this item in val column = shortestColumn(columnYPointers) placeable.place( x = columnWidth * column, y = columnYPointers[column], ) // Update the Y pointer column vise based on the item // we just placed. columnYPointers[column] += placeable.height } } } }
Then call StaggeredGrid fun in onCreate method
class PhotosGridLayout : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MyApplicationTheme { // A surface container using the 'background' color from the theme Surface(color = MaterialTheme.colors.background) { Column(modifier = Modifier.verticalScroll(rememberScrollState())) { StaggeredGrid( images = listOf( R.drawable.wallpaper5, R.drawable.wallpaper1, R.drawable.wallpaper2, R.drawable.wallpaper3, R.drawable.wallpaper4, R.drawable.wallpaper5, R.drawable.dog ), numberOfColumns = 2 ) } } } } } }