Blog Infos
Author
Published
Topics
Published

Photo by Codioful (Formerly Gradienta) on Unsplash & App Design by https://dribbble.com/nick_buturishvili

 

This is part of a series in which we build a Crypto Trade App using Compose:

https://medium.com/@zurcher/list/crypto-trade-app-in-jetpack-compose-bdbce2f1c5d4

In the last article, we completed what we called the Home Header. Today, we’ll continue iterating on our implementation and focus on the Crypto Cap View, which displays a preview of our full Crypto Cap on the home screen.

Today’s design spec

My Crypto Cap View

Let’s begin by creating a new Composable under our composables/home package to keep things organized in our project.

Now, let’s write the root function of our new Composable.

@Composable
fun CTAMyCryptoCap() {
}

This new view looks quite similar to a native Card, so for now, let’s start with that and see if we can make it have the exact shape specified in the design.

@Composable
fun CTAMyCryptoCap(modifier: Modifier = Modifier) {
Card(
modifier = Modifier
.background(color = FullWhite)
.padding(10.dp)
.fillMaxWidth()
.height(350.dp),
colors = CardDefaults.cardColors(
containerColor = CryptoOrange
),
shape = RoundedCornerShape(20)
) {
}
}

The background seems to have a vertical gradient, so I grabbed the top and bottom color and created 2 new color entries. With them, I can create a new Brush that creates the gradient, and I add a Box filling all the available size in the Card with this Brush as a background (since this is not doable for the background of the card, it seems!).

@Composable
fun CTAMyCryptoCap(modifier: Modifier = Modifier) {
val verticalOrangeGradient = Brush.verticalGradient(
colors = listOf(
CryptoOrange2,
CryptoOrange3
)
)
Card(
modifier = Modifier
.background(color = FullWhite)
.padding(10.dp)
.fillMaxWidth()
.height(350.dp),
shape = RoundedCornerShape(20)
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(verticalOrangeGradient)
) {
}
}
}

To continue developing the contents of our view, we need to bring in the data. We’ll define a new Data Class for it with the contents that will be required for now.

data class MyCryptoCapUIData(val value: Float, val currency: String)

We’ll use this data class to add a new parameter to our Composable and declare a “mockData” private value that will serve us as the default value to preview the Composable.

//...
private val mockData = MyCryptoCapUIData(38546.82f, "USD")
@Composable
fun CTAMyCryptoCap(modifier: Modifier = Modifier, data: MyCryptoCapUIData = mockData) {
//...

And finally, we’ll bring in 2 new Text composables inside a Column so that they are displayed on top of each other.

//...
Column {
Text(
text = "My Crypto Cap",
color = Color.White,
style = MaterialTheme.typography.displaySmall
)
Text(
text = "${data.value} ${data.currency}",
color = Color.White,
style = MaterialTheme.typography.displaySmall
)
}
//...
view raw CTACryptoCap.kt hosted with ❤ by GitHub

That’s not passing the first round of QA

 

And with some final styling tweaks, it’s already looking way closer to spec.

//...
Column(modifier = Modifier.padding(top = 50.dp, start = 30.dp)) { //<-- Adding padding
Text(
text = "My Crypto Cap",
color = Color.White,
style = MaterialTheme.typography.displaySmall,
fontWeight = FontWeight.ExtraLight //<-- Updating font weight
)
Text(
text = "${data.value} ${data.currency}",
color = Color.White,
style = MaterialTheme.typography.displayMedium, //<-- Updating style
fontWeight = FontWeight.ExtraBold //<-- Updating font weight
)
}
//...

Already looking good

 

Now let’s focus for a second on that curvy shape in the background of the card. In a real-world scenario, this might be an asset that we request from the designer so that we can match the background for both iOS and Android (if we are writing native apps, of course). But given I don’t have a direct line with the designer in this case and that this is, after all, a Jetpack Compose tutorial, let’s add that line “The Compose Way,” which, by the way, is the lightest option anyway.

To achieve this curvy shape, we’ll use a combination of 2 tools Compose gives us:

Shoutout to Vikas for his awesome post on how to use them:

//...
Box(
modifier = Modifier
.fillMaxSize()
.background(verticalOrangeGradient)
.drawBehind {
// Here we can access the DrawScope of our Box
}
//...

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Migrating to Jetpack Compose – an interop love story

Most of you are familiar with Jetpack Compose and its benefits. If you’re able to start anew and create a Compose-only app, you’re on the right track. But this talk might not be for you…
Watch Video

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer for Jetpack Compose
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engin ...
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer f ...
Google

Jobs

We’ll now move the initial pointer of the path to the top right of our drawing space to the point (x = 90% of the available width, y = 0% of the available height).

//...
val stroke = Path().apply {
moveTo(size.width.times(.9f), size.height.times(0f))
//...
view raw CryptoCard.kt hosted with ❤ by GitHub

We draw our 2 Bezier lines, update the Stroke color and size.

val stroke = Path().apply {
moveTo(size.width.times(.9f), size.height.times(0f))
quadraticBezierTo(
size.width.times(.9f), size.height.times(.28f),
size.width.times(.73f), size.height.times(.15f)
)
quadraticBezierTo(
size.width.times(.53f), size.height.times(0f),
size.width.times(.55f), size.height.times(.25f)
)
}
drawPath(
stroke,
color = CryptoOrange4,
style = Stroke(
width = 50f,
cap = StrokeCap.Round
)
)

I’m not gonna lie; placing those Beziers correctly takes some trial and error plus some drawing on paper helps, but the result speaks for itself!

Let’s extract our curvy line to keep our Composable readable.

private fun DrawScope.drawCurvyLine() {
val stroke = Path().apply {
moveTo(size.width.times(.9f), size.height.times(0f))
quadraticBezierTo(
size.width.times(.9f), size.height.times(.28f),
size.width.times(.73f), size.height.times(.15f)
)
quadraticBezierTo(
size.width.times(.53f), size.height.times(0f),
size.width.times(.55f), size.height.times(.25f)
)
}
drawPath(
stroke,
color = CryptoOrange4,
style = Stroke(
width = 50f,
cap = StrokeCap.Round
)
)
}
view raw curvyLine.kt hosted with ❤ by GitHub
//...
Box(
modifier = Modifier
.fillMaxSize()
.background(verticalOrangeGradient)
.drawBehind { drawCurvyLine() } //<--- NEW
) {
Column(modifier = Modifier.padding(top = 50.dp, start = 30.dp)) {
//...

That’s much better!

We need to turn our attention to the Bar Chart at the bottom of our widget now. This time let’s define the data that will fulfill it first, and then we can use that to build it.

From a glance, we can tell that it displays the cap for 5 different months at the same time and displays highlighted the one with the biggest cap (could work differently, but we’ll take this assumption due to the lack of context surrounding its behaviour). Worth mentioning that this logic to highlight the highest cap is a perfect scenario to do TDD and add coverage for this functionality with a Compose Test 😉

Building High Quality Android UI

Let’s extend our existing data class to have this information too.

data class MyCryptoCapUIData(
val value: Float,
val currency: String,
val monthlyPreview: List<Pair<String, Float>> //<---- NEW
)
private val mockData =
MyCryptoCapUIData(
38546.82f,
"USD",
listOf( //<---- NEW
Pair("Jan", 15000f),
Pair("Feb", 20000f),
Pair("Mar", 38000f),
Pair("Apr", 8000f),
Pair("May", 10000f)
)
)

And let’s place a new Composable under our 2 Text elements inside the Column.

Column(modifier = Modifier.padding(top = 50.dp, start = 30.dp)) {
Text(
text = "My Crypto Cap",
color = Color.White,
style = MaterialTheme.typography.displaySmall,
fontWeight = FontWeight.ExtraLight
)
Text(
text = "${data.value} ${data.currency}",
color = Color.White,
style = MaterialTheme.typography.displayMedium,
fontWeight = FontWeight.ExtraBold
)
MonthlyCapPreview(data.monthlyPreview) //<--- HERE
}
@Composable
fun MonthlyCapPreview(monthlyPreview: List<Pair<String, Float>>) {
TODO("Not yet implemented")
}

We’ll bring in a Row Composable so that we can iterate over the list of elements and draw Cards for each of them. To make it fully dynamic (or value-agnostic), we’ll determine the height of the Card based on the max value of the list and with it set a percentage-based size using the fillMaxHeight modifier.

@Composable
fun MonthlyCapPreview(monthlyPreview: List<Pair<String, Float>>) {
val maxMonthValue = monthlyPreview.maxBy { it.second }.second //<-- We find the max Float value of our Pair List
Row(
modifier = Modifier.fillMaxSize(),
verticalAlignment = Alignment.Bottom //<--- This is important to align them as required by spec
) {
for (pairPreview in monthlyPreview) {
val columnHeightWeight = pairPreview.second / maxMonthValue //<-- We use it to get a value between 0 and 1
Card(
modifier = Modifier
.weight(1f)
.fillMaxHeight(columnHeightWeight) //<-- Pass it through the modifier to determine Its height
.padding(5.dp)
) {
Text(text = pairPreview.first)
}
}
}
}

And to be able to display each month too, we’ll put a second Row for month names and bring it all together inside a Column.

@Composable
fun MonthlyCapPreview(monthlyPreview: List<Pair<String, Float>>) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
val maxMonthValue = monthlyPreview.maxBy { it.second }.second
Row(
modifier = Modifier.fillMaxHeight(.8f),
verticalAlignment = Alignment.Bottom
) {
for (pairPreview in monthlyPreview) {
val columnHeightWeight = pairPreview.second / maxMonthValue
Card(
modifier = Modifier
.weight(1f)
.fillMaxHeight(columnHeightWeight)
.padding(5.dp)
) { }
}
}
Row(
verticalAlignment = Alignment.CenterVertically
) {
for (pairPreview in monthlyPreview) {
Text(
modifier = Modifier.weight(1f),
text = pairPreview.first,
textAlign = TextAlign.Center
)
}
}
}
}

We are almost there, let’s add the final touches so we can match the Design. Add the functionality to highlight the highest month and update the text color of the text to white.

@Composable
fun MonthlyCapPreview(monthlyPreview: List<Pair<String, Float>>) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
val maxMonthValue = monthlyPreview.maxBy { it.second }.second
Row(
modifier = Modifier.fillMaxHeight(.8f),
verticalAlignment = Alignment.Bottom
) {
for (pairPreview in monthlyPreview) {
val columnHeightWeight = pairPreview.second / maxMonthValue
Card(
modifier = Modifier
.weight(1f)
.fillMaxHeight(columnHeightWeight)
.padding(5.dp),
colors = CardDefaults.cardColors(
containerColor = setHighlightColor(
pairPreview,
maxMonthValue
)
),
shape = RoundedCornerShape(30) //<--- Adding some "extra roundiness"
) { }
}
}
Row(
verticalAlignment = Alignment.CenterVertically
) {
for (pairPreview in monthlyPreview) {
Text(
modifier = Modifier.weight(1f),
text = pairPreview.first,
textAlign = TextAlign.Center,
fontWeight = FontWeight.SemiBold,
color = setHighlightColor(pairPreview, maxMonthValue)
)
}
}
}
}
@Composable
private fun setHighlightColor( //<--- Helper function to determine colour of Text and Bar
pairPreview: Pair<String, Float>,
maxMonthValue: Float
) = if (pairPreview.second == maxMonthValue) Color.White else Color.White.copy(
alpha = 0.4f
)

For the final touches, let’s update the background gradient to be radial instead of vertical, and let’s bring in a small loading animation so it’s a bit more alive.

//...
Box(
modifier = Modifier
.fillMaxSize()
.background(verticalOrangeGradient)
.drawBehind { drawCurvyLine() } //<--- NEW
) {
Column(modifier = Modifier.padding(top = 50.dp, start = 30.dp)) {
//...

Our 100% Functional Compose made UI

The Original Design Spec

 

That was a lot of code, but it was worth the push. Our custom UI element is looking just as the designer wanted it to, and it will be easy to update. In an upcoming post, we’ll add the finishing touches to our Home Screen so we can call it complete and move on to the next part of our crypto App.

Have a nice day! 🧉

P.S.: In this tag you can find the working example we reviewed today:

This article was previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu