perf: use multi-content Layout to eliminate all wrapper nodes
Replace Box/Column wrappers with the multi-content Layout overload (contents: List) which makes each slot's composables direct measurables. This removes 6 intermediate layout nodes per note item. Multi-child slots (noteContent, reactionsRow) are now measured and stacked inline using arrayOfNulls<Placeable> with zero-allocation iteration, avoiding Column's internal measure pass entirely. https://claude.ai/code/session_01Fam5VHNaNkKBBafg1gRfFS
This commit is contained in:
@@ -20,14 +20,12 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.layouts
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.layout.Placeable
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -47,8 +45,10 @@ private data class NoteLayoutPx(
|
||||
)
|
||||
|
||||
/**
|
||||
* Custom single-pass layout for note items that replaces nested Row/Column
|
||||
* measurements with direct constraint calculation using known fixed dimensions.
|
||||
* Custom zero-overhead layout for note items. Uses the multi-content [Layout]
|
||||
* overload to eliminate ALL intermediate wrapper nodes (Box/Column). Each slot's
|
||||
* composables become direct measurables measured with pre-computed constraints
|
||||
* in a single pass.
|
||||
*
|
||||
* Layout structure (when [showAuthorColumn] is true):
|
||||
* ```
|
||||
@@ -67,11 +67,12 @@ private data class NoteLayoutPx(
|
||||
* └───────────────────────────────────────────────┘
|
||||
* ```
|
||||
*
|
||||
* Performance benefits over nested Row/Column:
|
||||
* - Eliminates Row's two-pass measurement (measure author, then content with remaining width)
|
||||
* - Uses pre-computed pixel dimensions for author column width (55dp) and gaps
|
||||
* - Reduces content Column children from ~6 to ~3
|
||||
* - Single measure pass positions all sections with known constraints
|
||||
* Performance characteristics:
|
||||
* - Zero intermediate layout nodes (no Box/Column wrappers)
|
||||
* - Pre-computed pixel dimensions cached across recompositions
|
||||
* - Single measure pass with known constraints for all 6 slots
|
||||
* - Multi-child slots (noteContent, reactionsRow) stacked inline
|
||||
* - Hidden slots skip measurement entirely (empty measurable lists)
|
||||
*
|
||||
* @param addPadding Whether to add standard note padding (12dp sides, 10dp top)
|
||||
* @param showAuthorColumn Whether to reserve space for the author picture column
|
||||
@@ -109,22 +110,17 @@ fun NoteComposeLayout(
|
||||
}
|
||||
|
||||
Layout(
|
||||
content = {
|
||||
// Slot 0: Author picture (55x55 area)
|
||||
Box { authorPicture() }
|
||||
// Slot 1: Relay badges (55dp wide, below author)
|
||||
Box { relayBadges() }
|
||||
// Slot 2: First row (author name, time, options)
|
||||
Box { firstRow() }
|
||||
// Slot 3: Second row (NIP05, location, PoW)
|
||||
Box { secondRow() }
|
||||
// Slot 4: Note content (event-specific + zap splits + approval)
|
||||
Column(Modifier.fillMaxWidth()) { noteContent() }
|
||||
// Slot 5: Reactions row (full width)
|
||||
Column(Modifier.fillMaxWidth()) { reactionsRow() }
|
||||
},
|
||||
contents =
|
||||
listOf(
|
||||
authorPicture,
|
||||
relayBadges,
|
||||
firstRow,
|
||||
secondRow,
|
||||
noteContent,
|
||||
reactionsRow,
|
||||
),
|
||||
modifier = modifier,
|
||||
) { measurables, constraints ->
|
||||
) { allMeasurables, constraints ->
|
||||
val maxWidth = constraints.maxWidth
|
||||
|
||||
// Compute padding offsets
|
||||
@@ -138,70 +134,94 @@ fun NoteComposeLayout(
|
||||
val gap = if (showAuthorColumn) px.authorContentGap else 0
|
||||
val contentWidth = (innerWidth - authorW - gap).coerceAtLeast(0)
|
||||
|
||||
// Build constraints for each section
|
||||
val authorConstraints =
|
||||
if (showAuthorColumn) Constraints(maxWidth = px.authorWidth) else ZeroConstraints
|
||||
// Pre-compute constraint objects once
|
||||
val authorConstraints = Constraints(maxWidth = px.authorWidth)
|
||||
val contentConstraints = Constraints(maxWidth = contentWidth)
|
||||
val fullWidthConstraints = Constraints(maxWidth = maxWidth)
|
||||
|
||||
// Measure all children in a single pass with pre-computed constraints
|
||||
val authorPlaceable = measurables[0].measure(authorConstraints)
|
||||
val relayPlaceable = measurables[1].measure(authorConstraints)
|
||||
val firstRowPlaceable = measurables[2].measure(contentConstraints)
|
||||
// Measure author column (single child per slot, skip if empty)
|
||||
val authorPlaceable = allMeasurables[0].firstOrNull()?.measure(authorConstraints)
|
||||
val relayPlaceable = allMeasurables[1].firstOrNull()?.measure(authorConstraints)
|
||||
|
||||
// Measure header rows
|
||||
val firstRowPlaceable = allMeasurables[2].firstOrNull()?.measure(contentConstraints)
|
||||
val secondRowPlaceable =
|
||||
measurables[3].measure(
|
||||
if (showSecondRow) contentConstraints else ZeroConstraints,
|
||||
)
|
||||
val contentPlaceable = measurables[4].measure(contentConstraints)
|
||||
val reactionsPlaceable =
|
||||
measurables[5].measure(Constraints(maxWidth = maxWidth))
|
||||
if (showSecondRow) allMeasurables[3].firstOrNull()?.measure(contentConstraints) else null
|
||||
|
||||
// Measure note content children directly (no Column wrapper)
|
||||
val contentMeasurables = allMeasurables[4]
|
||||
val contentPlaceables = arrayOfNulls<Placeable>(contentMeasurables.size)
|
||||
var contentStackHeight = 0
|
||||
for (i in contentMeasurables.indices) {
|
||||
val placeable = contentMeasurables[i].measure(contentConstraints)
|
||||
contentPlaceables[i] = placeable
|
||||
contentStackHeight += placeable.height
|
||||
}
|
||||
|
||||
// Measure reactions row children directly (no Column wrapper)
|
||||
val reactionsMeasurables = allMeasurables[5]
|
||||
val reactionsPlaceables = arrayOfNulls<Placeable>(reactionsMeasurables.size)
|
||||
var reactionsHeight = 0
|
||||
for (i in reactionsMeasurables.indices) {
|
||||
val placeable = reactionsMeasurables[i].measure(fullWidthConstraints)
|
||||
reactionsPlaceables[i] = placeable
|
||||
reactionsHeight += placeable.height
|
||||
}
|
||||
|
||||
// Calculate section heights
|
||||
val spacer = if (showContentSpacer) px.contentSpacer else 0
|
||||
|
||||
val contentColumnHeight =
|
||||
firstRowPlaceable.height +
|
||||
(if (showSecondRow) secondRowPlaceable.height else 0) +
|
||||
(firstRowPlaceable?.height ?: 0) +
|
||||
(secondRowPlaceable?.height ?: 0) +
|
||||
spacer +
|
||||
contentPlaceable.height
|
||||
contentStackHeight
|
||||
|
||||
val authorColumnHeight =
|
||||
if (showAuthorColumn) {
|
||||
authorPlaceable.height + px.authorBadgeGap + relayPlaceable.height
|
||||
if (showAuthorColumn && authorPlaceable != null) {
|
||||
authorPlaceable.height + px.authorBadgeGap + (relayPlaceable?.height ?: 0)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
val mainAreaHeight = maxOf(authorColumnHeight, contentColumnHeight)
|
||||
val totalHeight = padTop + mainAreaHeight + reactionsPlaceable.height
|
||||
val totalHeight = padTop + mainAreaHeight + reactionsHeight
|
||||
|
||||
layout(maxWidth, totalHeight.coerceAtLeast(constraints.minHeight)) {
|
||||
// Place author column (inside padding)
|
||||
// Place author column
|
||||
if (showAuthorColumn) {
|
||||
authorPlaceable.placeRelative(padStart, padTop)
|
||||
relayPlaceable.placeRelative(
|
||||
authorPlaceable?.placeRelative(padStart, padTop)
|
||||
relayPlaceable?.placeRelative(
|
||||
padStart,
|
||||
padTop + authorPlaceable.height + px.authorBadgeGap,
|
||||
padTop + (authorPlaceable?.height ?: 0) + px.authorBadgeGap,
|
||||
)
|
||||
}
|
||||
|
||||
// Place content column (to the right of author, inside padding)
|
||||
// Place content column
|
||||
val contentX = padStart + authorW + gap
|
||||
var y = padTop
|
||||
|
||||
firstRowPlaceable.placeRelative(contentX, y)
|
||||
y += firstRowPlaceable.height
|
||||
firstRowPlaceable?.placeRelative(contentX, y)
|
||||
y += firstRowPlaceable?.height ?: 0
|
||||
|
||||
if (showSecondRow) {
|
||||
secondRowPlaceable.placeRelative(contentX, y)
|
||||
y += secondRowPlaceable.height
|
||||
secondRowPlaceable?.placeRelative(contentX, y)
|
||||
y += secondRowPlaceable?.height ?: 0
|
||||
}
|
||||
|
||||
y += spacer
|
||||
|
||||
contentPlaceable.placeRelative(contentX, y)
|
||||
for (placeable in contentPlaceables) {
|
||||
placeable?.placeRelative(contentX, y)
|
||||
y += placeable?.height ?: 0
|
||||
}
|
||||
|
||||
// Place reactions row at full width, below main area
|
||||
reactionsPlaceable.placeRelative(0, padTop + mainAreaHeight)
|
||||
var reactionsY = padTop + mainAreaHeight
|
||||
for (placeable in reactionsPlaceables) {
|
||||
placeable?.placeRelative(0, reactionsY)
|
||||
reactionsY += placeable?.height ?: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user