Group 6.png

Objective

Recreate the Custom Dropdown Menu UI component from the Taskose UI design kit I acquired, utilizing Android Jetpack Compose.

Screenshot 2024-03-14 at 2.11.11 p.m..png

Environment

Implementation

Step 1

Download the needed icons here, and add them to res/drawable directory.

Step 2

Create CustomDropdownMenu.kt.

// Your package ...

import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

// Styles configuration for dropdown menu
data class CustomDropdownMenuStyles(
    val height: Dp = 40.dp,
    val mainColor: Color = Color(0xFF31394F),
    val strokeColor: Color = mainColor.copy(alpha = 0.4f),
    val buttonContainerColor: Color = Color(0xFFF8FAFC),
    val buttonCornerRadius: Dp = 12.dp,
    val buttonVerticalPadding: Dp = 12.dp,
    val buttonBorderStroke: BorderStroke = BorderStroke(1.dp, strokeColor),
    val buttonHorizontalPadding: Dp = 16.dp,
    val buttonTextStyle: TextStyle = TextStyle(
        fontWeight = FontWeight.Medium,
        fontSize = 14.sp,
        color = mainColor.copy(alpha = 0.8f)
    ),
    val menuItemTextStyle: TextStyle = TextStyle(
        fontWeight = FontWeight.Normal,
        fontSize = 14.sp,
        color = mainColor.copy(alpha = 0.8f)
    ),
    val menuItemHeight: Dp = 40.dp,
    val menuItemHorizontalPadding: Dp = 16.dp,
    val iconColor: Color = mainColor.copy(alpha = 0.8f),
    val leftIconDrawableId: Int? = null,
    val expandedLeftIconDrawable: Int? = null,
    val rightIconDrawableId: Int? = null,
    val expandedRightIconDrawable: Int? = null
)

// Chooses the correct icon based on the dropdown's state
fun CustomDropdownMenuStyles.getIconDrawable(expanded: Boolean, isLeftIcon: Boolean): Int? {
    return if (isLeftIcon) {
        if (expanded) expandedLeftIconDrawable ?: leftIconDrawableId else leftIconDrawableId
    } else {
        if (expanded) expandedRightIconDrawable ?: rightIconDrawableId else rightIconDrawableId
    }
}

// Renders a custom dropdown menu
@Composable
fun CustomDropdownMenu(
    modifier: Modifier = Modifier,
    buttonText: String = "Select an item",
    menuItems: List<Pair<String, () -> Unit>>,
    styles: CustomDropdownMenuStyles = CustomDropdownMenuStyles()
) {
    val expanded = remember { mutableStateOf(false) }

    Column(modifier = modifier) {
        // Dropdown button that controls menu visibility
        OutlinedDropdownButton(
            buttonText = buttonText,
            leftIconDrawableId = styles.getIconDrawable(expanded.value, isLeftIcon = true),
            rightIconDrawableId = styles.getIconDrawable(expanded.value, isLeftIcon = false),
            styles = styles
        ) {
            expanded.value = true
        }
        // Conditional display of menu items
        DropdownMenu(
            expanded = expanded.value,
            onDismissRequest = { expanded.value = false }
        ) {
            menuItems.forEach { (label, onClick) ->
                DropdownMenuItem(
                    text = { Text(label, style = styles.menuItemTextStyle) },
                    onClick = {
                        expanded.value = false
                        onClick()
                    },
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(styles.menuItemHeight),
                    contentPadding = PaddingValues(horizontal = styles.menuItemHorizontalPadding)
                )
            }
        }
    }
}

// Dropdown button component
@Composable
fun OutlinedDropdownButton(
    buttonText: String,
    leftIconDrawableId: Int?,
    rightIconDrawableId: Int?,
    styles: CustomDropdownMenuStyles,
    onClick: () -> Unit
) {
    OutlinedButton(
        onClick = onClick,
        modifier = Modifier
            .height(styles.height)
            .wrapContentWidth(),
        border = styles.buttonBorderStroke,
        shape = RoundedCornerShape(styles.buttonCornerRadius),
        colors = ButtonDefaults.outlinedButtonColors(
            containerColor = styles.buttonContainerColor
        ),
        contentPadding = PaddingValues(
            horizontal = styles.buttonHorizontalPadding,
            vertical = styles.buttonVerticalPadding
        )
    ) {
        Row(verticalAlignment = Alignment.CenterVertically) {
            // Optional left icon
            leftIconDrawableId?.let {
                Icon(
                    imageVector = ImageVector.vectorResource(id = it),
                    contentDescription = null,
                    tint = styles.iconColor
                )
                Spacer(modifier = Modifier.width(4.dp))
            }
            // Button text
            Text(buttonText, style = styles.buttonTextStyle)
            // Optional right icon
            rightIconDrawableId?.let {
                Spacer(modifier = Modifier.width(4.dp))
                Icon(
                    imageVector = ImageVector.vectorResource(id = it),
                    contentDescription = null,
                    tint = styles.iconColor
                )
            }
        }
    }
}

Step 3

Create CustomDropdownMenuPreview.kt.

// Your package ...

import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
// import YOUR_PACKAGE_PATH.R

@Preview
@Composable
fun CustomDropdownMenuPreview() {
    val context = LocalContext.current
    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(Color(0xFFB4BACC)),
        contentAlignment = Alignment.Center
    ) {
        Column {
            CustomDropdownMenu(
                menuItems = listOf(
                    "Item 1" to {
                        Toast
                            .makeText(context, "Item 1 clicked", Toast.LENGTH_SHORT)
                            .show()
                    },
                    "Item 2" to {
                        Toast
                            .makeText(context, "Item 2 clicked", Toast.LENGTH_SHORT)
                            .show()
                    },
                    "Item 3" to {
                        Toast
                            .makeText(context, "Item 3 clicked", Toast.LENGTH_SHORT)
                            .show()
                    }
                ),
                styles = CustomDropdownMenuStyles(
                    rightIconDrawableId = R.drawable.ic_expand_more,
                    expandedRightIconDrawable = R.drawable.ic_expand_less
                ),
            )
            Spacer(modifier = Modifier.height(16.dp))
            CustomDropdownMenu(
                buttonText = "Select an option",
                menuItems = listOf(
                    "Option 1" to {
                        Toast
                            .makeText(context, "Option 1 clicked", Toast.LENGTH_SHORT)
                            .show()
                    },
                    "Option 2" to {
                        Toast
                            .makeText(context, "Option 2 clicked", Toast.LENGTH_SHORT)
                            .show()
                    },
                    "Option 3" to {
                        Toast
                            .makeText(context, "Option 3 clicked", Toast.LENGTH_SHORT)
                            .show()
                    }
                ),
                styles = CustomDropdownMenuStyles(
                    leftIconDrawableId = R.drawable.ic_more
                )
            )
        }
    }
}

Preview Result

untitled.gif