Text layout for Compose to flow text around arbitrary shapes

Combo Breaker

Composable widget for Jetpack Compose that allows to flow text around arbitrary shapes over multiple columns. The TextFlow composable behaves as a Box layout and will automatically flow the text content around its children.

For instance, the following code:

TextFlow(
    SampleText,
    style = TextStyle(fontSize = 14.sp),
    columns = 2
) {
    Image(
        bitmap = letterT.asImageBitmap(),
        contentDescription = "",
        modifier = Modifier
            .flowShape(FlowType.OutsideEnd)
    )

    Image(
        bitmap = badgeBitmap.asImageBitmap(),
        contentDescription = "",
        modifier = Modifier
            .align(Alignment.Center)
            .flowShape(margin = 6.dp)
    )
}

will produce this result:

As you can see, any child of TextFlow allows text to flow around a rectangular shape of the same dimensions of the child. The flowShape modifier is used to control where text flows around the shape (to the right/end of the T) and around both the left and right sides of the landscape photo (default behavior). In addition, you can define a margin around the shape.

The flowShape modifier also lets you specify a specific shape instead of a default rectangle. This can be done by passing a Path or a lambda that returns a Path. The lambda alternative is useful when you need to create a Path based on the dimensions of the TextFlow or the dimensions of its child.

Here is an example of a TextFlow using non-rectangular shapes:

val microphoneShape = microphoneBitmap.toContour(alphaThreshold = 0.5f).asComposePath()
val badgeShape = badgeShape.toContour(alphaThreshold = 0.5f).asComposePath()

TextFlow(
    SampleText,
    style = TextStyle(fontSize = 14.sp),
    columns = 2
) {
    Image(
        bitmap = microphoneBitmap.asImageBitmap(),
        contentDescription = "",
        modifier = Modifier
            .offset { Offset(-microphoneBitmap.width / 4.5f, 0.0f).round() }
            .flowShape(FlowType.OutsideEnd, 6.dp, microphoneShape)
    )

    Image(
        bitmap = badgeBitmap.asImageBitmap(),
        contentDescription = "",
        modifier = Modifier
            .align(Alignment.Center)
            .flowShape(FlowType.Outside, 6.dp, badgeShape)
    )
}

Using the extension Bitmap.toContour provided by this library, a shape can be extracted from a bitmap and used as the flow shape for the desired child:

Combo Breaker is compatible with API 29+.

Maven

repositories {
    // ...
    mavenCentral()
}

dependencies {
    implementation 'dev.romainguy:combo-breaker:0.2.0'
}

Limitations and planned work

  • Backport to earlier API levels.
  • Optimizations!
  • More comprehensive TextFlowLayoutResult.
  • Paths with multiple contours are treated as a single shape. A future feature will allow such paths to be treated as multiple shapes.
  • Add support to ellipsize the last line when the entire text cannot fit in the layout area.
  • Add support for text-relative placement of flow shapes.
  • Implement margins support without relying on Path.op which can be excessively expensive with complex paths.
  • Reduce allocations during the layout phase.
  • BiDi text hasn’t been tested yet, and probably doesn’t work properly (RTL layouts are however supported for the placement of flow shapes and the handling of columns).
  • Improve performance of contours extraction from an image (could be multi-threaded for instance).
  • Investigate an alternative and simpler way to handle placement around shapes (beam cast instead of the purely geometric approach that currently requires a lot of intersection work).
  • Support flowing text inside shapes.

License

Please see LICENSE.

Attribution

The render of the microphone was made possible thanks to RCA 44-BX Microphone by Tom Seddon, licensed under Creative Commons Attribution.

Sample text taken from the Wikipedia Hyphen article.

GitHub

View Github