Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix issue with element below the anchor blinking during data loading. #53

Open
wants to merge 34 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
a1a8481
Try to add logic
michbil Jan 5, 2025
c8ea258
hide on unlaid out elements
michbil Jan 5, 2025
23d6076
cleanup
michbil Jan 5, 2025
f4523e0
remove top element blinking issue
michbil Jan 5, 2025
15443ec
refactor - create setAnimated
michbil Jan 5, 2025
f1852f9
fix var naming
michbil Jan 5, 2025
1feb888
remove animated, because it has to many issues for now
mbilenko-florio Jan 16, 2025
6e2a6f2
Merge branch 'main' into fix/top-element-blinking
mbilenko-florio Jan 16, 2025
a4dac40
intermediate
mbilenko-florio Jan 16, 2025
92992dd
Merge remote-tracking branch 'origin/main' into fix/top-element-blinking
mbilenko-florio Jan 17, 2025
be459e2
Adapt shouldDisplayContent
mbilenko-florio Jan 17, 2025
344095e
Use bottom relative position for elements above the anchor
mbilenko-florio Jan 18, 2025
8c75fc8
undo visiblity change
mbilenko-florio Jan 18, 2025
20d9257
Revert sizes
mbilenko-florio Jan 18, 2025
776ff30
Fix initialization
mbilenko-florio Jan 18, 2025
dc7352a
add waitForInitialLayout
mbilenko-florio Jan 18, 2025
728833e
Rename anchoring prop
mbilenko-florio Jan 18, 2025
25428fe
realign sizes
mbilenko-florio Jan 18, 2025
5d46b08
fix
mbilenko-florio Jan 18, 2025
8c650b7
remove console.log
mbilenko-florio Jan 18, 2025
9f0300d
try to separate styles
mbilenko-florio Jan 18, 2025
6492de3
fix container
mbilenko-florio Jan 18, 2025
213ebb1
fix columns
mbilenko-florio Jan 18, 2025
ff757bd
fix containers issue
mbilenko-florio Jan 18, 2025
680eedb
add comments
mbilenko-florio Jan 18, 2025
bd11855
use lean view
mbilenko-florio Jan 18, 2025
566e97c
Merge remote-tracking branch 'origin/main' into fix/top-element-blinking
mbilenko-florio Jan 20, 2025
e4a3ff6
Remove comment, add conditional container
mbilenko-florio Jan 20, 2025
4e56a8b
Merge remote-tracking branch 'origin/main' into fix/top-element-blinking
mbilenko-florio Jan 23, 2025
dbfc729
remove did first measure
mbilenko-florio Jan 23, 2025
35adf4e
revert change
mbilenko-florio Jan 23, 2025
03aa342
remove containervisible
mbilenko-florio Jan 23, 2025
37fc10f
Merge remote-tracking branch 'origin/main' into fix/top-element-blinking
mbilenko-florio Jan 23, 2025
cf2fb0a
Make waitForInitialLayout default to true
mbilenko-florio Jan 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions example/app/bidirectional-infinite-list/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export default function BidirectionalInfiniteList() {
// }, 2000);
// }, []);

const { top, bottom } = useSafeAreaInsets();
const { bottom } = useSafeAreaInsets();

return (
<View style={[StyleSheet.absoluteFill, styles.outerContainer]} key="legendlist">
Expand All @@ -76,7 +76,6 @@ export default function BidirectionalInfiniteList() {
drawDistance={DRAW_DISTANCE}
maintainVisibleContentPosition
recycleItems={true}
ListHeaderComponent={<View style={{ height: top }} />}
ListFooterComponent={<View style={{ height: bottom }} />}
onStartReached={(props) => {
const time = performance.now();
Expand Down
70 changes: 45 additions & 25 deletions src/Container.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import React, { useMemo } from "react";
import { type DimensionValue, type LayoutChangeEvent, type StyleProp, View, type ViewStyle } from "react-native";
import type { DimensionValue, LayoutChangeEvent, StyleProp, ViewStyle } from "react-native";
import { LeanView } from "./LeanView";
import { ANCHORED_POSITION_OUT_OF_VIEW } from "./constants";
import { peek$, use$, useStateContext } from "./state";
import type { AnchoredPosition } from "./types";

export const Container = ({
id,
Expand All @@ -20,27 +23,28 @@ export const Container = ({
ItemSeparatorComponent?: React.ReactNode;
}) => {
const ctx = useStateContext();
const position = use$<number>(`containerPosition${id}`);
const maintainVisibleContentPosition = use$<boolean>("maintainVisibleContentPosition");
const position = use$<AnchoredPosition>(`containerPosition${id}`) || ANCHORED_POSITION_OUT_OF_VIEW;
const column = use$<number>(`containerColumn${id}`) || 0;
const numColumns = use$<number>("numColumns");

const otherAxisPos: DimensionValue | undefined = numColumns > 1 ? `${((column - 1) / numColumns) * 100}%` : 0;
const otherAxisSize: DimensionValue | undefined = numColumns > 1 ? `${(1 / numColumns) * 100}%` : undefined;

const style: StyleProp<ViewStyle> = horizontal
? {
flexDirection: "row",
position: "absolute",
top: otherAxisPos,
bottom: numColumns > 1 ? null : 0,
height: otherAxisSize,
left: position,
left: position.relativeCoordinate,
}
: {
position: "absolute",
left: otherAxisPos,
right: numColumns > 1 ? null : 0,
width: otherAxisSize,
top: position,
top: position.relativeCoordinate,
};

if (waitForInitialLayout) {
Expand All @@ -55,29 +59,45 @@ export const Container = ({

const renderedItem = useMemo(() => itemKey !== undefined && getRenderedItem(itemKey, id), [itemKey, data, extraData]);

const onLayout = (event: LayoutChangeEvent) => {
const key = peek$<string>(ctx, `containerItemKey${id}`);
if (key !== undefined) {
// Round to nearest quater pixel to avoid accumulating rounding errors
const size = Math.floor(event.nativeEvent.layout[horizontal ? "width" : "height"] * 8) / 8;
updateItemSize(id, key, size);

// const otherAxisSize = horizontal ? event.nativeEvent.layout.width : event.nativeEvent.layout.height;
// set$(ctx, "otherAxisSize", Math.max(otherAxisSize, peek$(ctx, "otherAxisSize") || 0));
}
};

const contentFragment = (
<React.Fragment key={recycleItems ? undefined : itemKey}>
{renderedItem}
{renderedItem && ItemSeparatorComponent && itemKey !== lastItemKey && ItemSeparatorComponent}
</React.Fragment>
);

// If maintainVisibleContentPosition is enabled, we need a way items to grow upwards
if (maintainVisibleContentPosition) {
const anchorStyle: StyleProp<ViewStyle> =
position.type === "top"
? { position: "absolute", top: 0, left: 0, right: 0 }
: { position: "absolute", bottom: 0, left: 0, right: 0 };
return (
<LeanView style={style}>
<LeanView style={anchorStyle} onLayout={onLayout}>
{contentFragment}
</LeanView>
</LeanView>
);
}
// Use a reactive View to ensure the container element itself
// is not rendered when style changes, only the style prop.
// This is a big perf boost to do less work rendering.
return (
<View
style={style}
onLayout={(event: LayoutChangeEvent) => {
const key = peek$<string>(ctx, `containerItemKey${id}`);
if (key !== undefined) {
// Round to nearest quater pixel to avoid accumulating rounding errors
const size = Math.floor(event.nativeEvent.layout[horizontal ? "width" : "height"] * 8) / 8;

updateItemSize(id, key, size);

// const otherAxisSize = horizontal ? event.nativeEvent.layout.width : event.nativeEvent.layout.height;
// set$(ctx, "otherAxisSize", Math.max(otherAxisSize, peek$(ctx, "otherAxisSize") || 0));
}
}}
>
<React.Fragment key={recycleItems ? undefined : itemKey}>
{renderedItem}
{renderedItem && ItemSeparatorComponent && itemKey !== lastItemKey && ItemSeparatorComponent}
</React.Fragment>
</View>
<LeanView style={style} onLayout={onLayout}>
{contentFragment}
</LeanView>
);
};
49 changes: 37 additions & 12 deletions src/LegendList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,20 @@ import {
} from "react-native";
import { ListComponent } from "./ListComponent";
import { ScrollAdjustHandler } from "./ScrollAdjustHandler";
import { ANCHORED_POSITION_OUT_OF_VIEW, POSITION_OUT_OF_VIEW } from "./constants";
import { type ListenerType, StateProvider, listen$, peek$, set$, useStateContext } from "./state";
import type { LegendListRecyclingState, LegendListRef, ViewabilityAmountCallback, ViewabilityCallback } from "./types";
import type {
AnchoredPosition,
LegendListRecyclingState,
LegendListRef,
ViewabilityAmountCallback,
ViewabilityCallback,
} from "./types";
import type { InternalState, LegendListProps } from "./types";
import { useInit } from "./useInit";
import { setupViewability, updateViewableItems } from "./viewability";

const DEFAULT_DRAW_DISTANCE = 250;
const POSITION_OUT_OF_VIEW = -10000000;
const DEFAULT_ITEM_SIZE = 100;

export const LegendList: <T>(props: LegendListProps<T> & { ref?: ForwardedRef<LegendListRef> }) => ReactElement =
Expand Down Expand Up @@ -68,6 +74,7 @@ const LegendListInner: <T>(props: LegendListProps<T> & { ref?: ForwardedRef<Lege
onItemSizeChanged,
scrollEventThrottle,
refScrollView,
waitForInitialLayout=true,
extraData,
...rest
} = props;
Expand Down Expand Up @@ -182,6 +189,7 @@ const LegendListInner: <T>(props: LegendListProps<T> & { ref?: ForwardedRef<Lege
}
}
set$(ctx, "scrollAdjust", 0);
set$(ctx, "maintainVisibleContentPosition", maintainVisibleContentPosition);
set$(ctx, "extraData", extraData);
}

Expand Down Expand Up @@ -500,7 +508,7 @@ const LegendListInner: <T>(props: LegendListProps<T> & { ref?: ForwardedRef<Lege
}

const index = state.indexByKey.get(key)!;
const pos = peek$<number>(ctx, `containerPosition${u}`);
const pos = peek$<AnchoredPosition>(ctx, `containerPosition${u}`).top;

if (index < startBuffered || index > endBuffered) {
const distance = Math.abs(pos - top);
Expand All @@ -523,7 +531,7 @@ const LegendListInner: <T>(props: LegendListProps<T> & { ref?: ForwardedRef<Lege
set$(ctx, `containerItemData${containerId}`, data[index]);

// TODO: This may not be necessary as it'll get a new one in the next loop?
set$(ctx, `containerPosition${containerId}`, POSITION_OUT_OF_VIEW);
set$(ctx, `containerPosition${containerId}`, ANCHORED_POSITION_OUT_OF_VIEW);
set$(ctx, `containerColumn${containerId}`, -1);

if (__DEV__ && numContainers > peek$<number>(ctx, "numContainersPooled")) {
Expand Down Expand Up @@ -555,29 +563,45 @@ const LegendListInner: <T>(props: LegendListProps<T> & { ref?: ForwardedRef<Lege
if (itemKey !== id || itemIndex < startBuffered || itemIndex > endBuffered) {
// This is fairly complex because we want to avoid setting container position if it's not even in view
// because it will trigger a render
const prevPos = peek$<number>(ctx, `containerPosition${i}`);
const prevPos = peek$<AnchoredPosition>(ctx, `containerPosition${i}`).top;
const pos = positions.get(id) || 0;
const size = getItemSize(id, itemIndex, data[i]);

if (
(pos + size >= scroll && pos <= scrollBottom) ||
(prevPos + size >= scroll && prevPos <= scrollBottom)
) {
set$(ctx, `containerPosition${i}`, POSITION_OUT_OF_VIEW);
set$(ctx, `containerPosition${i}`, ANCHORED_POSITION_OUT_OF_VIEW);
}
} else {
const pos = positions.get(id) || 0;
const pos: AnchoredPosition = {
type: "top",
relativeCoordinate: positions.get(id) || 0,
top: positions.get(id) || 0,
};
const column = columns.get(id) || 1;
const prevPos = peek$(ctx, `containerPosition${i}`);

// anchor elements to the bottom if element is below anchor
if (maintainVisibleContentPosition && itemIndex < anchorElementIndex) {
const currentRow = Math.floor(itemIndex / numColumnsProp);
const rowHeight = getRowHeight(currentRow);
const elementHeight = getItemSize(id, itemIndex, data[i]);
const diff = rowHeight - elementHeight; // difference between row height and element height
pos.relativeCoordinate = pos.top + getRowHeight(currentRow) - diff;
pos.type = "bottom";
}

const prevPos = peek$<AnchoredPosition>(ctx, `containerPosition${i}`);
const prevColumn = peek$(ctx, `containerColumn${i}`);
const prevData = peek$(ctx, `containerItemData${i}`);

if (pos > POSITION_OUT_OF_VIEW && pos !== prevPos) {
if (pos.relativeCoordinate > POSITION_OUT_OF_VIEW && pos.top !== prevPos.top) {
set$(ctx, `containerPosition${i}`, pos);
}
if (column >= 0 && column !== prevColumn) {
set$(ctx, `containerColumn${i}`, column);
}

if (prevData !== item) {
set$(ctx, `containerItemData${i}`, data[itemIndex]);
}
Expand Down Expand Up @@ -717,7 +741,7 @@ const LegendListInner: <T>(props: LegendListProps<T> & { ref?: ForwardedRef<Lege
const itemKey = peek$<string>(ctx, `containerItemKey${i}`);
if (!keyExtractorProp || (itemKey && state.indexByKey.get(itemKey) === undefined)) {
set$(ctx, `containerItemKey${i}`, undefined);
set$(ctx, `containerPosition${i}`, POSITION_OUT_OF_VIEW);
set$(ctx, `containerPosition${i}`, ANCHORED_POSITION_OUT_OF_VIEW);
set$(ctx, `containerColumn${i}`, -1);
}
}
Expand Down Expand Up @@ -941,7 +965,7 @@ const LegendListInner: <T>(props: LegendListProps<T> & { ref?: ForwardedRef<Lege
const numContainers = Math.ceil((scrollLength + scrollBuffer * 2) / averageItemSize) * numColumnsProp;

for (let i = 0; i < numContainers; i++) {
set$(ctx, `containerPosition${i}`, POSITION_OUT_OF_VIEW);
set$(ctx, `containerPosition${i}`, ANCHORED_POSITION_OUT_OF_VIEW);
set$(ctx, `containerColumn${i}`, -1);
}

Expand All @@ -957,7 +981,7 @@ const LegendListInner: <T>(props: LegendListProps<T> & { ref?: ForwardedRef<Lege
return;
}
const state = refState.current!;
const { sizes, indexByKey, idsInFirstRender, columns, sizesLaidOut } = state;
const { sizes, indexByKey, columns, sizesLaidOut } = state;
const index = indexByKey.get(itemKey)!;
const numColumns = peek$<number>(ctx, "numColumns");

Expand Down Expand Up @@ -1192,6 +1216,7 @@ const LegendListInner: <T>(props: LegendListProps<T> & { ref?: ForwardedRef<Lege
ListEmptyComponent={data.length === 0 ? ListEmptyComponent : undefined}
maintainVisibleContentPosition={maintainVisibleContentPosition}
scrollEventThrottle={scrollEventThrottle ?? (Platform.OS === "web" ? 16 : undefined)}
waitForInitialLayout={waitForInitialLayout}
style={style}
/>
);
Expand Down
1 change: 1 addition & 0 deletions src/ListComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ interface ListComponentProps
onLayout: (event: LayoutChangeEvent) => void;
maintainVisibleContentPosition: boolean;
renderScrollComponent?: (props: ScrollViewProps) => React.ReactElement<ScrollViewProps>;

}

const getComponent = (Component: React.ComponentType<any> | React.ReactElement) => {
Expand Down
10 changes: 8 additions & 2 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import { Platform } from 'react-native';
import type { AnchoredPosition } from './types';

export const USE_CONTENT_INSET = Platform.OS === 'ios';

export const POSITION_OUT_OF_VIEW = -10000000;
export const ANCHORED_POSITION_OUT_OF_VIEW: AnchoredPosition = {
type: "top",
relativeCoordinate: POSITION_OUT_OF_VIEW,
top: POSITION_OUT_OF_VIEW,
};
3 changes: 2 additions & 1 deletion src/state.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ export type ListenerType =
| "stylePaddingTop"
| "scrollAdjust"
| "headerSize"
| "footerSize";
| "footerSize"
| "maintainVisibleContentPosition"
// | "otherAxisSize";

export interface StateContext {
Expand Down
6 changes: 6 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ export type LegendListPropsBase<
extraData?: any;
};

export type AnchoredPosition = {
type: 'top' | 'bottom';
relativeCoordinate: number; // used for display
top: number; // used for calculating the position of the container
}

export type LegendListProps<ItemT> = LegendListPropsBase<ItemT, ComponentProps<typeof ScrollView>>;

export interface InternalState {
Expand Down