Horizontal weather cards are the second portion in my forecast screen. It contains a message, relative timestamp and an image depicting the weather. Since I don’t have images in handy, I picked one and used for all the cards. I got creative with messages though.
Why box layout?
We have card and an image stacked in z order and they overlap. A constraint layout can be used to build this card, but I wanted to try box. It’s like FrameLayout to me. Also using constraint layout for simple layout is an overkill. Be it android native UI or Compose.
Designing a Weather Card
The overall composable blueprint looks like in below image. To understand better, let’s build this layout bottom-up.
…
Card content
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// card content
Column(modifier = Modifier.padding(16.dp)) {
Spacer(modifier = Modifier.height(24.dp))
// Time
Text(
text = time, // "20 minutes ago",
style = MaterialTheme.typography.caption
)
Spacer(modifier = Modifier.height(8.dp))
// Message
Text(
text = message, // "If you don't want to get wet today, don't forget your umbrella.",
style = MaterialTheme.typography.body1
)
Spacer(modifier = Modifier.height(24.dp))
}
Two labels wrapped in a Column
with spacer makes our card content. Spacers are added in between the elements to make up required spacing between the labels and the parent (Column
).
…
Card design
Now put a card around it and apply few modifiers to it.
- a shape —
RoundedCornerShape
- top padding — to make space for weather icon on top right
1
2
3
4
5
6
7
Card(
shape = RoundedCornerShape(16.dp),
modifier = Modifier
.padding(top = 40.dp, start = 16.dp, end = 16.dp, bottom = 8.dp)
) {
Column(...)
}
…
Putting all in a Box
Box layout stacks all the elements in given z order. Most recent one will be on the top. So, let’s put the Card first then the Image.
1
2
3
4
5
6
7
8
9
Box {
Card(...)
Image(
painter = painterResource(id = R.drawable.cloudy),
contentDescription = "weather overlap image",
modifier = Modifier
.size(100.dp)
)
}
Hmm… The rain wasn’t supposed to shower there. Let’s pull it to the right and shift a bit to the left. Children of Box has access to few properties to help with alignment inside the container.
- alignment —
Alignment.TopEnd
- offset —
x = (-40).dp
negative translation in horizontal direction
1
2
3
4
5
6
7
8
Image(
painter = painterResource(id = R.drawable.cloudy),
contentDescription = "weather overlap image",
modifier = Modifier
.size(100.dp)
.align(alignment = Alignment.TopEnd)
.offset(x = (-40).dp)
)
Horizontal pager implementation
Google accompanist has an incredibly good set of extension for compose. To implement the pager & indicator, we need corresponding dependencies added to our codebase.
1
2
implementation "com.google.accompanist:accompanist-pager:0.9.0"
implementation "com.google.accompanist:accompanist-pager-indicators:0.9.0"
Next step is to wrap the pager and indicator inside a Column and provide a list of mock objects to the render in WeatherCard that we built in above section.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fun WeatherCardCarousal(cards: List<WeatherCard>) {
val pagerState = rememberPagerState(pageCount = cards.size)
Column {
HorizontalPager(
state = pagerState
) { page ->
WeatherUpdateCard(cards[page])
}
HorizontalPagerIndicator(
pagerState = pagerState,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(16.dp),
)
}
}
pagerState
provides the current index of element for theHorizontalPager
WeatherUpdateCard(cards[page])
with the given index, corresponding element picked from mock data and rendered in WeatherCardHorizontalPagerIndicator
placed below the pager to react with page swipes in Pager. If it needs to be overlapped on the Pager. We’ll wrap them inside a Box and align it.
This is again an example for thinking in compose. Pager updates the pagerState
, and PagerIndicator
reads the same. Both of ‘em doesn’t know other component exist. Decoupled and yet communicating.
Complete source
Embedded the entire gist below. If not loaded properly, use this link.
Happy composing!!