Skip to content

Commit

Permalink
[DF] feat(Shopify#547): experimentalMaintainTopContentPosition prop…
Browse files Browse the repository at this point in the history
… added to TypeScript
  • Loading branch information
friyiajr committed Mar 30, 2023
1 parent 641c838 commit b34e8ef
Show file tree
Hide file tree
Showing 9 changed files with 75 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Release"
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
Expand Down
78 changes: 51 additions & 27 deletions ios/Sources/AutoLayoutView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import UIKit
}

@objc func setScrollOffset(_ scrollOffset: Int) {
// self.scrollOffset = CGFloat(scrollOffset)
self.scrollOffset = CGFloat(scrollOffset)
}

@objc func setWindowSize(_ windowSize: Int) {
Expand All @@ -32,8 +32,13 @@ import UIKit
self.disableAutoLayout = disableAutoLayout
}

@objc func setExperimentalMaintainTopContentPosition(_ experimentalMaintainTopContentPosition: Bool) {
self.maintainTopContentPosition = experimentalMaintainTopContentPosition
}

private var horizontal = false
// private var scrollOffset: CGFloat = 0
private var maintainTopContentPosition = false
private var scrollOffset: CGFloat = 0
private var windowSize: CGFloat = 0
private var renderAheadOffset: CGFloat = 0
private var enableInstrumentation = false
Expand All @@ -55,9 +60,11 @@ import UIKit
/// State that informs us whether this is the first render
private var isInitialRender: Bool = true

/// Id of the anchor element when using `maintainTopContentPosition`
private var anchorStableId: String = ""

private var firstItemStableId: String = ""
private var firstItemOffset: CGFloat = 0
/// Offset of the anchor when using `maintainTopContentPosition`
private var anchorOffset: CGFloat = 0

override func layoutSubviews() {
fixLayout()
Expand Down Expand Up @@ -96,6 +103,23 @@ import UIKit
return sequence(first: self, next: { $0.superview }).first(where: { $0 is UIScrollView }) as? UIScrollView
}

func getScrollViewOffset(for scrollView: UIScrollView?) -> CGFloat {
/// When using `maintainTopContentPosition` we can't use the offset provided by React
/// Native. Because its async, it is sometimes sent in too late for the position maintainence
/// calculation causing list jumps or sometimes wrong scroll positions altogether. Since this is still
/// experimental, the old scrollOffset is here to not regress previous functionality if the feature
/// doesn't work at scale.
///
/// The goal is that we can remove this in the future and get the offset from only one place 🤞
if let scrollView, maintainTopContentPosition {
return horizontal ?
scrollView.contentOffset.x :
scrollView.contentOffset.y
}

return scrollOffset
}

/// Sorts views by index and then invokes clearGaps which does the correction.
/// Performance: Sort is needed. Given relatively low number of views in RecyclerListView render tree this should be a non issue.
private func fixLayout() {
Expand All @@ -121,7 +145,7 @@ import UIKit

/// Finds the item with the first stable id and adjusts the scroll view offset based on how much
/// it moved when a new item is added.
private func maintainTopContentPosition(
private func adjustTopContentPosition(
cellContainers: [CellContainer],
scrollView: UIScrollView?
) {
Expand All @@ -132,9 +156,9 @@ import UIKit
cellContainer.frame.minX :
cellContainer.frame.minY

if cellContainer.layoutType == firstItemStableId {
if minValue != firstItemOffset {
let diff = minValue - firstItemOffset
if cellContainer.stableId == anchorStableId {
if minValue != anchorOffset {
let diff = minValue - anchorOffset

let currentOffset = horizontal
? scrollView.contentOffset.x
Expand Down Expand Up @@ -162,11 +186,11 @@ import UIKit
var maxBound: CGFloat = 0
var minBound: CGFloat = CGFloat(Int.max)
var maxBoundNextCell: CGFloat = 0
let correctedScrollOffset = (horizontal ? scrollView!.contentOffset.x : scrollView!.contentOffset.y) - (horizontal ? frame.minX : frame.minY)
lastMaxBoundOverall = 0
let correctedScrollOffset = getScrollViewOffset(for: scrollView)

var nextFirstItemStableId = ""
var nextFirstItemOffset: CGFloat = 0
lastMaxBoundOverall = 0
var nextAnchorStableId = ""
var nextAnchorOffset: CGFloat = 0

cellContainers.indices.dropLast().forEach { index in
let cellContainer = cellContainers[index]
Expand Down Expand Up @@ -243,32 +267,32 @@ import UIKit
}
}

// This state update is used for maintainTopContentPosition only.
// This is ignored during normal use cases
if (
nextFirstItemStableId == "" ||
nextCell.layoutType == firstItemStableId
) {
nextFirstItemOffset = horizontal ?
let isAnchorFound =
nextAnchorStableId == "" ||
nextCell.stableId == anchorStableId

if maintainTopContentPosition && isAnchorFound {
nextAnchorOffset = horizontal ?
nextCell.frame.minX :
nextCell.frame.minY

nextFirstItemStableId = nextCell.layoutType
nextAnchorStableId = nextCell.stableId
}

updateLastMaxBoundOverall(currentCell: cellContainer, nextCell: nextCell)
}

// IF experimental_maintainTopContentPosition = true
maintainTopContentPosition(
cellContainers: cellContainers,
scrollView: scrollView
)
if maintainTopContentPosition {
adjustTopContentPosition(
cellContainers: cellContainers,
scrollView: scrollView
)
}

lastMaxBound = maxBoundNextCell
lastMinBound = minBound
firstItemStableId = nextFirstItemStableId
firstItemOffset = nextFirstItemOffset
anchorStableId = nextAnchorStableId
anchorOffset = nextAnchorOffset
}

private func updateLastMaxBoundOverall(currentCell: CellContainer, nextCell: CellContainer) {
Expand Down
1 change: 1 addition & 0 deletions ios/Sources/AutoLayoutViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ @interface RCT_EXTERN_MODULE(AutoLayoutViewManager, RCTViewManager)
RCT_EXPORT_VIEW_PROPERTY(enableInstrumentation, BOOL)
RCT_EXPORT_VIEW_PROPERTY(disableAutoLayout, BOOL)
RCT_EXPORT_VIEW_PROPERTY(onBlankAreaEvent, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(experimentalMaintainTopContentPosition, BOOL)

@end
6 changes: 3 additions & 3 deletions ios/Sources/CellContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import Foundation

@objc class CellContainer: UIView {
var index: Int = -1
var layoutType: String = ""
var stableId: String = ""

@objc func setIndex(_ index: Int) {
self.index = index
}

@objc func setType(_ layoutType: String) {
self.layoutType = layoutType
@objc func setStableId(_ stableId: String) {
self.stableId = stableId
}
}
2 changes: 1 addition & 1 deletion ios/Sources/CellContainerManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
@interface RCT_EXTERN_MODULE(CellContainerManager, RCTViewManager)

RCT_EXPORT_VIEW_PROPERTY(index, NSInteger)
RCT_EXPORT_VIEW_PROPERTY(type, NSString)
RCT_EXPORT_VIEW_PROPERTY(stableId, NSString)

@end
7 changes: 6 additions & 1 deletion src/FlashList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,9 @@ class FlashList<T> extends React.PureComponent<
onBlankAreaEvent={this.props.onBlankArea}
onLayout={this.updateDistanceFromWindow}
disableAutoLayout={this.props.disableAutoLayout}
experimentalMaintainTopContentPosition={
this.props.experimentalMaintainTopContentPosition
}
>
{children}
</AutoLayoutView>
Expand Down Expand Up @@ -501,7 +504,9 @@ class FlashList<T> extends React.PureComponent<
...getCellContainerPlatformStyles(this.props.inverted!!, parentProps),
}}
index={parentProps.index}
type={this.props.keyExtractor?.(parentProps.data, parentProps.index)}
stableId={
this.props.keyExtractor?.(parentProps.data, parentProps.index) ?? ""
}
>
<PureComponentWrapper
extendedState={parentProps.extendedState}
Expand Down
7 changes: 7 additions & 0 deletions src/FlashListProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,4 +332,11 @@ export interface FlashListProps<TItem> extends ScrollViewProps {
* `false` again.
*/
disableAutoLayout?: boolean;

/**
* If enabled, FlashList will try and maintain the position of the list when items are added from the top.
* This prop requires you define a `keyExtractor` function. The keyExtractor is used to compute the list
* top anchor. Without it, the list will fail to render.
*/
experimentalMaintainTopContentPosition?: boolean;
}
4 changes: 4 additions & 0 deletions src/native/auto-layout/AutoLayoutView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export interface AutoLayoutViewProps {
onBlankAreaEvent?: BlankAreaEventHandler;
onLayout?: (event: LayoutChangeEvent) => void;
disableAutoLayout?: boolean;
experimentalMaintainTopContentPosition?: boolean;
}

class AutoLayoutView extends React.Component<AutoLayoutViewProps> {
Expand Down Expand Up @@ -63,6 +64,9 @@ class AutoLayoutView extends React.Component<AutoLayoutViewProps> {
listeners.length !== 0 || Boolean(this.props.onBlankAreaEvent)
}
disableAutoLayout={this.props.disableAutoLayout}
experimentalMaintainTopContentPosition={Boolean(
this.props.experimentalMaintainTopContentPosition
)}
>
{this.props.children}
</AutoLayoutViewNativeComponent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ export interface AutoLayoutViewNativeComponentProps {
onBlankAreaEvent: OnBlankAreaEventHandler;
enableInstrumentation: boolean;
disableAutoLayout?: boolean;
experimentalMaintainTopContentPosition?: boolean;
}

0 comments on commit b34e8ef

Please sign in to comment.