androidkotlinandroid-jetpack-compose

How to combine two jetpack compose boxes


I am trying to create A UI Components like below

enter image description here

As Part of this i split the task into two , the curved bump and the rectangle.

The curved bump code is below

@Composable
fun TriangularCurve() {
    Box(
        modifier = Modifier
            .fillMaxSize()
            .rotate(-90f),
        contentAlignment = Alignment.Center
    ) {
        val curvePath = createGaussianPath(0.5f, 0.65f, 0.44f)
        Box(
            modifier = Modifier
                .fillMaxSize()
                .background(onboardingSideBarColour, shape = curvePath),
            contentAlignment = Alignment.Center
        ) {
            ConcentricCircles(
                innerRadius = 39.dp,
                outerRadius = 44.dp,
                innerColor = onboardingSideBarColour,
                outerColor = Color.White
            )
            Icon(
                painter = painterResource(id = R.drawable.arrowdouble),
                contentDescription = stringResource(id = R.string.arrow_icon_description),
                tint = Color.White,
                modifier = Modifier
                    .size(50.dp)
                    .rotate(270f)
            )
        }
    }
}

and it looks like this

enter image description here

then there is the rectangular component


@Composable
fun Rectangle() {
    Box(
        modifier = Modifier
            .fillMaxHeight()
            .width(20.dp)
            .background(onboardingSideBarColour) // Background color for the rectangle
    )
}

enter image description here

How can i combine them both side by side like the image at the very top?


Solution

  • You can achieve your goal in two ways:

    First Option: Using Two Composables

    Create two separate composables (like you did): one for the rectangle and another for the Gaussian curve. Then, combine them in a Row. Since you didn't provide the createGaussianPath() content, I implemented a basic version using two cubic Bézier curves.

    Here’s the code:

    class MainActivity : ComponentActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            enableEdgeToEdge()
            setContent {
                CombineBoxAppTheme {
                    Row(modifier = Modifier
                        .padding(4.dp)
                        .fillMaxSize()) {
                        CombinedBoxes(
                            fillColor = Color.Blue,
                            modifier = Modifier
                                .fillMaxHeight()
                                .width(80.dp)
                        )
                        Box(modifier = Modifier
                            .weight(1f)
                        )
                    }
                }
            }
        }
    }
    
    
    private fun Path.createGaussianPath(size: Size) {
        val width = size.width
        val height = size.height
    
        val start = Offset(width, 0f)
        val center = Offset(0f, height/2f)
        val end = Offset(width, height)
    
        // implement gaussian cure using two cubic bezier curves
        val centerDistance = 100f // you can tweak this value to adjust the curvature
        val sideDistance = 40f // you can tweak this value to adjust the curvature
    
        val c1 = Offset(width, sideDistance)
        val c2 = Offset(0f, center.y - centerDistance)
        val c3 = Offset(0f, center.y + centerDistance)
        val c4 = Offset(width, end.y - sideDistance)
    
        moveTo(start.x, start.y)
    
        cubicTo(
            x1 = c1.x,
            y1 = c1.y,
            x2 = c2.x,
            y2 = c2.y,
            x3 = center.x,
            y3 = center.y
        )
        cubicTo(
            x1 = c3.x,
            y1 = c3.y,
            x2 = c4.x,
            y2 = c4.y,
            x3 = end.x,
            y3 = end.y
        )
    }
    
    @Composable
    fun TriangleCurve(
        modifier: Modifier = Modifier,
        fillColor: Color = Color.Blue,
    ) {
        val shape = remember {
            GenericShape { size, _ -> createGaussianPath(size = size) }
        }
    
        Box(
            modifier = modifier
                .size(100.dp)
                .background(color = fillColor, shape = shape),
            contentAlignment = Alignment.Center
        ) {
            Icon(
                painter = painterResource(id = R.drawable.arrowdouble),
                contentDescription = null,
                tint = Color.White,
                modifier = Modifier
                    .size(40.dp)
                    .border(color = Color.White, shape = CircleShape, width = 1.dp)
            )
        }
    }
    
    @Preview(showBackground = true)
    @Composable
    private fun TriangleCurvePreview() {
        TriangleCurve(
            modifier = Modifier.size(width = 50.dp, height = 200.dp),
            fillColor = Color.Blue)
    }
    
    @Composable
    fun Rectangle(
        modifier: Modifier = Modifier,
        fillColor: Color = Color.Blue
    ) {
        Box(
            modifier = modifier
                .fillMaxHeight()
                .background(fillColor) // Background color for the rectangle
        )
    }
    
    @Composable
    fun CombinedBoxes(
        modifier: Modifier = Modifier,
        fillColor: Color = Color.Blue
    ) {
        Row(
            modifier = modifier,
            horizontalArrangement = Arrangement.Start,
            verticalAlignment = Alignment.CenterVertically,
        ) {
            TriangleCurve(
                modifier = Modifier.size(width = 50.dp, height = 200.dp),
                fillColor = fillColor
            )
            Rectangle(
                modifier = Modifier.width(20.dp),
                fillColor = fillColor
            )
        }
    }
    

    Demo:

    Demo first option

    Second Option: Single Path

    The second method, which I recommend, involves creating the entire shape using one path. Here's how to do it:

    class MainActivity : ComponentActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            enableEdgeToEdge()
            setContent {
                CustomBoxAppTheme {
                    Row(modifier = Modifier.padding(4.dp)) {
                        SideBar(
                            modifier = Modifier
                                .fillMaxHeight()
                                .width(80.dp)
                        ) {
                            Icon(
                                painter = painterResource(id = R.drawable.arrowdouble),
                                contentDescription = null,
                                tint = Color.White,
                                modifier = Modifier
                                    .size(50.dp)
                                    .border(color = Color.White, shape = CircleShape, width = 1.dp)
                            )
                        }
                        Box(modifier = Modifier.weight(1f))
                    }
                }
            }
        }
    }
    
    fun Path.buildSideBarPath(
        size: Size,
    ) {
        val width = size.width
        val height = size.height
    
        val thickness = width/4f // you can tweak this value to adjust the thickness
    
        val r = width - thickness
    
        val centerY = height/2f
    
        // Move to the top left corner
        moveTo(r, 0f)
    
        val start = Offset(r, centerY - r)
        val center = Offset(0f, centerY)
        val end = Offset(r, centerY + r)
    
        // implement gaussian cure using two cubic bezier curves
        val centerDistance = r/2 // you can tweak this value to adjust the curvature
        val sideDistance = r/4 // you can tweak this value to adjust the curvature
    
        val c1 = Offset(r, start.y + sideDistance)
        val c2 = Offset(0f, center.y - centerDistance)
        val c3 = Offset(0f, center.y + centerDistance)
        val c4 = Offset(r, end.y - sideDistance)
    
        lineTo(start.x, start.y)
        cubicTo(
            x1 = c1.x,
            y1 = c1.y,
            x2 = c2.x,
            y2 = c2.y,
            x3 = center.x,
            y3 = center.y
        )
        cubicTo(
            x1 = c3.x,
            y1 = c3.y,
            x2 = c4.x,
            y2 = c4.y,
            x3 = end.x,
            y3 = end.y
        )
    
        lineTo(r, height)
        lineTo(width, height)
        lineTo(width, 0f)
        close()
    }
    
    
    @Composable
    fun SideBar(
        modifier: Modifier = Modifier,
        fillColor: Color = Color.Blue,
        content: @Composable () -> Unit
    ) {
    
        val shape = remember {
            GenericShape { size, _ -> buildSideBarPath(size = size) }
        }
    
        Box(
            modifier = modifier
                .fillMaxSize()
                .background(color = fillColor, shape = shape),
            contentAlignment = Alignment.Center
        ) {
            content()
        }
    }
    

    Demo:

    Demo second option