From ff1311ebbc41c430d7950030205fe18f466d2db4 Mon Sep 17 00:00:00 2001 From: Romain Guy Date: Mon, 28 Nov 2022 17:28:44 -0800 Subject: [PATCH] Remove unnecessary allocations during layout --- README.md | 5 +-- .../text/combobreaker/BasicTextFlow.kt | 10 ++--- .../romainguy/text/combobreaker/FlowSlots.kt | 43 ++++++++++++------- .../romainguy/text/combobreaker/TextLayout.kt | 7 ++- 4 files changed, 39 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index e6d5102..749f386 100644 --- a/README.md +++ b/README.md @@ -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' } ``` @@ -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). diff --git a/combo-breaker/src/main/java/dev/romainguy/text/combobreaker/BasicTextFlow.kt b/combo-breaker/src/main/java/dev/romainguy/text/combobreaker/BasicTextFlow.kt index d5264bc..e349641 100644 --- a/combo-breaker/src/main/java/dev/romainguy/text/combobreaker/BasicTextFlow.kt +++ b/combo-breaker/src/main/java/dev/romainguy/text/combobreaker/BasicTextFlow.kt @@ -474,7 +474,7 @@ fun BasicTextFlow( column, container, state.shapes, - results + results = results ) slots.addAll(columnSlots) column.offset(columnWidth + spacing, 0.0f) @@ -488,11 +488,11 @@ fun BasicTextFlow( } } } - .pointerInput(Unit) { - detectDragGestures { change, _ -> - debugLinePosition = change.position.y - } + .pointerInput(Unit) { + detectDragGestures { change, _ -> + debugLinePosition = change.position.y } + } } ) } diff --git a/combo-breaker/src/main/java/dev/romainguy/text/combobreaker/FlowSlots.kt b/combo-breaker/src/main/java/dev/romainguy/text/combobreaker/FlowSlots.kt index baaef5a..bb0e989 100644 --- a/combo-breaker/src/main/java/dev/romainguy/text/combobreaker/FlowSlots.kt +++ b/combo-breaker/src/main/java/dev/romainguy/text/combobreaker/FlowSlots.kt @@ -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 = ArrayList() + + val intervals: ArrayList> = ArrayList() + val flowShapeHits: ArrayList = 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 @@ -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. @@ -41,26 +57,21 @@ internal fun findFlowSlots( box: RectF, container: RectF, flowShapes: ArrayList, + state: FlowSlotFinderState = FlowSlotFinderState(), results: MutableList>? = null ): List { - // TODO: Clean up all most of the allocations we do here - - // List of all the slots we found - val slots = mutableListOf() - - // List of all the intervals we must consider for a given shape - val intervals = mutableListOf>() - - // Temporary variable used to avoid allocations - val p1 = PointF() - val p2 = PointF() - val scratch = PointF() - - val flowShapeHits = mutableListOf() - var foundIntervals = false val searchInterval = Interval(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] @@ -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)) } diff --git a/combo-breaker/src/main/java/dev/romainguy/text/combobreaker/TextLayout.kt b/combo-breaker/src/main/java/dev/romainguy/text/combobreaker/TextLayout.kt index 6cf75dd..e88a65e 100644 --- a/combo-breaker/src/main/java/dev/romainguy/text/combobreaker/TextLayout.kt +++ b/combo-breaker/src/main/java/dev/romainguy/text/combobreaker/TextLayout.kt @@ -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 @@ -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 @@ -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 }