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
|
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.Composable
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.layout.Layout
|
import androidx.compose.ui.layout.Layout
|
||||||
|
import androidx.compose.ui.layout.Placeable
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.unit.Constraints
|
import androidx.compose.ui.unit.Constraints
|
||||||
import androidx.compose.ui.unit.dp
|
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
|
* Custom zero-overhead layout for note items. Uses the multi-content [Layout]
|
||||||
* measurements with direct constraint calculation using known fixed dimensions.
|
* 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):
|
* Layout structure (when [showAuthorColumn] is true):
|
||||||
* ```
|
* ```
|
||||||
@@ -67,11 +67,12 @@ private data class NoteLayoutPx(
|
|||||||
* └───────────────────────────────────────────────┘
|
* └───────────────────────────────────────────────┘
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* Performance benefits over nested Row/Column:
|
* Performance characteristics:
|
||||||
* - Eliminates Row's two-pass measurement (measure author, then content with remaining width)
|
* - Zero intermediate layout nodes (no Box/Column wrappers)
|
||||||
* - Uses pre-computed pixel dimensions for author column width (55dp) and gaps
|
* - Pre-computed pixel dimensions cached across recompositions
|
||||||
* - Reduces content Column children from ~6 to ~3
|
* - Single measure pass with known constraints for all 6 slots
|
||||||
* - Single measure pass positions all sections with known constraints
|
* - 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 addPadding Whether to add standard note padding (12dp sides, 10dp top)
|
||||||
* @param showAuthorColumn Whether to reserve space for the author picture column
|
* @param showAuthorColumn Whether to reserve space for the author picture column
|
||||||
@@ -109,22 +110,17 @@ fun NoteComposeLayout(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Layout(
|
Layout(
|
||||||
content = {
|
contents =
|
||||||
// Slot 0: Author picture (55x55 area)
|
listOf(
|
||||||
Box { authorPicture() }
|
authorPicture,
|
||||||
// Slot 1: Relay badges (55dp wide, below author)
|
relayBadges,
|
||||||
Box { relayBadges() }
|
firstRow,
|
||||||
// Slot 2: First row (author name, time, options)
|
secondRow,
|
||||||
Box { firstRow() }
|
noteContent,
|
||||||
// Slot 3: Second row (NIP05, location, PoW)
|
reactionsRow,
|
||||||
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() }
|
|
||||||
},
|
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) { measurables, constraints ->
|
) { allMeasurables, constraints ->
|
||||||
val maxWidth = constraints.maxWidth
|
val maxWidth = constraints.maxWidth
|
||||||
|
|
||||||
// Compute padding offsets
|
// Compute padding offsets
|
||||||
@@ -138,70 +134,94 @@ fun NoteComposeLayout(
|
|||||||
val gap = if (showAuthorColumn) px.authorContentGap else 0
|
val gap = if (showAuthorColumn) px.authorContentGap else 0
|
||||||
val contentWidth = (innerWidth - authorW - gap).coerceAtLeast(0)
|
val contentWidth = (innerWidth - authorW - gap).coerceAtLeast(0)
|
||||||
|
|
||||||
// Build constraints for each section
|
// Pre-compute constraint objects once
|
||||||
val authorConstraints =
|
val authorConstraints = Constraints(maxWidth = px.authorWidth)
|
||||||
if (showAuthorColumn) Constraints(maxWidth = px.authorWidth) else ZeroConstraints
|
|
||||||
val contentConstraints = Constraints(maxWidth = contentWidth)
|
val contentConstraints = Constraints(maxWidth = contentWidth)
|
||||||
|
val fullWidthConstraints = Constraints(maxWidth = maxWidth)
|
||||||
|
|
||||||
// Measure all children in a single pass with pre-computed constraints
|
// Measure author column (single child per slot, skip if empty)
|
||||||
val authorPlaceable = measurables[0].measure(authorConstraints)
|
val authorPlaceable = allMeasurables[0].firstOrNull()?.measure(authorConstraints)
|
||||||
val relayPlaceable = measurables[1].measure(authorConstraints)
|
val relayPlaceable = allMeasurables[1].firstOrNull()?.measure(authorConstraints)
|
||||||
val firstRowPlaceable = measurables[2].measure(contentConstraints)
|
|
||||||
|
// Measure header rows
|
||||||
|
val firstRowPlaceable = allMeasurables[2].firstOrNull()?.measure(contentConstraints)
|
||||||
val secondRowPlaceable =
|
val secondRowPlaceable =
|
||||||
measurables[3].measure(
|
if (showSecondRow) allMeasurables[3].firstOrNull()?.measure(contentConstraints) else null
|
||||||
if (showSecondRow) contentConstraints else ZeroConstraints,
|
|
||||||
)
|
// Measure note content children directly (no Column wrapper)
|
||||||
val contentPlaceable = measurables[4].measure(contentConstraints)
|
val contentMeasurables = allMeasurables[4]
|
||||||
val reactionsPlaceable =
|
val contentPlaceables = arrayOfNulls<Placeable>(contentMeasurables.size)
|
||||||
measurables[5].measure(Constraints(maxWidth = maxWidth))
|
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
|
// Calculate section heights
|
||||||
val spacer = if (showContentSpacer) px.contentSpacer else 0
|
val spacer = if (showContentSpacer) px.contentSpacer else 0
|
||||||
|
|
||||||
val contentColumnHeight =
|
val contentColumnHeight =
|
||||||
firstRowPlaceable.height +
|
(firstRowPlaceable?.height ?: 0) +
|
||||||
(if (showSecondRow) secondRowPlaceable.height else 0) +
|
(secondRowPlaceable?.height ?: 0) +
|
||||||
spacer +
|
spacer +
|
||||||
contentPlaceable.height
|
contentStackHeight
|
||||||
|
|
||||||
val authorColumnHeight =
|
val authorColumnHeight =
|
||||||
if (showAuthorColumn) {
|
if (showAuthorColumn && authorPlaceable != null) {
|
||||||
authorPlaceable.height + px.authorBadgeGap + relayPlaceable.height
|
authorPlaceable.height + px.authorBadgeGap + (relayPlaceable?.height ?: 0)
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
||||||
val mainAreaHeight = maxOf(authorColumnHeight, contentColumnHeight)
|
val mainAreaHeight = maxOf(authorColumnHeight, contentColumnHeight)
|
||||||
val totalHeight = padTop + mainAreaHeight + reactionsPlaceable.height
|
val totalHeight = padTop + mainAreaHeight + reactionsHeight
|
||||||
|
|
||||||
layout(maxWidth, totalHeight.coerceAtLeast(constraints.minHeight)) {
|
layout(maxWidth, totalHeight.coerceAtLeast(constraints.minHeight)) {
|
||||||
// Place author column (inside padding)
|
// Place author column
|
||||||
if (showAuthorColumn) {
|
if (showAuthorColumn) {
|
||||||
authorPlaceable.placeRelative(padStart, padTop)
|
authorPlaceable?.placeRelative(padStart, padTop)
|
||||||
relayPlaceable.placeRelative(
|
relayPlaceable?.placeRelative(
|
||||||
padStart,
|
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
|
val contentX = padStart + authorW + gap
|
||||||
var y = padTop
|
var y = padTop
|
||||||
|
|
||||||
firstRowPlaceable.placeRelative(contentX, y)
|
firstRowPlaceable?.placeRelative(contentX, y)
|
||||||
y += firstRowPlaceable.height
|
y += firstRowPlaceable?.height ?: 0
|
||||||
|
|
||||||
if (showSecondRow) {
|
if (showSecondRow) {
|
||||||
secondRowPlaceable.placeRelative(contentX, y)
|
secondRowPlaceable?.placeRelative(contentX, y)
|
||||||
y += secondRowPlaceable.height
|
y += secondRowPlaceable?.height ?: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
y += spacer
|
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
|
// 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