Building a Relaxing Meditation UI with Jetpack Compose

Fahad Habib
6 min readMay 2, 2024

--

This guide takes you through creating a visually appealing and functional home screen for a meditation app using Jetpack Compose. We’ll explore reusable composable functions that work together to build the complete UI.

Prerequisites

  • Basic understanding of Kotlin
  • Familiarity with Android development concepts
  • Android Studio installed with the latest Jetpack Compose plugins

Assuming you have the prerequisites covered, let’s jump into the code!

Building the Bottom Navigation Menu (BottomMenu)

The bottom navigation menu provides users with quick access to different sections of the app. Here’s the code breakdown for the BottomMenu composable function:

@Composable
fun BottomMenu(
items: List<BottomMenuContent> ,
modifier: Modifier = Modifier ,
activeHighlightColor: Color = ButtonBlue ,
activeTextColor: Color = White,
inactiveTextColor: Color = AquaBlue,
initialSelectedItemIndex: Int = 0
){
var selectedItemIndex by remember {
mutableIntStateOf(initialSelectedItemIndex)
}
Row(
horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
.fillMaxWidth()
.background(DeepBlue)
.padding(15.dp)
) {
items.forEachIndexed{index,items ->
BottomMenuItem(
item = items,
isSelected = index == selectedItemIndex,
activeHighlightColor = activeHighlightColor,
activeTextColor = activeTextColor,
inactiveTextColor = inactiveTextColor
) {
selectedItemIndex = index
}
}
}
}
  • The BottomMenu function takes a list of BottomMenuContent items containing title and icon information.
  • It uses a Row to arrange the items horizontally with equal spacing.
  • The function iterates through the items list and builds a BottomMenuItem for each item.
  • A remember state variable tracks the currently selected item index, which is updated based on user interaction.

Creating Interactive Navigation Items (BottomMenuItem)

Each item within the bottom navigation menu is represented by the BottomMenuItem composable function. It defines the visual appearance and interactive behavior of a single navigation item.

@Composable
fun BottomMenuItem(
item: BottomMenuContent,
isSelected: Boolean = false,
activeHighlightColor: Color = ButtonBlue ,
activeTextColor: Color = White,
inactiveTextColor: Color = AquaBlue,
onItemClick: () -> Unit
){
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.clickable { onItemClick() }
) {
Box(modifier = Modifier
.clip(RoundedCornerShape(10.dp))
.background(if (isSelected) activeHighlightColor else Color.Transparent)
.padding(10.dp))
{
Icon(painter = painterResource(id = item.iconId) ,
contentDescription = item.title,
tint = if (isSelected)activeTextColor else inactiveTextColor,
modifier = Modifier.size(20.dp)
)
}
Text(text = item.title,
color = (if (isSelected) activeTextColor else inactiveTextColor))
}
}
  • The BottomMenuItem takes an item of type BottomMenuContent (containing title and icon information) as input.
  • It uses a Column to stack the icon and text vertically within a clickable area (Modifier.clickable).
  • Clicking the item triggers the onItemClick callback function, allowing for custom actions based on user interaction.

GreetingSection: This composable personalizes the user experience by displaying a greeting message and a search icon.

@Composable
fun GreetingSection(
name: String = "Developers"
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(15.dp)
) {
Column(
verticalArrangement = Arrangement.Center
) {
Text(
text = "Good morning, $name",
style = MaterialTheme.typography.headlineMedium,
color = White
)
Text(
text = "We wish you have a good day!",
style = MaterialTheme.typography.bodyLarge,
color = White
)
}
Icon(painter = painterResource(id = R.drawable.ic_search),
contentDescription ="Search",
tint = White,
modifier = Modifier.size(24.dp)
)
}
}

ChipSection: It showcases various meditation categories using selectable chips, allowing users to filter their meditation choices.

@Composable
fun ChipSection(
chips: List<String>
) {
var selectedChipIndex by remember {
mutableIntStateOf(0)
}
LazyRow {
items(chips.size) { index ->
Box(contentAlignment = Alignment.Center ,
modifier = Modifier
.padding(start = 15.dp , top = 15.dp , bottom = 15.dp)
.clickable {
selectedChipIndex = index
}
.clip(RoundedCornerShape(10.dp))
.background(
if (selectedChipIndex == index) ButtonBlue
else DarkerButtonBlue
)
.padding(15.dp)
)
{
Text(text = chips[index], color = TextWhite)
}
}
}
}

CurrentMediation: This section highlights information about the user’s daily meditation session, potentially including the duration and a play button.

@Composable
fun CurrentMediation(
color: Color = LightRed
){
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.padding(15.dp)
.clip(RoundedCornerShape(10.dp))
.background(color)
.padding(horizontal = 15.dp , vertical = 20.dp)
.fillMaxWidth()
) {
Column(
verticalArrangement = Arrangement.Center
) {
Text(
text = "Daily Thought",
style = MaterialTheme.typography.headlineSmall,
color = White,
)
Text(
text = "Mediation . 3-10 min",
style = MaterialTheme.typography.bodyMedium,
color = White
)
}
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.background(ButtonBlue)
.padding(10.dp)){
Icon(painter = painterResource(id = R.drawable.ic_play),
contentDescription ="play",
tint = White,
modifier = Modifier.size(15.dp)
)
}

}
}

FeaturedSection & FeatureItem: This area presents featured meditation options in a visually appealing grid layout, enticing users to explore specific meditation experiences.

@Composable
fun FeaturedSection(features: List<Feature>) {
Column(modifier = Modifier.fillMaxWidth()) {
Text(
text = "Featured",
style = MaterialTheme.typography.headlineLarge,
color = White,
modifier = Modifier.padding(15.dp)
)
LazyVerticalGrid(
columns = GridCells.Fixed(2),
contentPadding = PaddingValues(start = 7.5.dp, end = 7.5.dp, bottom = 100.dp),
modifier = Modifier.fillMaxHeight()
){
items(features.size){
FeatureItem(feature = features[it])
}
}
}
}

@Composable
fun FeatureItem(
feature: Feature
) {
BoxWithConstraints(
modifier = Modifier
.padding(7.5.dp)
.aspectRatio(1f)
.clip(RoundedCornerShape(10.dp))
.background(feature.darkColor)
) {
val width = constraints.maxWidth
val height = constraints.maxHeight

// Medium colored path
val mediumColoredPoint1 = Offset(0f, height * 0.3f)
val mediumColoredPoint2 = Offset(width * 0.1f, height * 0.35f)
val mediumColoredPoint3 = Offset(width * 0.4f, height * 0.05f)
val mediumColoredPoint4 = Offset(width * 0.75f, height * 0.7f)
val mediumColoredPoint5 = Offset(width * 1.4f, -height.toFloat())

val mediumColoredPath = Path().apply {
moveTo(mediumColoredPoint1.x, mediumColoredPoint1.y)
standardQuadFromTo(mediumColoredPoint1, mediumColoredPoint2)
standardQuadFromTo(mediumColoredPoint2, mediumColoredPoint3)
standardQuadFromTo(mediumColoredPoint3, mediumColoredPoint4)
standardQuadFromTo(mediumColoredPoint4, mediumColoredPoint5)
lineTo(width.toFloat() + 100f, height.toFloat() + 100f)
lineTo(-100f, height.toFloat() + 100f)
close()
}

// Light colored path
val lightPoint1 = Offset(0f, height * 0.35f)
val lightPoint2 = Offset(width * 0.1f, height * 0.4f)
val lightPoint3 = Offset(width * 0.3f, height * 0.35f)
val lightPoint4 = Offset(width * 0.65f, height.toFloat())
val lightPoint5 = Offset(width * 1.4f, -height.toFloat() / 3f)

val lightColoredPath = Path().apply {
moveTo(lightPoint1.x, lightPoint1.y)
standardQuadFromTo(lightPoint1, lightPoint2)
standardQuadFromTo(lightPoint2, lightPoint3)
standardQuadFromTo(lightPoint3, lightPoint4)
standardQuadFromTo(lightPoint4, lightPoint5)
lineTo(width.toFloat() + 100f, height.toFloat() + 100f)
lineTo(-100f, height.toFloat() + 100f)
close()
}
Canvas(
modifier = Modifier
.fillMaxSize()
) {
drawPath(
path = mediumColoredPath,
color = feature.mediumColor
)
drawPath(
path = lightColoredPath,
color = feature.lightColor
)
}
Box(
modifier = Modifier
.fillMaxSize()
.padding(15.dp)
) {
Text(
text = feature.title,
style = MaterialTheme.typography.titleLarge,
lineHeight = 26.sp,
modifier = Modifier.align(Alignment.TopStart)
)
Icon(
painter = painterResource(id = feature.iconId),
contentDescription = feature.title,
tint = White,
modifier = Modifier.align(Alignment.BottomStart)
)
Text(
text = "Start",
color = TextWhite,
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier
.clickable {
// Handle the click
}
.align(Alignment.BottomEnd)
.clip(RoundedCornerShape(10.dp))
.background(ButtonBlue)
.padding(vertical = 6.dp , horizontal = 15.dp)
)
}
}
}

Finally combine all the composable in MeditationHomeScreen composable

@Composable
fun MediationHomeScreen(){
Box(modifier = Modifier
.background(DeepBlue)
.fillMaxSize()) {
Column {
GreetingSection()
ChipSection(chips = listOf("Sweet Sleep", "Insomnia", "Depression"))
CurrentMediation()
FeaturedSection(
features = listOf(
Feature(
"Sleep Mediation" ,
R.drawable.ic_headphone ,
BlueViolet1 ,
BlueViolet2 ,
BlueViolet3 ,
) ,
Feature(
"Tips for sleeping" ,
R.drawable.ic_videocam ,
LightGreen1 ,
LightGreen2 ,
LightGreen3 ,
) ,
Feature(
"Night island" ,
R.drawable.ic_moon ,
OrangeYellow1 ,
OrangeYellow2 ,
OrangeYellow3 ,
) ,
Feature(
"Calming sounds" ,
R.drawable.ic_bubble ,
Beige1 ,
Beige2 ,
Beige3 ,
)
)
)

}
BottomMenu(items = listOf(
BottomMenuContent("Home", R.drawable.ic_home),
BottomMenuContent("Meditate", R.drawable.ic_bubble),
BottomMenuContent("Sleep", R.drawable.ic_moon),
BottomMenuContent("Music", R.drawable.ic_music),
BottomMenuContent("Profile", R.drawable.ic_profile),
), modifier = Modifier.align(Alignment.BottomCenter))
}
}

Conclusion

By leveraging Jetpack Compose’s composable functions, you can build a visually stunning and interactive home screen for your meditation app. This guide has provided a foundation for understanding the key components and their functionalities. Feel free to explore further and customize these functions to create a unique user experience that promotes relaxation and well-being.

#HappyCoding

--

--

Fahad Habib

🚀 Android Dev | Kotlin 📱 | UI/UX Enthusiast 🎨