Skip to content

Commit

Permalink
Remove unnecessary allocations during layout
Browse files Browse the repository at this point in the history
  • Loading branch information
romainguy committed Nov 29, 2022
1 parent 169f8b8 commit ff1311e
Show file tree
Hide file tree
Showing 4 changed files with 39 additions and 26 deletions.
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,10 @@ repositories {
dependencies {
// Use this library and BasicTextFlow() if you don't want a dependency on material3
implementation 'dev.romainguy:combo-breaker:0.4.0'
implementation 'dev.romainguy:combo-breaker:0.5.0'
// Use this library and TextFlow() if you use material3
implementation 'dev.romainguy:combo-breaker-material3:0.4.0'
implementation 'dev.romainguy:combo-breaker-material3:0.5.0'
}
```

Expand All @@ -144,7 +144,6 @@ dependencies {
- 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).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ fun BasicTextFlow(
column,
container,
state.shapes,
results
results = results
)
slots.addAll(columnSlots)
column.offset(columnWidth + spacing, 0.0f)
Expand All @@ -488,11 +488,11 @@ fun BasicTextFlow(
}
}
}
.pointerInput(Unit) {
detectDragGestures { change, _ ->
debugLinePosition = change.position.y
}
.pointerInput(Unit) {
detectDragGestures { change, _ ->
debugLinePosition = change.position.y
}
}
}
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,21 @@ import android.graphics.RectF
import kotlin.math.max
import kotlin.math.min

/**
* Holder for pre-allocated structures that will be used when findFlowSlots() is called
* repeatedly.
*/
internal class FlowSlotFinderState {
val slots: ArrayList<RectF> = ArrayList()

val intervals: ArrayList<Interval<PathSegment>> = ArrayList()
val flowShapeHits: ArrayList<FlowShape> = ArrayList()

val p1: PointF = PointF()
val p2: PointF = PointF()
val scratch: PointF = PointF()
}

/**
* Given a layout area [box], finds all the slots (rectangles) that can be used to layout
* content around a series of given flow shapes. In our case [box] will be a line of text
Expand All @@ -33,6 +48,7 @@ import kotlin.math.min
* @param container Bounds of the [box] container, which will typically match [box] exactly.
* unless text is laid out over multiple columns/shapes.
* @param flowShapes List of shapes that content must flow around.
* @param state Optional [FlowSlotFinderState] structure to avoid allocations across invocations.
* @param results Optional for debug only: holds the list of [Interval] used to find slots.
*
* @return A list of rectangles indicating where content can be laid out.
Expand All @@ -41,26 +57,21 @@ internal fun findFlowSlots(
box: RectF,
container: RectF,
flowShapes: ArrayList<FlowShape>,
state: FlowSlotFinderState = FlowSlotFinderState(),
results: MutableList<Interval<PathSegment>>? = null
): List<RectF> {
// TODO: Clean up all most of the allocations we do here

// List of all the slots we found
val slots = mutableListOf<RectF>()

// List of all the intervals we must consider for a given shape
val intervals = mutableListOf<Interval<PathSegment>>()

// Temporary variable used to avoid allocations
val p1 = PointF()
val p2 = PointF()
val scratch = PointF()

val flowShapeHits = mutableListOf<FlowShape>()

var foundIntervals = false
val searchInterval = Interval<PathSegment>(box.top, box.bottom)

val slots = state.slots
val intervals = state.intervals
val flowShapeHits = state.flowShapeHits
val p1 = state.p1
val p2 = state.p2

slots.clear()
flowShapeHits.clear()

val flowShapeCount = flowShapes.size
for (i in 0 until flowShapeCount) {
val flowShape = flowShapes[i]
Expand Down Expand Up @@ -96,7 +107,7 @@ internal fun findFlowSlots(
p1.set(segment.x0, segment.y0)
p2.set(segment.x1, segment.y1)

if (clipSegment(p1, p2, container, scratch)) {
if (clipSegment(p1, p2, container, state.scratch)) {
shapeMin = min(shapeMin, min(p1.x, p2.x))
shapeMax = max(shapeMax, max(p1.x, p2.x))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ internal fun layoutTextFlow(
flowState.density
)

val slotFinderState = FlowSlotFinderState()

for (c in 0 until columnCount) {
// Cursor to indicate where to draw the next line of text
var y = 0.0f
Expand Down Expand Up @@ -154,7 +156,8 @@ internal fun layoutTextFlow(
val slots = findFlowSlots(
RectF(column.left, y, column.right, y + lineHeight),
RectF(0.0f, y, size.width.toFloat(), y + lineHeight),
flowState.shapes
flowState.shapes,
slotFinderState
)

var ascent = 0.0f
Expand Down Expand Up @@ -208,7 +211,7 @@ internal fun layoutTextFlow(
val lineDescent = result.getLineDescent(state.lastParagraphLine)

// Don't enqueue a new line if we'd lay it out out of bounds
if (y > column.height() || (y + lineAscent + lineDescent) > column.height()) {
if (y > column.height() || (y + lineAscent + lineDescent) > column.height()) {
break
}

Expand Down

0 comments on commit ff1311e

Please sign in to comment.