Group 5.png

Objective

Recreate the Task Item UI component from the Taskose UI design kit I acquired, utilizing Android Jetpack Compose. We will delve into drawing custom shapes, previewing, and toggling the task state.

Screenshot 2024-03-13 at 3.15.46 p.m..png

Environment

Implementation

Download and add the ic_tick.xml icon from here (add it to res/drawable directory).

// Your package...

import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
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.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
// import YOUR_PACKAGE_PATH.R

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TaskItem(
    isCompleted: Boolean = false,
    title: String = "No title",
    dueDateText: String = "Due Date: No due date",
    cardHeight: Dp = 72.dp,
    borderRadius: Dp = 8.dp,
    cardBackgroundColor: Color = Color(0xFFF8FAFC),
    indicatorWidth: Dp = 8.dp,
    completedIndicatorColor: Color = Color(0xFF5FD788),
    pendingIndicatorColor: Color = Color(0xFFE2E8F0),
    titleColor: Color = Color(0xFF31394F),
    spacerWidth: Dp = 16.dp,
    titleDueDateSpacing: Dp = 8.dp,
    outerIndicatorDiameter: Dp = 28.dp,
    innerIndicatorDiameter: Dp = 20.dp,
    titleTextStyle: TextStyle = TextStyle(
        fontWeight = FontWeight.Bold,
        fontSize = 16.sp,
        color = titleColor
    ),
    dueDateTextStyle: TextStyle = TextStyle(
        fontWeight = FontWeight.Medium,
        fontSize = 14.sp,
        color = titleColor.copy(alpha = 0.6f)
    ),
    checkedIconDrawableId: Int = R.drawable.ic_tick,
    testTag: String = "",
    onClicked: () -> Unit = {}
) {
    Card(
        onClick = onClicked,
        modifier = Modifier
            .height(cardHeight)
            .fillMaxWidth(),
        shape = RoundedCornerShape(borderRadius),
        colors = CardDefaults.cardColors(containerColor = cardBackgroundColor)
    ) {
        Row(
            verticalAlignment = Alignment.CenterVertically
        ) {
            Box(
                modifier = Modifier
                    .fillMaxHeight()
                    .width(indicatorWidth)
                    .background(if (isCompleted) completedIndicatorColor else pendingIndicatorColor)
            )
            Spacer(modifier = Modifier.width(spacerWidth))
            Column(
                modifier = Modifier.weight(1f)
            ) {
                Text(text = title, style = titleTextStyle)
                Spacer(modifier = Modifier.height(titleDueDateSpacing))
                Text(text = dueDateText, style = dueDateTextStyle)
            }
            Box(
                modifier = Modifier.size(outerIndicatorDiameter),
                contentAlignment = Alignment.Center
            ) {
                // Canvas is used here for drawing custom circles
                // It occupies the size of its parent
                Canvas(modifier = Modifier.matchParentSize()) {
                    // Calculate the center point of the Canvas
                    // This will be used as the center for drawing circles
                    val center = Offset(x = size.width / 2, y = size.height / 2)

                    // Determine the color of the circles based on the task's completion status
                    // Completed tasks get a different color indicator than pending ones
                    val circleColor =
                        if (isCompleted) completedIndicatorColor
                        else pendingIndicatorColor

                    // The radius is half of the minimum dimension of the canvas
                    drawCircle(
                        // Make the outer circle color lighter for a halo effect
                        color = circleColor.copy(alpha = 0.3f),
                        center = center, // Use the calculated center point
                        // Calculate radius to fit the circle within the canvas
                        radius = size.minDimension / 2
                    )

                    // Draw the inner circle with a solid color to indicate the task status visually
                    drawCircle(
                        color = circleColor, // Use the solid color for the inner circle
                        center = center, // Use the same center point as the outer circle
                        // Convert the diameter to radius and to pixels for drawing
                        radius = innerIndicatorDiameter.toPx() / 2
                    )
                }
                if (isCompleted) {
                    Icon(
                        modifier = Modifier.testTag(testTag),
                        imageVector = ImageVector.vectorResource(id = checkedIconDrawableId),
                        contentDescription = null,
                        tint = Color.White
                    )
                }
            }
            Spacer(modifier = Modifier.width(spacerWidth))
        }
    }
}

@Preview
@Composable
fun TodoItemPreview() {
    val clickState = remember {
        mutableStateOf(false)
    }
    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.White)
            .background(Color(0xFFB0B0B1))
            .padding(24.dp)
    ) {
        Column {
            TaskItem(
                title = "Meeting with Client",
                dueDateText = "Due Date: May 19 2023",
                isCompleted = clickState.value,
                onClicked = {
                    clickState.value = !clickState.value
                }
            )
        }
    }
}

Preview Result

untitled.gif

Explore More

For developers eager to deepen their understanding or explore more about modern Android development, a wealth of resources and guides await.

Android Compose Tutorials Library

The Ultimate Android Development Career Guide

If you've found this resource helpful, consider sharing your support through claps or a follow, and stay tuned for more insights into the evolving landscape of Android development. See you the next time!