Skip to content

Commit

Permalink
Add support for multiple shapes per elements
Browse files Browse the repository at this point in the history
This change introduces the new flowShapes() modifiers to return a list
of Path. It also adds a new Path.divide() extension to create a list
of paths from a single source Path.

This change also fixes an issue with the ordering of text segments
when multiple shapes are on the same line of text.
  • Loading branch information
romainguy committed Nov 30, 2022
1 parent d91e572 commit 13d04a9
Show file tree
Hide file tree
Showing 9 changed files with 275 additions and 71 deletions.
30 changes: 28 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ flow the text content around its children.
- Justification
- Hyphenation
- Generate shapes from images
- Path division (extract multiple contours as a list of paths)
- Compatible with API 29+

## Design Systems
Expand Down Expand Up @@ -115,6 +116,33 @@ the example below, both justification and hyphenation are enabled:

![Justification and hyphenation](art/screenshot_styles_and_justification.png)

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.
This feature can be used in combination with the supporting API `Path.divide` which divides a
path's contours into a list of individual paths. For instance:

```kotlin
val heartsShapes = heartsBitmap.toContour().asComposePath().divide()

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

The division operation create many shapes around which the text can flow:

![Multiple shapes per element](art/screenshot_shapes.png)

## Maven

```gradle
Expand All @@ -137,8 +165,6 @@ dependencies {
- Backport to earlier API levels.
- Lines containing styles of different line heights can lead to improper flow around certain shapes.
- 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
Expand Down
Binary file added art/screenshot_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 @@ -65,6 +65,7 @@ import dev.romainguy.text.combobreaker.FlowType
import dev.romainguy.text.combobreaker.TextFlowHyphenation
import dev.romainguy.text.combobreaker.TextFlowJustification
import dev.romainguy.text.combobreaker.demo.ui.theme.ComboBreakerTheme
import dev.romainguy.text.combobreaker.divide
import dev.romainguy.text.combobreaker.material3.TextFlow
import dev.romainguy.text.combobreaker.toContour

Expand All @@ -83,10 +84,20 @@ class ComboBreakerActivity : ComponentActivity() {
private fun TextFlowDemo() {
val colorScheme = MaterialTheme.colorScheme

var columns by remember { mutableStateOf(2) }
var useMultipleShapes by remember { mutableStateOf(false) }
var useRectangleShapes by remember { mutableStateOf(true) }
var isJustified by remember { mutableStateOf(false) }
var isHyphenated by remember { mutableStateOf(true) }
var isDebugOverlayEnabled by remember { mutableStateOf(true) }

//region Sample text
val sampleText = remember {
val sampleText by remember { derivedStateOf {
buildAnnotatedString {
withStyle(SpanStyle(fontWeight = FontWeight.Bold, fontSize = 24.sp)) {
if (useMultipleShapes || !useRectangleShapes) {
append("T")
}
append("he Hyphen")
}
append("\n\n")
Expand Down Expand Up @@ -135,7 +146,7 @@ class ComboBreakerActivity : ComponentActivity() {
append("syllabification in justified texts to avoid unsightly spacing (especially ")
append("in columns with narrow line lengths, as when used with newspapers).")
}
}
} }
//endregion

val microphone = remember {
Expand All @@ -155,11 +166,16 @@ class ComboBreakerActivity : ComponentActivity() {
val letterT = remember { BitmapFactory.decodeResource(resources, R.drawable.letter_t) }
val landscape = remember { BitmapFactory.decodeResource(resources, R.drawable.landscape) }

var columns by remember { mutableStateOf(2) }
var useRectangleShapes by remember { mutableStateOf(true) }
var isJustified by remember { mutableStateOf(false) }
var isHyphenated by remember { mutableStateOf(true) }
var isDebugOverlayEnabled by remember { mutableStateOf(true) }
val hearts = remember { BitmapFactory.decodeResource(resources, R.drawable.hearts) }
val heartsShape by remember {
derivedStateOf {
if (useMultipleShapes) {
hearts.toContour().asComposePath().divide()
} else {
emptyList()
}
}
}

val justification by remember {
derivedStateOf {
Expand All @@ -184,45 +200,61 @@ class ComboBreakerActivity : ComponentActivity() {
TextFlow(
sampleText,
modifier = Modifier.weight(1.0f).fillMaxWidth(),
style = LocalTextStyle.current.merge(TextStyle(color = colorScheme.onSurface)),
style = LocalTextStyle.current.merge(
TextStyle(
color = colorScheme.onSurface,
fontSize = if (useMultipleShapes) 12.sp else LocalTextStyle.current.fontSize
)
),
justification = justification,
hyphenation = hyphenation,
columns = columns,
debugOverlay = isDebugOverlayEnabled
) {
Image(
bitmap = (if (useRectangleShapes) letterT else microphone).asImageBitmap(),
contentDescription = "",
modifier = Modifier
.offset {
if (useRectangleShapes)
IntOffset(0, 0)
else
Offset(-microphone.width / 4.5f, 0.0f).round()
}
.flowShape(
FlowType.OutsideEnd,
if (useRectangleShapes) 0.dp else 8.dp,
if (useRectangleShapes) null else microphoneShape
)
)
if (!useMultipleShapes) {
Image(
bitmap = (if (useRectangleShapes) letterT else microphone).asImageBitmap(),
contentDescription = "",
modifier = Modifier
.offset {
if (useRectangleShapes)
IntOffset(0, 0)
else
Offset(-microphone.width / 4.5f, 0.0f).round()
}
.flowShape(
FlowType.OutsideEnd,
if (useRectangleShapes) 0.dp else 8.dp,
if (useRectangleShapes) null else microphoneShape
)
)

Image(
bitmap = (if (useRectangleShapes) landscape else badge).asImageBitmap(),
contentDescription = "",
modifier = Modifier
.align(Alignment.Center)
.flowShape(
FlowType.Outside,
if (useRectangleShapes) 8.dp else 10.dp,
if (useRectangleShapes) null else badgeShape
)
)
Image(
bitmap = (if (useRectangleShapes) landscape else badge).asImageBitmap(),
contentDescription = "",
modifier = Modifier
.align(Alignment.Center)
.flowShape(
FlowType.Outside,
if (useRectangleShapes) 8.dp else 10.dp,
if (useRectangleShapes) null else badgeShape
)
)
} else {
Image(
bitmap = hearts.asImageBitmap(),
contentDescription = "",
modifier = Modifier
.align(Alignment.Center)
.flowShapes(FlowType.Outside, 2.dp, heartsShape)
)
}
}

DemoControls(
columns, { columns = it },
useRectangleShapes, { useRectangleShapes = it },
useMultipleShapes, { useMultipleShapes = it },
isJustified, { isJustified = it},
isHyphenated, { isHyphenated = it},
isDebugOverlayEnabled, { isDebugOverlayEnabled = it},
Expand All @@ -235,7 +267,9 @@ class ComboBreakerActivity : ComponentActivity() {
columns: Int,
onColumnsChanged: (Int) -> Unit,
useRectangleShapes: Boolean,
onRectangleShapesChanged: (Boolean) -> Unit,
onUseRectangleShapesChanged: (Boolean) -> Unit,
useMultipleShapes: Boolean,
onUseMultipleShapesChanged: (Boolean) -> Unit,
justify: Boolean,
onJustifyChanged: (Boolean) -> Unit,
hyphenation: Boolean,
Expand All @@ -254,21 +288,20 @@ class ComboBreakerActivity : ComponentActivity() {
)
Spacer(modifier = Modifier.width(8.dp))

Checkbox(checked = useRectangleShapes, onCheckedChange = onRectangleShapesChanged)
Text(text = "Rectangles")
Checkbox(checked = useRectangleShapes, onCheckedChange = onUseRectangleShapesChanged)
Text(text = "Rects")

Checkbox(checked = useMultipleShapes, onCheckedChange = onUseMultipleShapesChanged)
Text(text = "Multi")
}

Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = justify, onCheckedChange = onJustifyChanged)
Text(text = "Justify")

Spacer(modifier = Modifier.width(8.dp))

Checkbox(checked = hyphenation, onCheckedChange = onHyphenationChanged)
Text(text = "Hyphenate")

Spacer(modifier = Modifier.width(8.dp))

Checkbox(checked = debugOverlay, onCheckedChange = onDebugOverlayChanged)
Text(text = "Debug")
}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions combo-breaker/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ dependencies {
implementation platform("androidx.compose:compose-bom:$compose_bom")
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.material3:material3'

implementation 'dev.romainguy:pathway:0.8.0'
}

apply plugin: 'com.vanniktech.maven.publish'
Loading

0 comments on commit 13d04a9

Please sign in to comment.