Skip to content

Commit

Permalink
Reduce unnecessary recompositions
Browse files Browse the repository at this point in the history
  • Loading branch information
aabewhite committed Nov 1, 2024
1 parent 5bc40b2 commit 8a42bc6
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 100 deletions.
156 changes: 70 additions & 86 deletions Sources/SkipUI/SkipUI/Compose/ComposeLayouts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import androidx.compose.foundation.layout.requiredHeightIn
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.requiredWidthIn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.layout.Layout
Expand Down Expand Up @@ -140,118 +142,100 @@ import androidx.compose.ui.unit.dp
return
}

// Note: We only allow edges we're interested in to affect our internal state and output. This is
// critical for reducing recompositions, especially during e.g. navigation animations
let expandOrCheckEdges = expandInto.union(checkEdges)
// Note: We only allow edges we're interested in to affect our internal state and output. This is critical
// for reducing recompositions, especially during e.g. navigation animations. We also match our internal
// state to our output to ensure we aren't re-calling the target block when output hasn't changed
let edgesState = remember { mutableStateOf(checkEdges) }
let edges = edgesState.value
var expansionTop = 0
if expandInto.contains(Edge.Set.top) && edges.contains(Edge.Set.top) {
expansionTop = Int(safeArea.safeBoundsPx.top - safeArea.presentationBoundsPx.top)
}
var expansionBottom = 0
if expandInto.contains(Edge.Set.bottom) && edges.contains(Edge.Set.bottom) {
expansionBottom = Int(safeArea.presentationBoundsPx.bottom - safeArea.safeBoundsPx.bottom)
}
var expansionLeft = 0
var expansionRight = 0
let isRTL = LocalLayoutDirection.current == androidx.compose.ui.unit.LayoutDirection.Rtl

let boundsPx = remember { mutableStateOf(Rect.Zero) }
let (boundsLeft, boundsTop, boundsRight, boundsBottom) = boundsPx.value
var (safeLeft, safeTop, safeRight, safeBottom) = safeArea.safeBoundsPx
var topPx = 0
var bottomPx = 0
var leadingPx = 0
var trailingPx = 0
var edges: Edge.Set = []
if boundsPx.value != Rect.Zero {
if expandOrCheckEdges.contains(Edge.Set.top), boundsTop <= safeTop + 0.1 {
if checkEdges.contains(Edge.Set.top) {
edges.insert(Edge.Set.top)
}
if expandInto.contains(Edge.Set.top) {
topPx = Int(safeArea.safeBoundsPx.top - safeArea.presentationBoundsPx.top)
safeTop = safeArea.presentationBoundsPx.top
}
if isRTL {
if expandInto.contains(Edge.Set.leading) && edges.contains(Edge.Set.leading) {
expansionRight = Int(safeArea.presentationBoundsPx.right - safeArea.safeBoundsPx.right)
}
if expandOrCheckEdges.contains(Edge.Set.bottom), boundsBottom >= safeBottom - 0.1 {
if checkEdges.contains(Edge.Set.bottom) {
edges.insert(Edge.Set.bottom)
}
if expandInto.contains(Edge.Set.bottom) {
bottomPx = Int(safeArea.presentationBoundsPx.bottom - safeArea.safeBoundsPx.bottom)
safeBottom = safeArea.presentationBoundsPx.bottom
}
if expandInto.contains(Edge.Set.trailing) && edges.contains(Edge.Set.trailing) {
expansionLeft = Int(safeArea.safeBoundsPx.left - safeArea.presentationBoundsPx.left)
}
if isRTL {
if expandOrCheckEdges.contains(Edge.Set.leading), boundsRight >= safeRight - 0.1 {
if checkEdges.contains(Edge.Set.leading) {
edges.insert(Edge.Set.leading)
}
if expandInto.contains(Edge.Set.leading) {
leadingPx = Int(safeArea.presentationBoundsPx.right - safeArea.safeBoundsPx.right)
safeRight = safeArea.presentationBoundsPx.right
}
}
if expandOrCheckEdges.contains(Edge.Set.trailing), boundsLeft <= safeLeft + 0.1 {
if checkEdges.contains(Edge.Set.trailing) {
edges.insert(Edge.Set.trailing)
}
if expandInto.contains(Edge.Set.trailing) {
leadingPx = Int(safeArea.safeBoundsPx.left - safeArea.presentationBoundsPx.left)
safeLeft = safeArea.presentationBoundsPx.left
}
}
} else {
if expandOrCheckEdges.contains(Edge.Set.leading), boundsLeft <= safeLeft + 0.1 {
if checkEdges.contains(Edge.Set.leading) {
edges.insert(Edge.Set.leading)
}
if expandInto.contains(Edge.Set.leading) {
leadingPx = Int(safeArea.safeBoundsPx.left - safeArea.presentationBoundsPx.left)
safeLeft = safeArea.presentationBoundsPx.left
}
}
if expandOrCheckEdges.contains(Edge.Set.trailing), boundsRight >= safeRight - 0.1 {
if checkEdges.contains(Edge.Set.trailing) {
edges.insert(Edge.Set.trailing)
}
if expandInto.contains(Edge.Set.trailing) {
leadingPx = Int(safeArea.presentationBoundsPx.right - safeArea.safeBoundsPx.right)
safeRight = safeArea.presentationBoundsPx.right
}
}
} else {
if expandInto.contains(Edge.Set.leading) && edges.contains(Edge.Set.leading) {
expansionLeft = Int(safeArea.safeBoundsPx.left - safeArea.presentationBoundsPx.left)
}
if expandInto.contains(Edge.Set.trailing) && edges.contains(Edge.Set.trailing) {
expansionRight = Int(safeArea.presentationBoundsPx.right - safeArea.safeBoundsPx.right)
}
}

var (safeLeft, safeTop, safeRight, safeBottom) = safeArea.safeBoundsPx
safeLeft -= expansionLeft
safeTop -= expansionTop
safeRight += expansionRight
safeBottom += expansionBottom

let contentSafeBounds = Rect(top: safeTop, left: safeLeft, bottom: safeBottom, right: safeRight)
let contentSafeArea = SafeArea(presentation: safeArea.presentationBoundsPx, safe: contentSafeBounds, absoluteSystemBars: safeArea.absoluteSystemBarEdges)
let expansion = IntRect(top: topPx, left: isRTL ? trailingPx : leadingPx, bottom: bottomPx, right: isRTL ? leadingPx : trailingPx)
EnvironmentValues.shared.setValues {
$0.set_safeArea(contentSafeArea)
} in: {
Layout(modifier: modifier.onGloballyPositionedInWindow { bounds in
let top = expandOrCheckEdges.contains(Edge.Set.top) ? bounds.top : Float(0.0)
let bottom = expandOrCheckEdges.contains(Edge.Set.bottom) ? bounds.bottom : Float(0.0)
let left: Float
let right: Float
if isRTL {
left = expandOrCheckEdges.contains(Edge.Set.trailing) ? bounds.left : Float(0.0)
right = expandOrCheckEdges.contains(Edge.Set.leading) ? bounds.right : Float(0.0)
} else {
left = expandOrCheckEdges.contains(Edge.Set.leading) ? bounds.left : Float(0.0)
right = expandOrCheckEdges.contains(Edge.Set.trailing) ? bounds.right : Float(0.0)
}
boundsPx.value = Rect(top: top, left: left, bottom: bottom, right: right)
Layout(modifier: modifier.onGloballyPositionedInWindow {
let edges = adjacentSafeAreaEdges(bounds: $0, safeArea: safeArea, isRTL: isRTL, checkEdges: expandInto.union(checkEdges))
edgesState.value = edges
}, content: {
target(expansion, edges)
let expansion = IntRect(top: expansionTop, left: expansionLeft, bottom: expansionBottom, right: expansionRight)
target(expansion, edges.intersection(checkEdges))
}) { measurables, constraints in
guard !measurables.isEmpty() else {
return layout(width: 0, height: 0) {}
}
let updatedConstraints = constraints.copy(maxWidth: constraints.maxWidth + leadingPx + trailingPx, maxHeight: constraints.maxHeight + topPx + bottomPx)
let updatedConstraints = constraints.copy(maxWidth: constraints.maxWidth + expansionLeft + expansionRight, maxHeight: constraints.maxHeight + expansionTop + expansionBottom)
let targetPlaceables = measurables.map { $0.measure(updatedConstraints) }
layout(width: targetPlaceables[0].width, height: targetPlaceables[0].height) {
// Layout will center extra space by default
let relativeTopPx = topPx - ((topPx + bottomPx) / 2)
let relativeLeadingPx = leadingPx - ((leadingPx + trailingPx) / 2)
let relativeTop = expansionTop - ((expansionTop + expansionBottom) / 2)
let expansionLeading = isRTL ? expansionRight : expansionLeft
let relativeLeading = expansionLeading - ((expansionLeft + expansionRight) / 2)
for targetPlaceable in targetPlaceables {
targetPlaceable.placeRelative(x = -relativeLeadingPx, y = -relativeTopPx)
targetPlaceable.placeRelative(x = -relativeLeading, y = -relativeTop)
}
}
}
}
}

private func adjacentSafeAreaEdges(bounds: Rect, safeArea: SafeArea, isRTL: Bool, checkEdges: Edge.Set) -> Edge.Set {
var edges: Edge.Set = []
if checkEdges.contains(Edge.Set.top), bounds.top <= safeArea.safeBoundsPx.top + 0.1 {
edges.insert(Edge.Set.top)
}
if checkEdges.contains(Edge.Set.bottom), bounds.bottom >= safeArea.safeBoundsPx.bottom - 0.1 {
edges.insert(Edge.Set.bottom)
}
if isRTL {
if checkEdges.contains(Edge.Set.leading), bounds.right >= safeArea.safeBoundsPx.right - 0.1 {
edges.insert(Edge.Set.leading)
}
if checkEdges.contains(Edge.Set.trailing), bounds.left <= safeArea.safeBoundsPx.left + 0.1 {
edges.insert(Edge.Set.trailing)
}
} else {
if checkEdges.contains(Edge.Set.leading), bounds.left <= safeArea.safeBoundsPx.left + 0.1 {
edges.insert(Edge.Set.leading)
}
if checkEdges.contains(Edge.Set.trailing), bounds.right >= safeArea.safeBoundsPx.right - 0.1 {
edges.insert(Edge.Set.trailing)
}
}
return edges
}

/// Layout the given view with the given padding.
@Composable func PaddingLayout(view: View, padding: EdgeInsets, context: ComposeContext) {
PaddingLayout(padding: padding, context: context) { view.Compose($0) }
Expand Down
36 changes: 22 additions & 14 deletions Sources/SkipUI/SkipUI/Containers/List.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ public final class List : View {
var ignoresSafeAreaEdges: Edge.Set = [.top, .bottom]
ignoresSafeAreaEdges.formIntersection(safeArea?.absoluteSystemBarEdges ?? [])
ComposeContainer(scrollAxes: .vertical, modifier: context.modifier, fillWidth: true, fillHeight: true, then: Modifier.background(BackgroundColor(styling: styling, isItem: false))) { modifier in
IgnoresSafeAreaLayout(expandInto: ignoresSafeAreaEdges, checkEdges: [.top, .bottom], modifier: modifier) { safeAreaExpansion, safeAreaEdges in
IgnoresSafeAreaLayout(expandInto: ignoresSafeAreaEdges, checkEdges: [.bottom], modifier: modifier) { safeAreaExpansion, safeAreaEdges in
let containerModifier: Modifier
let refreshing = remember { mutableStateOf(false) }
let refreshAction = EnvironmentValues.shared.refresh
Expand All @@ -136,7 +136,7 @@ public final class List : View {
let density = LocalDensity.current
let headerSafeAreaHeight = with(density) { safeAreaExpansion.top.toDp() }
let footerSafeAreaHeight = with(density) { safeAreaExpansion.bottom.toDp() }
ComposeList(context: itemContext, styling: styling, headerSafeAreaHeight: headerSafeAreaHeight, footerSafeAreaHeight: footerSafeAreaHeight, safeAreaEdges: safeAreaEdges)
ComposeList(context: itemContext, styling: styling, arguments: ListArguments(headerSafeAreaHeight: headerSafeAreaHeight, footerSafeAreaHeight: footerSafeAreaHeight, safeAreaEdges: safeAreaEdges))
if let refreshState {
PullRefreshIndicator(refreshing.value, refreshState, Modifier.align(androidx.compose.ui.Alignment.TopCenter))
}
Expand All @@ -146,7 +146,7 @@ public final class List : View {
}

// SKIP INSERT: @OptIn(ExperimentalFoundationApi::class)
@Composable private func ComposeList(context: ComposeContext, styling: ListStyling, headerSafeAreaHeight: Dp, footerSafeAreaHeight: Dp, safeAreaEdges: Edge.Set) {
@Composable private func ComposeList(context: ComposeContext, styling: ListStyling, arguments: ListArguments) {
// Collect all top-level views to compose. The LazyColumn itself is not a composable context, so we have to execute
// our content's Compose function to collect its views before entering the LazyColumn body, then use LazyColumn's
// LazyListScope functions to compose individual items
Expand All @@ -167,13 +167,13 @@ public final class List : View {
let searchableState = EnvironmentValues.shared._searchableState
let isSearchable = searchableState?.isOnNavigationStack() == false

let hasHeader = styling.style != ListStyle.plain || (!isSearchable && headerSafeAreaHeight.value > 0)
let hasFooter = styling.style != ListStyle.plain || footerSafeAreaHeight.value > 0
let hasHeader = styling.style != ListStyle.plain || (!isSearchable && arguments.headerSafeAreaHeight.value > 0)
let hasFooter = styling.style != ListStyle.plain || arguments.footerSafeAreaHeight.value > 0

// Remember the factory because we use it in the remembered reorderable state
let factoryContext = remember { mutableStateOf(LazyItemFactoryContext()) }
let moveTrigger = remember { mutableStateOf(0) }
let listState = rememberLazyListState(initialFirstVisibleItemIndex = isSearchable && headerSafeAreaHeight.value <= 0 ? 1 : 0)
let listState = rememberLazyListState(initialFirstVisibleItemIndex = isSearchable && arguments.headerSafeAreaHeight.value <= 0 ? 1 : 0)
let reorderableState = rememberReorderableLazyListState(listState: listState, onMove: { from, to in
// Trigger recompose on move, but don't read the trigger state until we're inside the list content to limit its scope
factoryContext.value.move(from: from.index, to: to.index, trigger: { moveTrigger.value = $0 })
Expand Down Expand Up @@ -204,11 +204,13 @@ public final class List : View {
}
PreferenceValues.shared.contribute(context: context, key: ScrollToIDPreferenceKey.self, value: scrollToID)
let isSystemBackground = styling.style != ListStyle.plain && styling.backgroundVisibility != Visibility.hidden
// When there is a nav search bar we won't be up against the safe area, but assume we're against the search bar
if safeAreaEdges.contains(Edge.Set.top) || (searchableState?.isOnNavigationStack() == true && LocalNavigator.current?.isRoot == true) {
PreferenceValues.shared.contribute(context: context, key: ToolbarPreferenceKey.self, value: ToolbarPreferences(isSystemBackground: isSystemBackground, scrollableState: listState, for: [ToolbarPlacement.navigationBar]))
}
if safeAreaEdges.contains(Edge.Set.bottom) {
// We contribute top bar preferences even without knowing we're safe area-adjacent for multiple reasons:
// - When there is a search bar we may not be adjacent to the top safe area, but we should act like it
// - An expanding nav bar can causes issues detecting safe area adjacency
// - It is unlikely that anyone will use a grouped-style list that is not top-bar adjacent, so the top
// bar should always have the grouped-style system color
PreferenceValues.shared.contribute(context: context, key: ToolbarPreferenceKey.self, value: ToolbarPreferences(isSystemBackground: isSystemBackground, scrollableState: listState, for: [ToolbarPlacement.navigationBar]))
if arguments.safeAreaEdges.contains(Edge.Set.bottom) {
PreferenceValues.shared.contribute(context: context, key: ToolbarPreferenceKey.self, value: ToolbarPreferences(isSystemBackground: isSystemBackground, scrollableState: listState, for: [ToolbarPlacement.bottomBar]))
PreferenceValues.shared.contribute(context: context, key: TabBarPreferenceKey.self, value: ToolbarBarPreferences(isSystemBackground: isSystemBackground, scrollableState: listState))
}
Expand Down Expand Up @@ -324,13 +326,13 @@ public final class List : View {

if isSearchable {
item {
ComposeSearchField(state: searchableState!, context: context, styling: styling, safeAreaHeight: headerSafeAreaHeight)
ComposeSearchField(state: searchableState!, context: context, styling: styling, safeAreaHeight: arguments.headerSafeAreaHeight)
}
}
if hasHeader {
let hasTopSection = collectingComposer.views.firstOrNull() is LazySectionHeader
item {
ComposeHeader(styling: styling, safeAreaHeight: isSearchable ? 0.dp : headerSafeAreaHeight, hasTopSection: hasTopSection)
ComposeHeader(styling: styling, safeAreaHeight: isSearchable ? 0.dp : arguments.headerSafeAreaHeight, hasTopSection: hasTopSection)
}
}
for (view, level) in collectingComposer.views {
Expand All @@ -343,7 +345,7 @@ public final class List : View {
if hasFooter {
let hasBottomSection = collectingComposer.views.lastOrNull() is LazySectionFooter
item {
ComposeFooter(styling: styling, safeAreaHeight: footerSafeAreaHeight, hasBottomSection: hasBottomSection)
ComposeFooter(styling: styling, safeAreaHeight: arguments.footerSafeAreaHeight, hasBottomSection: hasBottomSection)
}
}
}
Expand Down Expand Up @@ -705,6 +707,12 @@ final class ListItemComposer: RenderingComposer {
}
}
}

@Stable struct ListArguments: Equatable {
let headerSafeAreaHeight: Dp
let footerSafeAreaHeight: Dp
let safeAreaEdges: Edge.Set
}
#endif

public struct ListStyle: RawRepresentable, Equatable {
Expand Down

0 comments on commit 8a42bc6

Please sign in to comment.