From 43ec8d27d2021fcaad9874772283a1056837ef9b Mon Sep 17 00:00:00 2001 From: Daniel Friyia JR Date: Fri, 10 Feb 2023 09:17:15 -0500 Subject: [PATCH 1/6] [DF] feat(#547): `experimentalMaintainTopContentPosition` works vertically --- .../xcschemes/FlatListPro.xcscheme | 2 +- fixture/ios/Podfile.lock | 2 +- fixture/src/List.tsx | 42 +++++++++++--- ios/Sources/AutoLayoutView.swift | 58 ++++++++++++++++++- ios/Sources/CellContainer.swift | 5 ++ ios/Sources/CellContainerManager.m | 1 + src/FlashList.tsx | 4 +- 7 files changed, 99 insertions(+), 15 deletions(-) diff --git a/fixture/ios/FlatListPro.xcodeproj/xcshareddata/xcschemes/FlatListPro.xcscheme b/fixture/ios/FlatListPro.xcodeproj/xcshareddata/xcschemes/FlatListPro.xcscheme index 571df8db7..1c408c6f3 100644 --- a/fixture/ios/FlatListPro.xcodeproj/xcshareddata/xcschemes/FlatListPro.xcscheme +++ b/fixture/ios/FlatListPro.xcodeproj/xcshareddata/xcschemes/FlatListPro.xcscheme @@ -41,7 +41,7 @@ { - const arr = new Array(size); +interface ListItem { + value: number; + type?: string; +} + +let newItemIndexes = 1001; + +const generateArray = (size: number): ListItem[] => { + const arr = new Array(size); for (let i = 0; i < size; i++) { - arr[i] = i; + arr[i] = { value: i }; } + return arr; }; const List = () => { const [refreshing, setRefreshing] = useState(false); const [data, setData] = useState(generateArray(100)); + const [isLoading, setIsLoading] = useState(false); - const list = useRef | null>(null); + const list = useRef | null>(null); + + useFocusEffect( + React.useCallback(() => { + newItemIndexes = 1001; + }, []) + ); const removeItem = (item: number) => { setData( data.filter((dataItem) => { - return dataItem !== item; + return dataItem.value !== item; }) ); list.current?.prepareForLayoutAnimationRender(); @@ -37,12 +54,19 @@ const List = () => { LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); }; - const renderItem = ({ item }: { item: number }) => { - const backgroundColor = item % 2 === 0 ? "#00a1f1" : "#ffbb00"; + const renderItem = ({ item }: { item: ListItem }) => { + const backgroundColor = item.value % 2 === 0 ? "#00a1f1" : "#ffbb00"; + + // if (Number(item.value) >= 90 && Number(item.value) <= 99) { + // return ; + // } + // item.value % 2 === 0 + // ? 100 + (item.value > 1000 ? item.value / 10 : item.value) + 1 + // : 200 + (item.value > 1000 ? item.value / 10 : item.value), return ( { - removeItem(item); + removeItem(item.value); }} > { height: item % 2 === 0 ? 100 : 200, }} > - Cell Id: {item} + Cell Id: {item.value} ); diff --git a/ios/Sources/AutoLayoutView.swift b/ios/Sources/AutoLayoutView.swift index f18e92c3d..03e43c717 100644 --- a/ios/Sources/AutoLayoutView.swift +++ b/ios/Sources/AutoLayoutView.swift @@ -13,7 +13,7 @@ import UIKit } @objc func setScrollOffset(_ scrollOffset: Int) { - self.scrollOffset = CGFloat(scrollOffset) +// self.scrollOffset = CGFloat(scrollOffset) } @objc func setWindowSize(_ windowSize: Int) { @@ -33,7 +33,7 @@ import UIKit } private var horizontal = false - private var scrollOffset: CGFloat = 0 +// private var scrollOffset: CGFloat = 0 private var windowSize: CGFloat = 0 private var renderAheadOffset: CGFloat = 0 private var enableInstrumentation = false @@ -46,9 +46,23 @@ import UIKit /// Tracks where first pixel is drawn in the visible window private var lastMinBound: CGFloat = 0 + /// Marks the first Item in the Scroll View + private var firstItemMarker: CellContainer? = nil + + /// The position of the item in the Scroll View after insertion / deletion + private var previousMarkerOffset: CGFloat = -1 + + /// State that informs us whether this is the first render + private var isInitialRender: Bool = true + + + private var firstItemStableId: String = "" + private var firstItemOffset: CGFloat = 0 + override func layoutSubviews() { fixLayout() super.layoutSubviews() + self.isInitialRender = false guard enableInstrumentation, let scrollView = getScrollView() else { return } @@ -107,13 +121,19 @@ import UIKit /// Checks for overlaps or gaps between adjacent items and then applies a correction. /// Performance: RecyclerListView renders very small number of views and this is not going to trigger multiple layouts on the iOS side. private func clearGaps(for cellContainers: [CellContainer]) { + let scrollView = getScrollView() var maxBound: CGFloat = 0 var minBound: CGFloat = CGFloat(Int.max) var maxBoundNextCell: CGFloat = 0 - let correctedScrollOffset = scrollOffset - (horizontal ? frame.minX : frame.minY) + let correctedScrollOffset = scrollView!.contentOffset.y - (horizontal ? frame.minX : frame.minY) lastMaxBoundOverall = 0 + + var nextFirstItemStableId = "" + var nextFirstItemOffset: CGFloat = 0 + cellContainers.indices.dropLast().forEach { index in let cellContainer = cellContainers[index] + let cellTop = cellContainer.frame.minY let cellBottom = cellContainer.frame.maxY let cellLeft = cellContainer.frame.minX @@ -185,9 +205,41 @@ import UIKit maxBoundNextCell = max(maxBound, nextCell.frame.maxY) } } + if(nextFirstItemStableId == "" || nextCell.layoutType == firstItemStableId) { + nextFirstItemOffset = nextCell.frame.minY + nextFirstItemStableId = nextCell.layoutType + } updateLastMaxBoundOverall(currentCell: cellContainer, nextCell: nextCell) } + // This was placed here so that offset adjustments would ONLY be performed after + // all necessary views were pulled up to remove the white space + cellContainers.indices.forEach { index in + let cellContainer = cellContainers[index] + + if cellContainer.layoutType == firstItemStableId { + if cellContainers[index].frame.minY != firstItemOffset { + print("TAG: firstItemStableId: \(firstItemStableId)") + print("TAG: nextFirstItemStableId: \(nextFirstItemStableId)") + + let diff = cellContainers[index].frame.minY - firstItemOffset + + print("TAG: diff: \(diff)") + + if let scrollView = scrollView, !self.isInitialRender { + let newValue = scrollView.contentOffset.y + diff + print("NewValue \(newValue)") + let newScrollHeight = superview!.frame.height - scrollView.frame.height + print("New Scroll Height \(newScrollHeight)") + scrollView.contentOffset = CGPoint(x: 0, y: scrollView.contentOffset.y + diff) + print("Content Offset \(scrollView.contentOffset.y)") + } + } + } + } + + firstItemStableId = nextFirstItemStableId + firstItemOffset = nextFirstItemOffset lastMaxBound = maxBoundNextCell lastMinBound = minBound } diff --git a/ios/Sources/CellContainer.swift b/ios/Sources/CellContainer.swift index 7f09ce767..6b1f119ae 100644 --- a/ios/Sources/CellContainer.swift +++ b/ios/Sources/CellContainer.swift @@ -2,8 +2,13 @@ import Foundation @objc class CellContainer: UIView { var index: Int = -1 + var layoutType: String = "" @objc func setIndex(_ index: Int) { self.index = index } + + @objc func setType(_ layoutType: String) { + self.layoutType = layoutType + } } diff --git a/ios/Sources/CellContainerManager.m b/ios/Sources/CellContainerManager.m index 7bcad13fd..e207de0f4 100644 --- a/ios/Sources/CellContainerManager.m +++ b/ios/Sources/CellContainerManager.m @@ -4,5 +4,6 @@ @interface RCT_EXTERN_MODULE(CellContainerManager, RCTViewManager) RCT_EXPORT_VIEW_PROPERTY(index, NSInteger) +RCT_EXPORT_VIEW_PROPERTY(type, NSString) @end diff --git a/src/FlashList.tsx b/src/FlashList.tsx index 39e5c94e0..0616a78db 100644 --- a/src/FlashList.tsx +++ b/src/FlashList.tsx @@ -160,6 +160,7 @@ class FlashList extends React.PureComponent< prevState: FlashListState ): FlashListState { const newState = { ...prevState }; + if (prevState.numColumns !== nextProps.numColumns) { newState.numColumns = nextProps.numColumns || 1; newState.layoutProvider = FlashList.getLayoutProvider( @@ -173,7 +174,6 @@ class FlashList extends React.PureComponent< ); // RLV retries to reposition the first visible item on layout provider change. // It's not required in our case so we're disabling it - newState.layoutProvider.shouldRefreshWithAnchoring = false; } if (nextProps.data !== prevState.data) { newState.data = nextProps.data; @@ -188,6 +188,7 @@ class FlashList extends React.PureComponent< newState.extraData = { value: nextProps.extraData }; } newState.renderItem = nextProps.renderItem; + newState.layoutProvider.shouldRefreshWithAnchoring = false; return newState; } @@ -500,6 +501,7 @@ class FlashList extends React.PureComponent< ...getCellContainerPlatformStyles(this.props.inverted!!, parentProps), }} index={parentProps.index} + type={this.props.keyExtractor?.(parentProps.data, parentProps.index)} > Date: Thu, 23 Feb 2023 17:26:24 -0500 Subject: [PATCH 2/6] [DF] feat(#547): `experimentalMaintainTopContentPosition` works horizontally --- fixture/src/List.tsx | 1 + ios/Sources/AutoLayoutView.swift | 86 +++++++++++++++++++++----------- 2 files changed, 57 insertions(+), 30 deletions(-) diff --git a/fixture/src/List.tsx b/fixture/src/List.tsx index d8acd66f6..ce50e8141 100644 --- a/fixture/src/List.tsx +++ b/fixture/src/List.tsx @@ -13,6 +13,7 @@ import { } from "react-native"; import { FlashList } from "@shopify/flash-list"; import { useFocusEffect } from "@react-navigation/native"; +import { TouchableOpacity } from "react-native-gesture-handler"; interface ListItem { value: number; diff --git a/ios/Sources/AutoLayoutView.swift b/ios/Sources/AutoLayoutView.swift index 03e43c717..108a37c67 100644 --- a/ios/Sources/AutoLayoutView.swift +++ b/ios/Sources/AutoLayoutView.swift @@ -118,6 +118,42 @@ import UIKit fixFooter() } + /// 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( + cellContainers: [CellContainer], + scrollView: UIScrollView? + ) { + guard let scrollView = scrollView, !self.isInitialRender else { return } + + for cellContainer in cellContainers { + let minValue = horizontal ? + cellContainer.frame.minX : + cellContainer.frame.minY + + if cellContainer.layoutType == firstItemStableId { + if minValue != firstItemOffset { + let diff = minValue - firstItemOffset + + let currentOffset = horizontal + ? scrollView.contentOffset.x + : scrollView.contentOffset.y + + let scrollValue = diff + currentOffset + + scrollView.contentOffset = CGPoint( + x: horizontal ? scrollValue : 0, + y: horizontal ? 0 : scrollValue + ) + + // You only need to adjust the scroll view once. Break the + // loop after this + return + } + } + } + } + /// Checks for overlaps or gaps between adjacent items and then applies a correction. /// Performance: RecyclerListView renders very small number of views and this is not going to trigger multiple layouts on the iOS side. private func clearGaps(for cellContainers: [CellContainer]) { @@ -125,7 +161,7 @@ import UIKit var maxBound: CGFloat = 0 var minBound: CGFloat = CGFloat(Int.max) var maxBoundNextCell: CGFloat = 0 - let correctedScrollOffset = scrollView!.contentOffset.y - (horizontal ? frame.minX : frame.minY) + let correctedScrollOffset = (horizontal ? scrollView!.contentOffset.x : scrollView!.contentOffset.y) - (horizontal ? frame.minX : frame.minY) lastMaxBoundOverall = 0 var nextFirstItemStableId = "" @@ -205,43 +241,33 @@ import UIKit maxBoundNextCell = max(maxBound, nextCell.frame.maxY) } } - if(nextFirstItemStableId == "" || nextCell.layoutType == firstItemStableId) { - nextFirstItemOffset = nextCell.frame.minY + + // This state update is used for maintainTopContentPosition only. + // This is ignored during normal use cases + if ( + nextFirstItemStableId == "" || + nextCell.layoutType == firstItemStableId + ) { + nextFirstItemOffset = horizontal ? + nextCell.frame.minX : + nextCell.frame.minY + nextFirstItemStableId = nextCell.layoutType } + updateLastMaxBoundOverall(currentCell: cellContainer, nextCell: nextCell) } - // This was placed here so that offset adjustments would ONLY be performed after - // all necessary views were pulled up to remove the white space - cellContainers.indices.forEach { index in - let cellContainer = cellContainers[index] - - if cellContainer.layoutType == firstItemStableId { - if cellContainers[index].frame.minY != firstItemOffset { - print("TAG: firstItemStableId: \(firstItemStableId)") - print("TAG: nextFirstItemStableId: \(nextFirstItemStableId)") - - let diff = cellContainers[index].frame.minY - firstItemOffset - - print("TAG: diff: \(diff)") - - if let scrollView = scrollView, !self.isInitialRender { - let newValue = scrollView.contentOffset.y + diff - print("NewValue \(newValue)") - let newScrollHeight = superview!.frame.height - scrollView.frame.height - print("New Scroll Height \(newScrollHeight)") - scrollView.contentOffset = CGPoint(x: 0, y: scrollView.contentOffset.y + diff) - print("Content Offset \(scrollView.contentOffset.y)") - } - } - } - } + // IF experimental_maintainTopContentPosition = true + maintainTopContentPosition( + cellContainers: cellContainers, + scrollView: scrollView + ) - firstItemStableId = nextFirstItemStableId - firstItemOffset = nextFirstItemOffset lastMaxBound = maxBoundNextCell lastMinBound = minBound + firstItemStableId = nextFirstItemStableId + firstItemOffset = nextFirstItemOffset } private func updateLastMaxBoundOverall(currentCell: CellContainer, nextCell: CellContainer) { From bc60a31a9f69bad07e74c694dab9f4b155263f9f Mon Sep 17 00:00:00 2001 From: Daniel Friyia JR Date: Sat, 25 Feb 2023 09:51:32 -0500 Subject: [PATCH 3/6] [DF] feat(#547): `experimentalMaintainTopContentPosition` prop added to TypeScript --- .../xcschemes/FlatListPro.xcscheme | 2 +- ios/Sources/AutoLayoutView.swift | 78 ++++++++++++------- ios/Sources/AutoLayoutViewManager.m | 1 + ios/Sources/CellContainer.swift | 6 +- ios/Sources/CellContainerManager.m | 2 +- src/FlashList.tsx | 7 +- src/FlashListProps.ts | 7 ++ src/native/auto-layout/AutoLayoutView.tsx | 4 + .../AutoLayoutViewNativeComponentProps.ts | 1 + 9 files changed, 75 insertions(+), 33 deletions(-) diff --git a/fixture/ios/FlatListPro.xcodeproj/xcshareddata/xcschemes/FlatListPro.xcscheme b/fixture/ios/FlatListPro.xcodeproj/xcshareddata/xcschemes/FlatListPro.xcscheme index 1c408c6f3..571df8db7 100644 --- a/fixture/ios/FlatListPro.xcodeproj/xcshareddata/xcschemes/FlatListPro.xcscheme +++ b/fixture/ios/FlatListPro.xcodeproj/xcshareddata/xcschemes/FlatListPro.xcscheme @@ -41,7 +41,7 @@ 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() { @@ -120,7 +144,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? ) { @@ -131,9 +155,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 @@ -161,11 +185,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] @@ -242,32 +266,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) { diff --git a/ios/Sources/AutoLayoutViewManager.m b/ios/Sources/AutoLayoutViewManager.m index 855ce72e5..021ecf536 100644 --- a/ios/Sources/AutoLayoutViewManager.m +++ b/ios/Sources/AutoLayoutViewManager.m @@ -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 diff --git a/ios/Sources/CellContainer.swift b/ios/Sources/CellContainer.swift index 6b1f119ae..444ebfcb5 100644 --- a/ios/Sources/CellContainer.swift +++ b/ios/Sources/CellContainer.swift @@ -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 } } diff --git a/ios/Sources/CellContainerManager.m b/ios/Sources/CellContainerManager.m index e207de0f4..1defa7d6f 100644 --- a/ios/Sources/CellContainerManager.m +++ b/ios/Sources/CellContainerManager.m @@ -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 diff --git a/src/FlashList.tsx b/src/FlashList.tsx index 0616a78db..3f4a6fdd4 100644 --- a/src/FlashList.tsx +++ b/src/FlashList.tsx @@ -466,6 +466,9 @@ class FlashList extends React.PureComponent< onBlankAreaEvent={this.props.onBlankArea} onLayout={this.updateDistanceFromWindow} disableAutoLayout={this.props.disableAutoLayout} + experimentalMaintainTopContentPosition={ + this.props.experimentalMaintainTopContentPosition + } > {children} @@ -501,7 +504,9 @@ class FlashList 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) ?? "" + } > 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; } diff --git a/src/native/auto-layout/AutoLayoutView.tsx b/src/native/auto-layout/AutoLayoutView.tsx index c040dfa1f..bb7b636bd 100644 --- a/src/native/auto-layout/AutoLayoutView.tsx +++ b/src/native/auto-layout/AutoLayoutView.tsx @@ -29,6 +29,7 @@ export interface AutoLayoutViewProps { onBlankAreaEvent?: BlankAreaEventHandler; onLayout?: (event: LayoutChangeEvent) => void; disableAutoLayout?: boolean; + experimentalMaintainTopContentPosition?: boolean; } class AutoLayoutView extends React.Component { @@ -63,6 +64,9 @@ class AutoLayoutView extends React.Component { listeners.length !== 0 || Boolean(this.props.onBlankAreaEvent) } disableAutoLayout={this.props.disableAutoLayout} + experimentalMaintainTopContentPosition={Boolean( + this.props.experimentalMaintainTopContentPosition + )} > {this.props.children} diff --git a/src/native/auto-layout/AutoLayoutViewNativeComponentProps.ts b/src/native/auto-layout/AutoLayoutViewNativeComponentProps.ts index 008c8f660..2ea467d45 100644 --- a/src/native/auto-layout/AutoLayoutViewNativeComponentProps.ts +++ b/src/native/auto-layout/AutoLayoutViewNativeComponentProps.ts @@ -14,4 +14,5 @@ export interface AutoLayoutViewNativeComponentProps { onBlankAreaEvent: OnBlankAreaEventHandler; enableInstrumentation: boolean; disableAutoLayout?: boolean; + experimentalMaintainTopContentPosition?: boolean; } From 530abe04171573c82cc6538c9228e5e23fc0098f Mon Sep 17 00:00:00 2001 From: Daniel Friyia JR Date: Sat, 25 Feb 2023 11:01:32 -0500 Subject: [PATCH 4/6] [DF] feat(#547): update documentation --- src/FlashList.tsx | 1 + src/FlashListProps.ts | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/FlashList.tsx b/src/FlashList.tsx index 3f4a6fdd4..140fb18a5 100644 --- a/src/FlashList.tsx +++ b/src/FlashList.tsx @@ -505,6 +505,7 @@ class FlashList extends React.PureComponent< }} index={parentProps.index} stableId={ + /* Empty string is used so the list can still render without an extractor */ this.props.keyExtractor?.(parentProps.data, parentProps.index) ?? "" } > diff --git a/src/FlashListProps.ts b/src/FlashListProps.ts index a87fd4c55..31a8d3da9 100644 --- a/src/FlashListProps.ts +++ b/src/FlashListProps.ts @@ -335,8 +335,9 @@ export interface FlashListProps extends ScrollViewProps { /** * 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. + * 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. If in debug mode, you may see flashes if new data + * comes in quickly. If this happens, please confirm you see this in release mode before reporting an issue. */ experimentalMaintainTopContentPosition?: boolean; } From 8d7143c6bceb4a692eedc900349d350e9f880316 Mon Sep 17 00:00:00 2001 From: Daniel Friyia JR Date: Sat, 25 Feb 2023 11:02:45 -0500 Subject: [PATCH 5/6] [DF] feat(#547): final cleanups --- CHANGELOG.md | 3 +++ fixture/src/List.tsx | 43 +++++++------------------------- ios/Sources/AutoLayoutView.swift | 6 ----- src/FlashList.tsx | 6 ++--- 4 files changed, 15 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e363408b5..f8fb3592e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## [Unreleased] + - Fix definition conflicts with previous value - https://github.com/Shopify/flash-list/pull/795 +- Add support for `experimentalMaintainTopContentPosition` + - https://github.com/Shopify/flash-list/issues/547 ## [1.4.2] - 2023-03-20 diff --git a/fixture/src/List.tsx b/fixture/src/List.tsx index ce50e8141..1ce8dd728 100644 --- a/fixture/src/List.tsx +++ b/fixture/src/List.tsx @@ -9,45 +9,27 @@ import { Pressable, LayoutAnimation, StyleSheet, - Button, } from "react-native"; import { FlashList } from "@shopify/flash-list"; -import { useFocusEffect } from "@react-navigation/native"; -import { TouchableOpacity } from "react-native-gesture-handler"; -interface ListItem { - value: number; - type?: string; -} - -let newItemIndexes = 1001; - -const generateArray = (size: number): ListItem[] => { - const arr = new Array(size); +const generateArray = (size: number) => { + const arr = new Array(size); for (let i = 0; i < size; i++) { - arr[i] = { value: i }; + arr[i] = i; } - return arr; }; const List = () => { const [refreshing, setRefreshing] = useState(false); const [data, setData] = useState(generateArray(100)); - const [isLoading, setIsLoading] = useState(false); - const list = useRef | null>(null); - - useFocusEffect( - React.useCallback(() => { - newItemIndexes = 1001; - }, []) - ); + const list = useRef | null>(null); const removeItem = (item: number) => { setData( data.filter((dataItem) => { - return dataItem.value !== item; + return dataItem !== item; }) ); list.current?.prepareForLayoutAnimationRender(); @@ -55,19 +37,12 @@ const List = () => { LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); }; - const renderItem = ({ item }: { item: ListItem }) => { - const backgroundColor = item.value % 2 === 0 ? "#00a1f1" : "#ffbb00"; - - // if (Number(item.value) >= 90 && Number(item.value) <= 99) { - // return ; - // } - // item.value % 2 === 0 - // ? 100 + (item.value > 1000 ? item.value / 10 : item.value) + 1 - // : 200 + (item.value > 1000 ? item.value / 10 : item.value), + const renderItem = ({ item }: { item: number }) => { + const backgroundColor = item % 2 === 0 ? "#00a1f1" : "#ffbb00"; return ( { - removeItem(item.value); + removeItem(item); }} > { height: item % 2 === 0 ? 100 : 200, }} > - Cell Id: {item.value} + Cell Id: {item} ); diff --git a/ios/Sources/AutoLayoutView.swift b/ios/Sources/AutoLayoutView.swift index 58cbc6184..10e523c6c 100644 --- a/ios/Sources/AutoLayoutView.swift +++ b/ios/Sources/AutoLayoutView.swift @@ -51,12 +51,6 @@ import UIKit /// Tracks where first pixel is drawn in the visible window private var lastMinBound: CGFloat = 0 - /// Marks the first Item in the Scroll View - private var firstItemMarker: CellContainer? = nil - - /// The position of the item in the Scroll View after insertion / deletion - private var previousMarkerOffset: CGFloat = -1 - /// State that informs us whether this is the first render private var isInitialRender: Bool = true diff --git a/src/FlashList.tsx b/src/FlashList.tsx index 140fb18a5..766f5d561 100644 --- a/src/FlashList.tsx +++ b/src/FlashList.tsx @@ -160,7 +160,6 @@ class FlashList extends React.PureComponent< prevState: FlashListState ): FlashListState { const newState = { ...prevState }; - if (prevState.numColumns !== nextProps.numColumns) { newState.numColumns = nextProps.numColumns || 1; newState.layoutProvider = FlashList.getLayoutProvider( @@ -172,8 +171,6 @@ class FlashList extends React.PureComponent< newState.numColumns, nextProps ); - // RLV retries to reposition the first visible item on layout provider change. - // It's not required in our case so we're disabling it } if (nextProps.data !== prevState.data) { newState.data = nextProps.data; @@ -188,6 +185,9 @@ class FlashList extends React.PureComponent< newState.extraData = { value: nextProps.extraData }; } newState.renderItem = nextProps.renderItem; + + // RLV retries to reposition the first visible item on layout provider change. + // It's not required in our case so we're disabling it newState.layoutProvider.shouldRefreshWithAnchoring = false; return newState; } From e2d1699b764eea751007885e2f2df2681c287ff5 Mon Sep 17 00:00:00 2001 From: Daniel Friyia JR Date: Wed, 29 Mar 2023 22:23:47 -0400 Subject: [PATCH 6/6] [DF] feat(#547): update podfile.lock --- fixture/ios/Podfile.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/fixture/ios/Podfile.lock b/fixture/ios/Podfile.lock index 3afe187ac..42832f000 100644 --- a/fixture/ios/Podfile.lock +++ b/fixture/ios/Podfile.lock @@ -366,9 +366,9 @@ PODS: - React-Core - SDWebImage (~> 5.11.1) - SDWebImageWebPCoder (~> 0.8.4) - - RNFlashList (1.4.1): + - RNFlashList (1.4.2): - React-Core - - RNFlashList/Tests (1.4.1): + - RNFlashList/Tests (1.4.2): - React-Core - RNGestureHandler (2.5.0): - React-Core @@ -601,7 +601,7 @@ SPEC CHECKSUMS: libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 libwebp: 98a37e597e40bfdb4c911fc98f2c53d0b12d05fc OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c - RCT-Folly: 803a9cfd78114b2ec0f140cfa6fa2a6bafb2d685 + RCT-Folly: 4d8508a426467c48885f1151029bc15fa5d7b3b8 RCTRequired: 0f06b6068f530932d10e1a01a5352fad4eaacb74 RCTTypeSafety: b0ee81f10ef1b7d977605a2b266823dabd565e65 React: 3becd12bd51ea8a43bdde7e09d0f40fba7820e03 @@ -630,7 +630,7 @@ SPEC CHECKSUMS: ReactCommon: 149e2c0acab9bac61378da0db5b2880a1b5ff59b ReactNativePerformanceListsProfiler: b9f7cfe8d08631fbce8e4729d388a5a3f7f562c2 RNFastImage: 1f2cab428712a4baaf78d6169eaec7f622556dd7 - RNFlashList: 8ec7f7454721145fe84566bb9e88bcf58981c9fe + RNFlashList: 7fbca4fc075484a9426f1610d648dbea2de94eb0 RNGestureHandler: bad495418bcbd3ab47017a38d93d290ebd406f50 RNReanimated: 3d1432ce7b6b7fc31f375dcabe5b4585e0634a43 RNScreens: 40a2cb40a02a609938137a1e0acfbf8fc9eebf19