Compose by example: BoxWithConstraints
source link: https://www.valueof.io/blog/compose-boxwithconstraints-boxwithconstraintsscope
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Compose by example: BoxWithConstraints
November 18, 2022
BoxWithConstraints
Composable lets us build adaptive layouts based on available height/width and other constraints. This post will provide an example when it can be useful.
Let’s imagine our Designer is asking to build a horizontal carousel with the following requirements:
in portrait mode, 2 cards are fully visible and an additional one is peeking letting users know that there is more content to scroll through
in landscape mode, 4 cards are fully visible and an additional one is peeking letting users know that there is more content to scroll through
To visualize this layout and behavior:
Since BoxWithConstraints
gives us access to maxWidth
and maxHeight
, we can:
Figure out if we are currently in portrait or landscape mode
Calculate width of each card based on available screen width also taking into consideration that one card should be peeking (partially visible)
Let’s see the code:
BoxWithConstraints { val boxWithConstraintsScope = this LazyRow( horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically, contentPadding = PaddingValues(24.dp) ) { items(cardData) { card -> if (boxWithConstraintsScope.maxWidth > maxHeight) { // in landscape mode val cardWidth = boxWithConstraintsScope.maxWidth / 4 MyCard( title = card.first, subtitle = card.second, height = boxWithConstraintsScope.maxHeight / 3, width = cardWidth - cardWidth * 0.15f ) } else { // in portrait mode val cardWidth = boxWithConstraintsScope.maxWidth / 2 MyCard( title = card.first, subtitle = card.second, height = boxWithConstraintsScope.maxHeight / 4, width = cardWidth - cardWidth * 0.2f ) } } } }
We can clean this up by creating an intermediate Composable scoped to BoxWithConstraintsScope
: and use maxWidth
and maxHeight
without specifying scope every time.
@Composable fun BoxWithConstraintsScope.AdaptiveLayoutCardList(cardData: List<Pair<String, String>>) { LazyRow( horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically, contentPadding = PaddingValues(24.dp) ) { items(cardData) { card -> if (maxWidth > maxHeight) { // in landscape mode val cardWidth = maxWidth / 4 MyCard( title = card.first, subtitle = card.second, height = maxHeight / 3, width = cardWidth - cardWidth * 0.15f ) } else { // in portrait mode val cardWidth = maxWidth / 2 MyCard( title = card.first, subtitle = card.second, height = maxHeight / 4, width = cardWidth - cardWidth * 0.2f ) } } } }
And calling it like so:
setContent { BoxWithConstraintsDemoTheme { val cardData = remember { generateCards() } BoxWithConstraints { AdaptiveLayoutCardList(cardData) } } }
To summarize, BoxWithConstraints
is similar to Box
but gives us access to boxWithConstraintsScope
that can help us analyze parent dimensions and available constraints to help you decide what content to display or how to lay it out.
Note that we calculate cardWidth
based on boxWithConstraintsScope.maxWidth
Affecting Compose phases
As a reminder, normally the order of Jetpack Compose phases is:
However, when BoxWithConstraints
is used, these phases are affected as follows:
The Composition phase is deferred until the Layout phase until the constraints and dimensions are known
More work is done in the Layout phase which may be noticeable, especially in complex layouts
boxWithConstraintsScope
makes it easy to figure if we are in landscape or portrait mode just by comparing width
and height
:
if (maxWidth > maxHeight) { // in landscape } else { // in portrait mode }
A more verbose alternative to determine the orientation would be LocalConfiguration
.
val configuration = LocalConfiguration.current when (configuration.orientation) { Configuration.ORIENTATION_LANDSCAPE -> { // in landscape mode } else -> { // in portrait mode } }
In this case, since we are already using BoxWithConstraints
, using LocalConfiguration
is unnecessary.
Full source for this example is below as well as at https://github.com/jshvarts/BoxWithConstraintsDemo
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { BoxWithConstraintsDemoTheme { Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { val cardData = remember { generateCards() } BoxWithConstraints { AdaptiveLayoutCardList(cardData) } } } } } } @Composable fun BoxWithConstraintsScope.AdaptiveLayoutCardList(cardData: List<Pair<String, String>>) { LazyRow( horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically, contentPadding = PaddingValues(24.dp) ) { items(cardData) { card -> if (maxWidth > maxHeight) { // in landscape mode val cardWidth = maxWidth / 4 MyCard( title = card.first, subtitle = card.second, height = maxHeight / 3, width = cardWidth - cardWidth * 0.15f ) } else { // in portrait mode val cardWidth = maxWidth / 2 MyCard( title = card.first, subtitle = card.second, height = maxHeight / 4, width = cardWidth - cardWidth * 0.2f ) } } } } @Composable fun MyCard( title: String, subtitle: String, height: Dp, width: Dp ) { Card( shape = RoundedCornerShape(12.dp), modifier = Modifier .height(height) .width(width) ) { Column( verticalArrangement = Arrangement.SpaceBetween, horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier .background(Color.DarkGray) .padding(24.dp) ) { Text( text = title, color = Color.White, style = MaterialTheme.typography.h6, textAlign = TextAlign.Center ) Text( text = subtitle, color = Color.White, style = MaterialTheme.typography.subtitle1, textAlign = TextAlign.Center ) } } } private fun generateCards(): List<Pair<String, String>> { return MutableList(20) { index -> val cardNumber = index + 1 "Title $cardNumber" to "Subtitle $cardNumber" } } @Preview(showBackground = true) @Composable fun MyCardPreview( title: String = "Title 1", subtitle: String = "Subtitle 1", height: Dp = 80.dp, width: Dp = 60.dp ) { BoxWithConstraintsDemoTheme { MyCard( title = title, subtitle = subtitle, height = height, width = width ) } }
Recommend
-
88
Function Composition in Go I did this more to see if I could than because it's a good idea. The compose.New function can take two or more functions and compose them. It checks that the output of one function matches the input of the next....
-
89
Part three of a ten part series in creating microservices in golang. Using technologies such as Docker, Kubernetes, CircleCI, go-micro, MongodDB and more.
-
175
Docker Compose 1.18.0 之服务编排详解 一个使用Docker容器的应用,通常由多个容器组成。使用Docker Compose,不再需要使用shell脚本来启动容器。在配置文件中,所有的容器通过...
-
90
redux-bundler - Compose a redux store out of smaller bundles of functionality
-
76
react-composer - Compose render prop components
-
14
Clean Chat Example App with Jetpack ComposeJetpack Compose makes it easy to build beautiful UI. Check out this Chat UI sample, and learn some exciting bits of Compose along the way! M...
-
4
Example Voting App A simple distributed application running across multiple Docker containers. Getting started Download Docker Desktop for Mac or Windows.
-
12
example docker compose for postgresql with db init script · GitHub Instantly share code, notes, and snippets. ...
-
7
-
6
Dual-screen example adds Jetpack Compose to the experience A...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK