Building a Relaxing Meditation UI with Jetpack Compose
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 ofBottomMenuContent
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 aBottomMenuItem
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 typeBottomMenuContent
(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