0

What do I want to achieve?

I want to create the following layout. It is important that the vertical "connecting" lines go inside the numbered boxes:

This is the result I want

What did I already try (first, naive approach)?

I created the layout using a composable and a few lines. The numbered boxes get a boolean parameter which indicate if the top or bottom lines inside the box are shown. There also are vertical lines between the composables which draw the vetical lines. It produces the expected result and renders this image:

The naive approach

This is the code I used:

@Composable
fun ConnectedComposable(modifier: Modifier) {
    Column(modifier = modifier) {
        NumberedBox(
            number = 1,
            topLine = false,
            bottomLine = true
        )
        VerticalDivider(Modifier.height(64.dp))
        NumberedBox(
            number = 2,
            topLine = true,
            bottomLine = true
        )
        VerticalDivider(Modifier.height(64.dp))
        NumberedBox(
            number = 3,
            topLine = true,
            bottomLine = false
        )
    }
}

@Composable
fun ColumnScope.VerticalDivider(
    modifier: Modifier = Modifier
) {
    Divider(
        modifier = modifier
            .width(2.dp)
            .align(Alignment.CenterHorizontally),
        color = Color.Black
    )
}

@Composable
fun NumberedBox(
    number: Int,
    topLine: Boolean,
    bottomLine: Boolean,
    modifier: Modifier = Modifier
) {
    Surface(
        color = Color.LightGray,
        border = BorderStroke(1.dp, Color.Black),
        modifier = modifier
            .size(100.dp)
            .fillMaxSize()
    ) {
        Column(
            modifier = Modifier
                .padding(horizontal = 16.dp)
                .fillMaxSize()
        ) {
            if (topLine) {
                VerticalDivider(
                    Modifier.weight(1f)
                )
            }
            else {
                Spacer(modifier = Modifier.weight(1f))
            }

            Text(
                number.toString(),
                modifier = Modifier
                    .align(Alignment.CenterHorizontally)
                    .padding(vertical = 16.dp),
                textAlign = TextAlign.Center
            )


            if (bottomLine) {
                VerticalDivider(
                    Modifier.weight(1f)
                )
            }
            else {
                Spacer(modifier = Modifier.weight(1f))
            }
        }
    }
}

@Preview
@Composable
fun ConnectedComposable_Preview() {
    Scaffold {
        ConnectedComposable(
            Modifier
                .padding(it)
                .padding(16.dp)
        )
    }
}

My question

I do not like the above code because of these points:

  • It is hard to maintain if the layout changes in the future
  • It gets more complex if the size of the NumberedBox is not fixed
  • The code to calculate the line positions inside and outside the box must be kept in sync
  • The positioning code may not be trivial if the lines are not centered.

Is there a different/better way to archieve this layout?

Bennik2000
  • 1,112
  • 11
  • 25
  • You should consider using a Layout instead (the equivalent of a ViewGroup in the view system) to position the boxes and the lines. – Francesc Jul 12 '22 at 23:09

2 Answers2

1

First option is to have Boxes and lines as Composables and lay them out accordingly, which is easy for people who started layout and constraints. This will need you to add NumberedBoxed and Dividers as child Composables.

@Composable
private fun DrawLineLayout(
    modifier: Modifier,
    content: @Composable () -> Unit
) {
    Layout(modifier = Modifier, 
           content = content) { measurables: List<Measurable>, constraints: Constraints ->

        val placeables = measurables.map { measurable ->
            measurable.measure(constraints)
        }

        // calculate max width and total height by offsetting 
        // VerticalDivider inside boxes
        // and x, y position of each Composable
        var totalHeight = 0
        val verticalPosition = mutableListOf<Int>()

        val maxWidth = placeables.maxOf { it.width }

        layout(maxWidth, totalHeight) {
            placeables.forEachIndexed() { index:Int, placeable: Placeable ->
                placeable.placeRelative(xPos, verticalPosition[index])
            }
        }
    }
}

Second option is using SubcomposeLayout, i added samples here, here, here and here, to draw boxes as mainConent and drawing overlay after getting position from subcomposing with SubcompoeLayout and passing these positions to dependent content.

@Composable
private fun LineDrawLayout(
    modifier: Modifier = Modifier,
    mainContent: @Composable () -> Unit,
    dependentContent: @Composable (List<Point>) -> Unit
) {

    SubcomposeLayout(modifier = modifier) { constraints ->
       // Implementation details
    }
}


enum class SlotsEnum { Main, Dependent }

and returns centers of boxes to dependent content. Then you can draw lines between these positions considering width of the text. You can do it with Text(onTextLayout) with line height if you wish to.

LineDrawLayout(
    mainContent = {
      // Your content is here
      NumberedBox(...)
      NumberedBox(...)
      ....
    },
    dependentContent = { points: List<Point> ->
      // This is overlay for drawing lines
      // Can be Canvas or Box with `Modifier.drawWithContent{}`
     
    }
)
Thracian
  • 43,021
  • 16
  • 133
  • 222
0

I hope this help. You need to modify it according to you. [this is not tested]

@Composable
    fun ConnectedComposable(modifier: Modifier) {
        val data = listOf(
            NumberedBoxData(1, false, true),
            NumberedBoxData(2, true, true),
            NumberedBoxData(3, true, false)
        )
    
        LazyColumn(modifier = modifier) {
            items(data.size) { index ->
                NumberedBox(data[index])
                if (index < data.size - 1) {
                    VerticalDivider(Modifier.height(64.dp))
                }
            }
        }
    }
    
    data class NumberedBoxData(
        val number: Int,
        val topLine: Boolean,
        val bottomLine: Boolean
    )
    
    @Composable
    fun VerticalDivider(
        modifier: Modifier = Modifier
    ) {
        Divider(
            modifier = modifier
                .width(2.dp)
                .align(Alignment.CenterHorizontally),
            color = Color.Black
        )
    }
    
    @Composable
    fun NumberedBox(data: NumberedBoxData) {
        Surface(
            color = Color.LightGray,
            border = BorderStroke(1.dp, Color.Black),
            modifier = Modifier
                .fillMaxWidth()
        ) {
            Column(
                modifier = Modifier
                    .padding(horizontal = 16.dp)
                    .fillMaxSize()
            ) {
                if (data.topLine) {
                    VerticalDivider(Modifier.fillMaxWidth())
                }
    
                Text(
                    data.number.toString(),
                    modifier = Modifier
                        .align(Alignment.CenterHorizontally)
                        .padding(vertical = 16.dp),
                    textAlign = TextAlign.Center
                )
    
                if (data.bottomLine) {
                    VerticalDivider(Modifier.fillMaxWidth())
                }
            }
        }
    }
    
    @Preview
    @Composable
    fun ConnectedComposable_Preview() {
        Scaffold {
            ConnectedComposable(
                Modifier
                    .padding(it)
                    .padding(16.dp)
            )
        }
    }
Deepak Ror
  • 2,084
  • 2
  • 20
  • 26