Skip to content

Commit

Permalink
Update README
Browse files Browse the repository at this point in the history
  • Loading branch information
romainguy committed Nov 17, 2022
1 parent 60bed1b commit a9e56a9
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 10 deletions.
19 changes: 19 additions & 0 deletions .github/workflows/continuous-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Android

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

workflow_dispatch:

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2

- name: Build library
run: ./gradlew assembleRelease
117 changes: 116 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,123 @@
# combo-breaker
Text layout for Compose to flow text around arbitrary shapes.

[![combo-breaker](https://maven-badges.herokuapp.com/maven-central/dev.romainguy/combo-breaker/badge.svg?subject=combo-breaker)](https://maven-badges.herokuapp.com/maven-central/dev.romainguy/combo-breaker)
[![Android build status](https://github.com/romainguy/combo-breaker/workflows/Android/badge.svg)](https://github.com/romainguy/combo-breaker/actions?query=workflow%3AAndroid)

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:

```kotlin
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:

![Flow around rectangular shapes](art/screenshot_default_shapes.png)

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

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:

```kotlin
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(-bitmap1.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:

![Flow around non-rectangular shapes](art/screenshot_arbitrary_shapes.png)

Combo Breaker is compatible with API 29+.

## Maven

```gradle
repositories {
// ...
mavenCentral()
}
dependencies {
implementation 'dev.romainguy:combo-breaker:0.1.0'
}
```

## Limitations and planned work

- Backport to earlier API levels.
- Paths with multiple contours are treated as a single shape. A future feature will allow such
paths to be treated as multiple shapes.
- Only one `TextStyle` is supported. Support for multiple text styles (at least one per paragraph)
is planned.
- 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).
- Support flowing text inside shapes.

## License

Please see [LICENSE](./LICENSE).

## Attribution

The render of the microphone was made possible thanks to
[RCA 44-BX Microphone](https://skfb.ly/6AKHx) by Tom Seddon, licensed under
[Creative Commons Attribution](http://creativecommons.org/licenses/by/4.0/).

Sample text taken from the [Wikipedia Hyphen article](https://en.wikipedia.org/wiki/Hyphen).
Binary file added art/screenshot_arbitrary_shapes.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added art/screenshot_default_shapes.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,13 @@ import dev.romainguy.text.combobreaker.demo.ui.theme.ComboBreakerTheme
import dev.romainguy.text.combobreaker.toContour

//region Sample text
const val SampleText = """The correctness and coherence of the lighting environment is paramount to achieving plausible visuals. After surveying existing rendering engines (such as Unity or Unreal Engine 4) as well as the traditional real-time rendering literature, it is obvious that coherency is rarely achieved.
const val SampleText = """The English language does not have definitive hyphenation rules, though various style guides provide detailed usage recommendations and have a significant amount of overlap in what they advise. Hyphens are mostly used to break single words into parts or to join ordinarily separate words into single words. Spaces are not placed between a hyphen and either of the elements it connects except when using a suspended or "hanging" hyphen that stands in for a repeated word (e.g., nineteenth- and twentieth-century writers). Style conventions that apply to hyphens (and dashes) have evolved to support ease of reading in complex constructions; editors often accept deviations if they aid rather than hinder easy comprehension.
The Unreal Engine, for instance, lets artists specify the “brightness” of a point light in lumen, a unit of luminous power. The brightness of directional lights is however expressed using an arbitrary unnamed unit. To match the brightness of a point light with a luminous power of 5,000 lumen, the artist must use a directional light of brightness 10. This kind of mismatch makes it difficult for artists to maintain the visual integrity of a scene when adding, removing or modifying lights. Using solely arbitrary units is a coherent solution but it makes reusing lighting rigs a difficult task. For instance, an outdoor scene will use a directional light of brightness 10 as the sun and all other lights will be defined relative to that value. Moving these lights to an indoor environment would make them too bright.
The use of the hyphen in English compound nouns and verbs has, in general, been steadily declining. Compounds that might once have been hyphenated are increasingly left with spaces or are combined into one word. Reflecting this changing usage, in 2007, the sixth edition of the Shorter Oxford English Dictionary removed the hyphens from 16,000 entries, such as fig-leaf (now fig leaf), pot-belly (now pot belly), and pigeon-hole (now pigeonhole). The increasing prevalence of computer technology and the advent of the Internet have given rise to a subset of common nouns that might have been hyphenated in the past (e.g., toolbar, hyperlink, and pastebin).
Our goal is therefore to make all lighting correct by default, while giving artists enough freedom to achieve the desired look. We will support a number of lights, split in two categories, direct and indirect lighting:
Despite decreased use, hyphenation remains the norm in certain compound-modifier constructions and, among some authors, with certain prefixes (see below). Hyphenation is also routinely used as part of syllabification in justified texts to avoid unsightly spacing (especially in columns with narrow line lengths, as when used with newspapers).
Deferred rendering is used by many modern 3D rendering engines to easily support dozens, hundreds or even thousands of light source (amongst other benefits). This method is unfortunately very expensive in terms of bandwidth. With our default PBR material model, our G-buffer would use between 160 and 192 bits per pixel, which would translate directly to rather high bandwidth requirements. Forward rendering methods on the other hand have historically been bad at handling multiple lights. A common implementation is to render the scene multiple times, once per visible light, and to blend (add) the results. Another technique consists in assigning a fixed maximum of lights to each object in the scene. This is however impractical when objects occupy a vast amount of space in the world (building, road, etc.).
Tiled shading can be applied to both forward and deferred rendering methods."""
When flowing text, it is sometimes preferable to break a word into two so that it continues on another line rather than moving the entire word to the next line. The word may be divided at the nearest break point between syllables (syllabification) and a hyphen inserted to indicate that the letters form a word fragment, rather than a full word. This allows more efficient use of paper, allows flush appearance of right-side margins (justification) without oddly large word spaces, and decreases the problem of rivers. This kind of hyphenation is most useful when the width of the column (called the "line length" in typography) is very narrow."""
//endregion

class ComboBreakerActivity : ComponentActivity() {
Expand All @@ -91,7 +89,8 @@ class ComboBreakerActivity : ComponentActivity() {
@Composable
private fun Page() {
val bitmap1 = remember {
BitmapFactory.decodeResource(resources, R.drawable.microphone).let {
BitmapFactory.decodeResource(resources, R.drawable.microphone)
.let {
Bitmap.createScaledBitmap(it, it.width / 2, it.height / 2, true)
}
}
Expand Down Expand Up @@ -135,15 +134,15 @@ class ComboBreakerActivity : ComponentActivity() {
contentDescription = "",
modifier = Modifier
.offset { Offset(-bitmap1.width / 4.5f, 0.0f).round() }
.flowShape(FlowType.OutsideEnd, 10.dp, shape1)
.flowShape(FlowType.OutsideEnd, 6.dp, shape1)
)

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

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ fun Bitmap.toContour(
val w = width
val h = height

if (!hasAlpha()) {
return Path().apply {
addRect(0.0f, 0.0f, w.toFloat(), h.toFloat(), Path.Direction.CW)
}
}

val pixels = IntArray(w * h)
getPixels(pixels, 0, w, 0, 0, w, h)

Expand Down

0 comments on commit a9e56a9

Please sign in to comment.