1

I am following the document of Intrinsics. I want to wrap the canvas view. I want to use canvas into multiple times because I want to make timeline view. So drawing in the canvas is everything works fine.

@OptIn(ExperimentalTextApi::class)
@Preview(showBackground = true)
@Composable
fun CanvasView() {
    val titleTextMeasurer = rememberTextMeasurer()
    val titleText = "Hello world!!"
    val listOfPair = listOf(
        Pair("Text 1", "app"),
        Pair("Text 2", "link"),
        Pair("Text 3", "app"),
        Pair("Text 4", "link"),
        Pair("Text 5", ""),
    )
    val multipleString = buildAnnotatedString {
        listOfPair.forEachIndexed { _, item ->
            val (text, type) = item
            when (type) {
                "app" -> {
                    withStyle(style = SpanStyle(color = Color.DarkGray)) {
                        append("$text\n")
                    }
                }

                "link" -> {
                    withStyle(style = SpanStyle(color = Color.Blue)) {
                        append("$text\n")
                    }
                }

                else -> {
                    append("$text\n")
                }
            }
        }
    }
    val titleTextLayoutResult = remember {
        titleTextMeasurer.measure(titleText, TextStyle(fontSize = 16.sp))
    }
    val multipleTextLayoutResult = remember {
        titleTextMeasurer.measure(multipleString, TextStyle(fontSize = 14.sp, lineHeight = 1.3.em))
    }
    val endPadding = with(LocalDensity.current) { 10.dp.toPx() }
    val circleSize = with(LocalDensity.current) { 6.dp.toPx() }
    CanvasContent(
        titleTextLayoutResult,
        multipleTextLayoutResult,
        circleSize,
        endPadding,
    )
}

@OptIn(ExperimentalTextApi::class)
@Composable
fun CanvasContent(
    titleTextLayoutResult: TextLayoutResult,
    multipleTextLayoutResult: TextLayoutResult,
    circleSize: Float,
    endPadding: Float,
) {
    Canvas(modifier = Modifier.fillMaxSize()) {
        val circleX = (center.x / 2) + (titleTextLayoutResult.size.width / 2) - endPadding
        drawText(
            textLayoutResult = titleTextLayoutResult,
            topLeft = Offset(
                x = center.x - titleTextLayoutResult.size.width / 2,
                y = center.y - titleTextLayoutResult.size.height / 2,
            )
        )
        drawText(
            textLayoutResult = multipleTextLayoutResult,
            topLeft = Offset(
                x = center.x - titleTextLayoutResult.size.width / 2,
                y = center.y + titleTextLayoutResult.size.height / 2,
            )
        )
        drawLine(
            color = Color.Black,
            start = Offset(x = circleX, y = 0F),
            end = Offset(x = circleX, y = center.y - circleSize),
            strokeWidth = 2.dp.toPx(),
        )
        drawLine(
            color = Color.Black,
            start = Offset(x = circleX, y = center.y + circleSize),
            end = Offset(x = circleX, y = size.height),
            strokeWidth = 2.dp.toPx(),
        )
        drawCircle(
            color = Color.Gray,
            radius = circleSize,
            center = Offset(circleX, center.y)
        )
    }
}

The view looks full screen like this

enter image description here

Now I want to use this view in multiple times. I tried the parent item to height(IntrinsicSize.Min) and inside Canvas(modifier = Modifier.fillMaxSize()) it not warping the view i.e. view is not visible any more. Code looks like this

Column(
        Modifier
            .fillMaxWidth()
            .height(IntrinsicSize.Min)
    ) {
        CanvasContent(
            titleTextLayoutResult,
            multipleTextLayoutResult,
            circleSize,
            endPadding,
        )
    }

So view is not visible any more. So what is the wrong in here?

My main goal is to create the view like below image.

enter image description here

Thanks

Kotlin Learner
  • 3,995
  • 6
  • 47
  • 127

1 Answers1

0

You need to do this inside a loop for each draw element by increasing height with an index

// This is for demonstration
val offset = 50.dp.toPx()

for(i in 0..2) {


  drawText(
            textLayoutResult = titleTextLayoutResult,
            topLeft = Offset(
                x = center.x - titleTextLayoutResult.size.width / 2,
                y = center.y - titleTextLayoutResult.size.height / 2 + i * offset,
            )
        )
}
   // Rest of the drawing on each iteration
}

You can't use IntrinsicSize.Min with Canvas, you can't even use a non-fixed(Modifier.heighIn(min,max)) size if you wish to get a non-zero size from DrawScope because Canvas is a Spacer with Modifier.drawBehind{}. And as you can see in source code below it returns 0 width or height if you don't assign a fixed size modifier.

You can see in this answer which modifier returns which kind of Constraints with different kind of flags

@Composable
fun Canvas(modifier: Modifier, onDraw: DrawScope.() -> Unit) =
    Spacer(modifier.drawBehind(onDraw))

@Composable
@NonRestartableComposable
fun Spacer(modifier: Modifier) {
    Layout({}, measurePolicy = SpacerMeasurePolicy, modifier = modifier)
}

private object SpacerMeasurePolicy : MeasurePolicy {

    override fun MeasureScope.measure(
        measurables: List<Measurable>,
        constraints: Constraints
    ): MeasureResult {
        return with(constraints) {
            val width = if (hasFixedWidth) maxWidth else 0
            val height = if (hasFixedHeight) maxHeight else 0
            layout(width, height) {}
        }
    }
}

Even if you assign Modifier.draw to a Box or Column you won't be able to use intrinsic size because, intrinsic size is some kind of placeholder assigned to Composables in MeasurePolicy. So there has to be some Composables like Text or Image as child Composable to get intrinsic size from.

Let's say you have 3 Texts. To get max height of these Texts parent needs to get biggest of this intrinsic heights and then measure itself with max of the Texts.

For example this Layout assigns fixed numbers for demonstration

@Composable
fun CustomColumnWithIntrinsicDimensions(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    val measurePolicy = object : MeasurePolicy {

        override fun MeasureScope.measure(
            measurables: List<Measurable>,
            constraints: Constraints
        ): MeasureResult {

            val looseConstraints = constraints.copy(minHeight = 0)
            val placeables = measurables.map { measurable ->
                measurable.measure(looseConstraints)
            }

            var yPosition = 0

            val totalHeight: Int = placeables.sumOf {
                it.height
            }

            //  This can be sum or longest of Composable widths, or maxWidth of Constraints
            val maxWidth: Int = placeables.maxOf {
                it.width
            }

            return layout(maxWidth, totalHeight) {
                placeables.forEach { placeable ->
                    placeable.placeRelative(x = 0, y = yPosition)
                    yPosition += placeable.height
                }
            }
        }

        override fun IntrinsicMeasureScope.minIntrinsicHeight(
            measurables: List<IntrinsicMeasurable>,
            width: Int
        ): Int {

            println(" minIntrinsicHeight() width: $width, measurables: ${measurables.size}")
            //  This is just sample to show usage of minIntrinsicHeight, don't set
            // static values
            return 200
        }

        override fun IntrinsicMeasureScope.maxIntrinsicHeight(
            measurables: List<IntrinsicMeasurable>,
            width: Int
        ): Int {

            println(" maxIntrinsicHeight() width: $width, measurables: ${measurables.size}")

            //  This is just sample to show usage of maxIntrinsicHeight, don't set
            // static values
            return 400
        }
    }

    Layout(modifier = modifier, content = content, measurePolicy = measurePolicy)
}

You can see how size is set when you assign intrinsic sizes in demo below

enter image description here

Text(text = "No height Modifier")
CustomColumnWithIntrinsicDimensions(
    modifier = Modifier
        .width(100.dp)
        .background(Green400)
        .padding(4.dp)
) {
    Text(
        "First Text",
        modifier = Modifier
            .background(Color(0xffF44336)),
        color = Color.White
    )
    Text(
        "Second Text",
        modifier = Modifier
            .background(Color(0xff9C27B0)),
        color = Color.White
    )
}

Text(text = "height(IntrinsicSize.Min)")
CustomColumnWithIntrinsicDimensions(
    modifier = Modifier
        .width(100.dp)
        .height(IntrinsicSize.Min)
        .background(Yellow400)
        .padding(4.dp)
) {
    Text(
        "First Text",
        modifier = Modifier
            .background(Color(0xffF44336)),
        color = Color.White
    )
    Text(
        "Second Text",
        modifier = Modifier
            .background(Color(0xff9C27B0)),
        color = Color.White
    )
}

Text(text = "height(IntrinsicSize.Max)")
CustomColumnWithIntrinsicDimensions(
    modifier = Modifier
        .width(100.dp)
        .height(IntrinsicSize.Max)
        .background(Blue400)
        .padding(4.dp)
) {
    Text(
        "First Text",
        modifier = Modifier
            .background(Color(0xffF44336)),
        color = Color.White
    )
    Text(
        "Second Text",
        modifier = Modifier
            .background(Color(0xff9C27B0)),
        color = Color.White
    )
}
Thracian
  • 43,021
  • 16
  • 133
  • 222
  • thanks for explaining all in details. I still in confused what should be best solution to achieve my output. – Kotlin Learner May 02 '23 at 10:07
  • Use for loop and increase y position via index yOffset = someHeight * index – Thracian May 02 '23 at 10:08
  • My Canvas is taking full height so I want to wrap the height. I don't get it to be honest. Thanks – Kotlin Learner May 02 '23 at 10:08
  • I don't my view heights, that's my main problem.. – Kotlin Learner May 02 '23 at 10:09
  • If you look on `listOfPair` variable that is coming from the web server and I don't know how many items coming. So that's the problem to know accurate height for the view... – Kotlin Learner May 02 '23 at 10:10
  • That's what i explained, You can't use intrinsic size if it's not provided by child Composables. And to check what DrawScope size is you can observe `size` param. even if it's zero you can still draw any shape unless you use clip – Thracian May 02 '23 at 10:10
  • You can get item size and set height for canvas if that's the case – Thracian May 02 '23 at 10:11
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/253425/discussion-between-thracian-and-vivek-modi). – Thracian May 02 '23 at 10:11
  • I am accepting this answer because you teach me that Canvas will need height. We cannot use `IntrinsicSize.Min` in this situation. Thanks – Kotlin Learner May 02 '23 at 13:09