• Stars
    star
    577
  • Rank 77,363 (Top 2 %)
  • Language
    Kotlin
  • License
    Apache License 2.0
  • Created about 2 years ago
  • Updated about 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Text layout for Compose to flow text around arbitrary shapes.

Combo Breaker

combo-breaker combo-breaker-material3 Android build status

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.

Features

  • Multi-column layout
  • Styled strings (AnnotatedString)
  • Default rectangular shapes
  • Arbitrary shapes (any Path)
  • Justification
  • Hyphenation
  • Compatible with API 29+

Design Systems

Combo Breaker provides two levels of APIs depending on what design system you use:

  • BasicTextFlow from the dev.romainguy:combo-breaker artifact, which works with any design system
  • TextFlow from the dev.romainguy:combo-breaker-material3 artifact, which works with Material3

Choose BasicTextFlow if you do not have or do not want a dependency on androidx.compose.material3:material3.

Examples

The following code defines two images to flow text around:

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)
    )
}

Flow around rectangular shapes

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.toPath(alphaThreshold = 0.5f).asComposePath()
val badgeShape = badgeShape.toPath(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)
    )
}

The non-rectangular Path shape is created using the extension Bitmap.toPath from the pathway library. Using that API, a shape can be extracted from a bitmap and used as the flow shape for the desired child:

Flow around non-rectangular shapes

TextFlow supports multiple text styles and lets you control justification and hyphenation. In the example below, both justification and hyphenation are enabled:

Justification and hyphenation

You can also specify multiple shapes for any given element by using the flowShapes modifiers instead of flowShape. flowShapes accepts/returns list of paths instead of a single path. For instance, with pathway you can easily extract a list of paths from a Bitmap by using Bitmap.toPaths() instead of Bitmap.toPath().

val heartsShapes = heartsBitmap.toPaths().map { it.asComposePath() }

TextFlow(
    SampleText,
    style = TextStyle(fontSize = 12.sp),
    columns = 2
) {
    Image(
        bitmap = heartsBitmap.asImageBitmap(),
        contentDescription = "",
        modifier = Modifier
            .align(Alignment.Center)
            .flowShapes(FlowType.Outside, 4.dp, heartsShapes)
    )
}

This creates many shapes around which the text can flow:

Multiple shapes per element

Maven

repositories {
    // ...
    mavenCentral()
}

dependencies {
    // Use this library and BasicTextFlow() if you don't want a dependency on material3
    implementation 'dev.romainguy:combo-breaker:0.8.0'

    // Use this library and TextFlow() if you use material3
    implementation 'dev.romainguy:combo-breaker-material3:0.8.0'
}

Roadmap

  • Backport to earlier API levels.
  • Lines containing styles of different line heights can lead to improper flow around certain shapes.
  • More comprehensive TextFlowLayoutResult.
  • 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.
  • 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.