Skip to content

Commit

Permalink
[DF] feat(Shopify#547): experimentalMaintainTopContentPosition work…
Browse files Browse the repository at this point in the history
…s vertically
  • Loading branch information
friyiajr authored and johankasperi committed Jun 13, 2024
1 parent f4fea33 commit 55a7bcc
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
buildConfiguration = "Release"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
Expand Down
42 changes: 33 additions & 9 deletions fixture/src/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,40 +9,64 @@ import {
Pressable,
LayoutAnimation,
StyleSheet,
Button,
} from "react-native";
import { FlashList } from "@shopify/flash-list";
import { useFocusEffect } from "@react-navigation/native";

const generateArray = (size: number) => {
const arr = new Array(size);
interface ListItem {
value: number;
type?: string;
}

let newItemIndexes = 1001;

const generateArray = (size: number): ListItem[] => {
const arr = new Array<ListItem>(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<FlashList<number> | null>(null);
const list = useRef<FlashList<ListItem> | 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();
// after removing the item, we start animation
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 <View />;
// }
// item.value % 2 === 0
// ? 100 + (item.value > 1000 ? item.value / 10 : item.value) + 1
// : 200 + (item.value > 1000 ? item.value / 10 : item.value),
return (
<Pressable
onPress={() => {
removeItem(item);
removeItem(item.value);
}}
>
<View
Expand All @@ -52,7 +76,7 @@ const List = () => {
height: item % 2 === 0 ? 100 : 200,
}}
>
<Text>Cell Id: {item}</Text>
<Text>Cell Id: {item.value}</Text>
</View>
</Pressable>
);
Expand Down
58 changes: 55 additions & 3 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 @@ -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
Expand All @@ -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 }

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
5 changes: 5 additions & 0 deletions ios/Sources/CellContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
1 change: 1 addition & 0 deletions ios/Sources/CellContainerManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
@interface RCT_EXTERN_MODULE(CellContainerManager, RCTViewManager)

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

@end
3 changes: 3 additions & 0 deletions src/FlashList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ class FlashList<T> extends React.PureComponent<
prevState: FlashListState<T>
): FlashListState<T> {
const newState = { ...prevState };

if (prevState.numColumns !== nextProps.numColumns) {
newState.numColumns = nextProps.numColumns || 1;
newState.layoutProvider = FlashList.getLayoutProvider<T>(
Expand Down Expand Up @@ -195,6 +196,7 @@ class FlashList<T> extends React.PureComponent<
newState.extraData = { value: nextProps.extraData };
}
newState.renderItem = nextProps.renderItem;
newState.layoutProvider.shouldRefreshWithAnchoring = false;
return newState;
}

Expand Down Expand Up @@ -511,6 +513,7 @@ class FlashList<T> extends React.PureComponent<
...getCellContainerPlatformStyles(this.props.inverted!!, parentProps),
}}
index={parentProps.index}
type={this.props.keyExtractor?.(parentProps.data, parentProps.index)}
>
<PureComponentWrapper
extendedState={parentProps.extendedState}
Expand Down

0 comments on commit 55a7bcc

Please sign in to comment.