-
-
Save march213/fa8c7fda5e940ac475f67d0d597a27ca to your computer and use it in GitHub Desktop.
Discord's FastList, but in TypeScript
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { forEachObjIndexed } from "ramda"; | |
import * as React from "react"; | |
import { | |
Animated, | |
ScrollView, | |
View, | |
ViewStyle, | |
LayoutChangeEvent, | |
NativeScrollEvent, | |
} from "react-native"; | |
type HeaderHeight = number | (() => number); | |
type FooterHeight = number | (() => number); | |
type SectionHeight = number | ((section: number) => number); | |
type RowHeight = number | ((section: number, row?: number) => number); | |
type SectionFooterHeight = number | ((section: number) => number); | |
enum FastListItemType { | |
SPACER = "spacer", | |
HEADER = "header", | |
SECTION = "section", | |
ROW = "row", | |
FOOTER = "footer", | |
SECTION_FOOTER = "footer", | |
} | |
type FastListItem = { | |
type: FastListItemType; | |
key: number; | |
layoutY: number; | |
layoutHeight: number; | |
section: number; | |
row: number; | |
}; | |
interface ScrollEvent { | |
nativeEvent: NativeScrollEvent; | |
} | |
interface FastListComputerProps { | |
headerHeight: HeaderHeight; | |
footerHeight: FooterHeight; | |
sectionHeight: SectionHeight; | |
rowHeight: RowHeight; | |
sectionFooterHeight: SectionFooterHeight; | |
sections: number[]; | |
insetTop: number; | |
insetBottom: number; | |
} | |
export class FastListComputer { | |
headerHeight: HeaderHeight; | |
footerHeight: FooterHeight; | |
sectionHeight: SectionHeight; | |
rowHeight: RowHeight; | |
sectionFooterHeight: SectionFooterHeight; | |
sections: number[]; | |
insetTop: number; | |
insetBottom: number; | |
uniform: boolean; | |
constructor({ | |
headerHeight, | |
footerHeight, | |
sectionHeight, | |
rowHeight, | |
sectionFooterHeight, | |
sections, | |
insetTop, | |
insetBottom, | |
}: FastListComputerProps) { | |
this.headerHeight = headerHeight; | |
this.footerHeight = footerHeight; | |
this.sectionHeight = sectionHeight; | |
this.rowHeight = rowHeight; | |
this.sectionFooterHeight = sectionFooterHeight; | |
this.sections = sections; | |
this.insetTop = insetTop; | |
this.insetBottom = insetBottom; | |
this.uniform = typeof rowHeight === "number"; | |
} | |
getHeightForHeader(): number { | |
const { headerHeight } = this; | |
return typeof headerHeight === "number" ? headerHeight : headerHeight(); | |
} | |
getHeightForFooter(): number { | |
const { footerHeight } = this; | |
return typeof footerHeight === "number" ? footerHeight : footerHeight(); | |
} | |
getHeightForSection(section: number): number { | |
const { sectionHeight } = this; | |
return typeof sectionHeight === "number" | |
? sectionHeight | |
: sectionHeight(section); | |
} | |
getHeightForRow(section: number, row?: number): number { | |
const { rowHeight } = this; | |
return typeof rowHeight === "number" ? rowHeight : rowHeight(section, row); | |
} | |
getHeightForSectionFooter(section: number): number { | |
const { sectionFooterHeight } = this; | |
return typeof sectionFooterHeight === "number" | |
? sectionFooterHeight | |
: sectionFooterHeight(section); | |
} | |
compute( | |
top: number, | |
bottom: number, | |
prevItems: FastListItem[] | |
): { | |
height: number; | |
items: FastListItem[]; | |
} { | |
const { sections } = this; | |
let height = this.insetTop; | |
let spacerHeight = height; | |
let items = [] as FastListItem[]; | |
const recycler = new FastListItemRecycler(prevItems); | |
function isVisible(itemHeight: number): boolean { | |
const prevHeight = height; | |
height += itemHeight; | |
if (height < top || prevHeight > bottom) { | |
spacerHeight += itemHeight; | |
return false; | |
} else { | |
return true; | |
} | |
} | |
function isBelowVisibility(itemHeight: number): boolean { | |
if (height > bottom) { | |
spacerHeight += itemHeight; | |
return false; | |
} else { | |
return true; | |
} | |
} | |
function push(item: FastListItem) { | |
if (spacerHeight > 0) { | |
items.push( | |
recycler.get( | |
FastListItemType.SPACER, | |
item.layoutY - spacerHeight, | |
spacerHeight, | |
item.section, | |
item.row | |
) | |
); | |
spacerHeight = 0; | |
} | |
items.push(item); | |
} | |
let layoutY; | |
const headerHeight = this.getHeightForHeader(); | |
if (headerHeight > 0) { | |
layoutY = height; | |
if (isVisible(headerHeight)) { | |
push(recycler.get(FastListItemType.HEADER, layoutY, headerHeight)); | |
} | |
} | |
for (let section = 0; section < sections.length; section++) { | |
const rows = sections[section]; | |
if (rows === 0) { | |
continue; | |
} | |
const sectionHeight = this.getHeightForSection(section); | |
layoutY = height; | |
height += sectionHeight; | |
// Replace previous spacers and sections, so we only render section headers | |
// whose children are visible + previous section (required for sticky header animation). | |
if ( | |
section > 1 && | |
items.length > 0 && | |
items[items.length - 1].type === FastListItemType.SECTION | |
) { | |
const spacerLayoutHeight = items.reduce((totalHeight, item, i) => { | |
if (i !== items.length - 1) { | |
return totalHeight + item.layoutHeight; | |
} | |
return totalHeight; | |
}, 0); | |
const prevSection = items[items.length - 1]; | |
const spacer = recycler.get( | |
FastListItemType.SPACER, | |
0, | |
spacerLayoutHeight, | |
prevSection.section, | |
0 | |
); | |
items = [spacer, prevSection]; | |
} | |
if (isBelowVisibility(sectionHeight)) { | |
push( | |
recycler.get( | |
FastListItemType.SECTION, | |
layoutY, | |
sectionHeight, | |
section | |
) | |
); | |
} | |
if (this.uniform) { | |
const rowHeight = this.getHeightForRow(section); | |
for (let row = 0; row < rows; row++) { | |
layoutY = height; | |
if (isVisible(rowHeight)) { | |
push( | |
recycler.get( | |
FastListItemType.ROW, | |
layoutY, | |
rowHeight, | |
section, | |
row | |
) | |
); | |
} | |
} | |
} else { | |
for (let row = 0; row < rows; row++) { | |
const rowHeight = this.getHeightForRow(section, row); | |
layoutY = height; | |
if (isVisible(rowHeight)) { | |
push( | |
recycler.get( | |
FastListItemType.ROW, | |
layoutY, | |
rowHeight, | |
section, | |
row | |
) | |
); | |
} | |
} | |
} | |
const sectionFooterHeight = this.getHeightForSectionFooter(section); | |
if (sectionFooterHeight > 0) { | |
layoutY = height; | |
if (isVisible(sectionFooterHeight)) { | |
push( | |
recycler.get( | |
FastListItemType.SECTION_FOOTER, | |
layoutY, | |
sectionFooterHeight, | |
section | |
) | |
); | |
} | |
} | |
} | |
const footerHeight = this.getHeightForFooter(); | |
if (footerHeight > 0) { | |
layoutY = height; | |
if (isVisible(footerHeight)) { | |
push(recycler.get(FastListItemType.FOOTER, layoutY, footerHeight)); | |
} | |
} | |
height += this.insetBottom; | |
spacerHeight += this.insetBottom; | |
if (spacerHeight > 0) { | |
items.push( | |
recycler.get( | |
FastListItemType.SPACER, | |
height - spacerHeight, | |
spacerHeight, | |
sections.length | |
) | |
); | |
} | |
recycler.fill(); | |
return { | |
height, | |
items, | |
}; | |
} | |
computeScrollPosition( | |
targetSection: number, | |
targetRow: number | |
): { | |
scrollTop: number; | |
sectionHeight: number; | |
} { | |
const { sections, insetTop } = this; | |
let scrollTop = insetTop + this.getHeightForHeader(); | |
let section = 0; | |
let foundRow = false; | |
while (section <= targetSection) { | |
const rows = sections[section]; | |
if (rows === 0) { | |
section += 1; | |
continue; | |
} | |
scrollTop += this.getHeightForSection(section); | |
if (this.uniform) { | |
const uniformHeight = this.getHeightForRow(section); | |
if (section === targetSection) { | |
scrollTop += uniformHeight * targetRow; | |
foundRow = true; | |
} else { | |
scrollTop += uniformHeight * rows; | |
} | |
} else { | |
for (let row = 0; row < rows; row++) { | |
if ( | |
section < targetSection || | |
(section === targetSection && row < targetRow) | |
) { | |
scrollTop += this.getHeightForRow(section, row); | |
} else if (section === targetSection && row === targetRow) { | |
foundRow = true; | |
break; | |
} | |
} | |
} | |
if (!foundRow) { | |
scrollTop += this.getHeightForSectionFooter(section); | |
} | |
section += 1; | |
} | |
return { | |
scrollTop, | |
sectionHeight: this.getHeightForSection(targetSection), | |
}; | |
} | |
} | |
/** | |
* FastListItemRecycler is used to recycle FastListItem objects between | |
* recomputations of the list. By doing this we ensure that components | |
* maintain their keys and avoid reallocations. | |
*/ | |
export class FastListItemRecycler { | |
static _LAST_KEY: number = 0; | |
items: Partial< | |
{ | |
[key in FastListItemType]: Partial<{ | |
[key: string]: FastListItem; | |
}>; | |
} | |
> = {}; | |
pendingItems: Partial<{ | |
[key: string]: FastListItem[]; | |
}> = {}; | |
constructor(items: FastListItem[]) { | |
items.forEach((item) => { | |
const { type, section, row } = item; | |
const [itemsForType] = this.itemsForType(type); | |
itemsForType[`${type}:${section}:${row}`] = item; | |
}); | |
} | |
itemsForType( | |
type: FastListItemType | |
): [ | |
{ | |
[key: string]: FastListItem; | |
}, | |
FastListItem[] | |
] { | |
return [ | |
this.items[type] || (this.items[type] = {}), | |
this.pendingItems[type] || (this.pendingItems[type] = []), | |
]; | |
} | |
get( | |
type: FastListItemType, | |
layoutY: number, | |
layoutHeight: number, | |
section: number = 0, | |
row: number = 0 | |
): FastListItem { | |
const [items, pendingItems] = this.itemsForType(type); | |
return this._get( | |
type, | |
layoutY, | |
layoutHeight, | |
section, | |
row, | |
items, | |
pendingItems | |
); | |
} | |
_get( | |
type: FastListItemType, | |
layoutY: number, | |
layoutHeight: number, | |
section: number, | |
row: number, | |
items: { | |
[key: string]: FastListItem; | |
}, | |
pendingItems: FastListItem[] | |
) { | |
const itemKey = `${type}:${section}:${row}`; | |
let item = items[itemKey]; | |
if (item == null) { | |
item = { type, key: -1, layoutY, layoutHeight, section, row }; | |
pendingItems.push(item); | |
} else { | |
item.layoutY = layoutY; | |
item.layoutHeight = layoutHeight; | |
delete items[itemKey]; | |
} | |
return item; | |
} | |
fill() { | |
forEachObjIndexed((type) => { | |
const [items, pendingItems] = this.itemsForType(type); | |
this._fill(items, pendingItems); | |
}, FastListItemType); | |
} | |
_fill( | |
items: { | |
[key: string]: FastListItem; | |
}, | |
pendingItems: FastListItem[] | |
) { | |
let index = 0; | |
forEachObjIndexed(({ key }) => { | |
const item = pendingItems[index]; | |
if (item == null) { | |
return false; | |
} | |
item.key = key; | |
index++; | |
}, items); | |
for (; index < pendingItems.length; index++) { | |
pendingItems[index].key = ++FastListItemRecycler._LAST_KEY; | |
} | |
pendingItems.length = 0; | |
} | |
} | |
const FastListSectionRenderer = ({ | |
layoutY, | |
layoutHeight, | |
nextSectionLayoutY, | |
scrollTopValue, | |
children, | |
}: { | |
layoutY: number; | |
layoutHeight: number; | |
nextSectionLayoutY?: number; | |
scrollTopValue: Animated.Value; | |
children: React.ReactElement<{ style?: ViewStyle }>; | |
}) => { | |
const inputRange: number[] = [-1, 0]; | |
const outputRange: number[] = [0, 0]; | |
inputRange.push(layoutY); | |
outputRange.push(0); | |
const collisionPoint = (nextSectionLayoutY || 0) - layoutHeight; | |
if (collisionPoint >= layoutY) { | |
inputRange.push(collisionPoint, collisionPoint + 1); | |
outputRange.push(collisionPoint - layoutY, collisionPoint - layoutY); | |
} else { | |
inputRange.push(layoutY + 1); | |
outputRange.push(1); | |
} | |
const translateY = scrollTopValue.interpolate({ | |
inputRange, | |
outputRange, | |
}); | |
const child = React.Children.only(children); | |
return ( | |
<Animated.View | |
style={[ | |
React.isValidElement(child) && child.props.style | |
? child.props.style | |
: undefined, | |
{ | |
zIndex: 10, | |
height: layoutHeight, | |
transform: [{ translateY }], | |
}, | |
]} | |
> | |
{React.isValidElement(child) && | |
React.cloneElement(child, { | |
style: { flex: 1 }, | |
})} | |
</Animated.View> | |
); | |
}; | |
const FastListItemRenderer = ({ | |
layoutHeight: height, | |
children, | |
}: { | |
layoutHeight: number; | |
children?: React.ReactNode; | |
}) => <View style={{ height }}>{children}</View>; | |
export interface FastListProps { | |
renderActionSheetScrollViewWrapper?: ( | |
wrapper: React.ReactNode | |
) => React.ReactNode; | |
actionSheetScrollRef?: { current: React.ReactNode | null | undefined }; | |
onScroll?: (event: ScrollEvent) => any; | |
onScrollEnd?: (event: ScrollEvent) => any; | |
onLayout?: (event: LayoutChangeEvent) => any; | |
renderHeader: () => React.ReactElement<any> | null | undefined; | |
renderFooter: () => React.ReactElement<any> | null | undefined; | |
renderSection: ( | |
section: number | |
) => React.ReactElement<any> | null | undefined; | |
renderRow: ( | |
section: number, | |
row: number | |
) => React.ReactElement<any> | null | undefined; | |
renderSectionFooter: ( | |
section: number | |
) => React.ReactElement<any> | null | undefined; | |
renderAccessory?: (list: FastList) => React.ReactNode; | |
renderEmpty?: () => React.ReactElement<any> | null | undefined; | |
headerHeight: HeaderHeight; | |
footerHeight: FooterHeight; | |
sectionHeight: SectionHeight; | |
sectionFooterHeight: SectionFooterHeight; | |
rowHeight: RowHeight; | |
sections: number[]; | |
insetTop: number; | |
insetBottom: number; | |
scrollTopValue?: Animated.Value; | |
contentInset: { | |
top?: number; | |
left?: number; | |
right?: number; | |
bottom?: number; | |
}; | |
} | |
interface FastListState { | |
batchSize: number; | |
blockStart: number; | |
blockEnd: number; | |
height?: number; | |
items?: FastListItem[]; | |
} | |
const computeBlock = ( | |
containerHeight: number, | |
scrollTop: number | |
): FastListState => { | |
if (containerHeight === 0) { | |
return { | |
batchSize: 0, | |
blockStart: 0, | |
blockEnd: 0, | |
}; | |
} | |
const batchSize = Math.ceil(containerHeight / 2); | |
const blockNumber = Math.ceil(scrollTop / batchSize); | |
const blockStart = batchSize * blockNumber; | |
const blockEnd = blockStart + batchSize; | |
return { batchSize, blockStart, blockEnd }; | |
}; | |
function getFastListState( | |
{ | |
headerHeight, | |
footerHeight, | |
sectionHeight, | |
rowHeight, | |
sectionFooterHeight, | |
sections, | |
insetTop, | |
insetBottom, | |
}: FastListProps, | |
{ batchSize, blockStart, blockEnd, items: prevItems }: FastListState | |
): FastListState { | |
if (batchSize === 0) { | |
return { | |
batchSize, | |
blockStart, | |
blockEnd, | |
height: insetTop + insetBottom, | |
items: [], | |
}; | |
} | |
const computer = new FastListComputer({ | |
headerHeight, | |
footerHeight, | |
sectionHeight, | |
rowHeight, | |
sectionFooterHeight, | |
sections, | |
insetTop, | |
insetBottom, | |
}); | |
return { | |
batchSize, | |
blockStart, | |
blockEnd, | |
...computer.compute( | |
blockStart - batchSize, | |
blockEnd + batchSize, | |
prevItems || [] | |
), | |
}; | |
} | |
export class FastList extends React.PureComponent< | |
FastListProps, | |
FastListState | |
> { | |
static defaultProps = { | |
isFastList: true, | |
renderHeader: () => null, | |
renderFooter: () => null, | |
renderSection: () => null, | |
renderSectionFooter: () => null, | |
headerHeight: 0, | |
footerHeight: 0, | |
sectionHeight: 0, | |
sectionFooterHeight: 0, | |
insetTop: 0, | |
insetBottom: 0, | |
contentInset: { top: 0, right: 0, left: 0, bottom: 0 }, | |
}; | |
containerHeight: number = 0; | |
scrollTop: number = 0; | |
scrollTopValue: Animated.Value = | |
this.props.scrollTopValue || new Animated.Value(0); | |
scrollTopValueAttachment: { detach: () => void } | null | undefined; | |
scrollView: { current: ScrollView | null | undefined } = React.createRef(); | |
state = getFastListState( | |
this.props, | |
computeBlock(this.containerHeight, this.scrollTop) | |
); | |
static getDerivedStateFromProps(props: FastListProps, state: FastListState) { | |
return getFastListState(props, state); | |
} | |
getItems(): FastListItem[] { | |
return this.state.items || []; | |
} | |
isVisible = (layoutY: number): boolean => { | |
return ( | |
layoutY >= this.scrollTop && | |
layoutY <= this.scrollTop + this.containerHeight | |
); | |
}; | |
scrollToLocation = ( | |
section: number, | |
row: number, | |
animated: boolean = true | |
) => { | |
const scrollView = this.scrollView.current; | |
if (scrollView != null) { | |
const { | |
headerHeight, | |
footerHeight, | |
sectionHeight, | |
rowHeight, | |
sectionFooterHeight, | |
sections, | |
insetTop, | |
insetBottom, | |
} = this.props; | |
const computer = new FastListComputer({ | |
headerHeight, | |
footerHeight, | |
sectionHeight, | |
sectionFooterHeight, | |
rowHeight, | |
sections, | |
insetTop, | |
insetBottom, | |
}); | |
const { | |
scrollTop: layoutY, | |
sectionHeight: layoutHeight, | |
} = computer.computeScrollPosition(section, row); | |
scrollView.scrollTo({ | |
x: 0, | |
y: Math.max(0, layoutY - layoutHeight), | |
animated, | |
}); | |
} | |
}; | |
handleScroll = (event: ScrollEvent) => { | |
const { nativeEvent } = event; | |
const { contentInset } = this.props; | |
this.containerHeight = | |
nativeEvent.layoutMeasurement.height - | |
(contentInset.top || 0) - | |
(contentInset.bottom || 0); | |
this.scrollTop = Math.min( | |
Math.max(0, nativeEvent.contentOffset.y), | |
nativeEvent.contentSize.height - this.containerHeight | |
); | |
const nextState = computeBlock(this.containerHeight, this.scrollTop); | |
if ( | |
nextState.batchSize !== this.state.batchSize || | |
nextState.blockStart !== this.state.blockStart || | |
nextState.blockEnd !== this.state.blockEnd | |
) { | |
this.setState(nextState); | |
} | |
const { onScroll } = this.props; | |
if (onScroll != null) { | |
onScroll(event); | |
} | |
}; | |
handleLayout = (event: LayoutChangeEvent) => { | |
const { nativeEvent } = event; | |
const { contentInset } = this.props; | |
this.containerHeight = | |
nativeEvent.layout.height - | |
(contentInset.top || 0) - | |
(contentInset.bottom || 0); | |
const nextState = computeBlock(this.containerHeight, this.scrollTop); | |
if ( | |
nextState.batchSize !== this.state.batchSize || | |
nextState.blockStart !== this.state.blockStart || | |
nextState.blockEnd !== this.state.blockEnd | |
) { | |
this.setState(nextState); | |
} | |
const { onLayout } = this.props; | |
if (onLayout != null) { | |
onLayout(event); | |
} | |
}; | |
/** | |
* FastList only re-renders when items change which which does not happen with | |
* every scroll event. Since an accessory might depend on scroll position this | |
* ensures the accessory at least re-renders when scrolling ends | |
*/ | |
handleScrollEnd = (event: ScrollEvent) => { | |
const { renderAccessory, onScrollEnd } = this.props; | |
if (renderAccessory != null) { | |
this.forceUpdate(); | |
} | |
if (onScrollEnd) { | |
onScrollEnd(event); | |
} | |
}; | |
renderItems() { | |
const { | |
renderHeader, | |
renderFooter, | |
renderSection, | |
renderRow, | |
renderSectionFooter, | |
renderEmpty, | |
} = this.props; | |
const { items = [] } = this.state; | |
if (renderEmpty != null && this.isEmpty()) { | |
return renderEmpty(); | |
} | |
const sectionLayoutYs = [] as number[]; | |
items.forEach(({ type, layoutY }) => { | |
if (type === FastListItemType.SECTION) { | |
sectionLayoutYs.push(layoutY); | |
} | |
}); | |
const children = [] as JSX.Element[]; | |
items.forEach(({ type, key, layoutY, layoutHeight, section, row }) => { | |
switch (type) { | |
case FastListItemType.SPACER: { | |
const child = ( | |
<FastListItemRenderer key={key} layoutHeight={layoutHeight} /> | |
); | |
children.push(child); | |
break; | |
} | |
case FastListItemType.HEADER: { | |
const child = renderHeader(); | |
if (child != null) { | |
children.push( | |
<FastListItemRenderer key={key} layoutHeight={layoutHeight}> | |
{child} | |
</FastListItemRenderer> | |
); | |
} | |
break; | |
} | |
case FastListItemType.FOOTER: { | |
const child = renderFooter(); | |
if (child != null) { | |
children.push( | |
<FastListItemRenderer key={key} layoutHeight={layoutHeight}> | |
{child} | |
</FastListItemRenderer> | |
); | |
} | |
break; | |
} | |
case FastListItemType.SECTION: { | |
sectionLayoutYs.shift(); | |
const child = renderSection(section); | |
if (child != null) { | |
children.push( | |
<FastListSectionRenderer | |
key={key} | |
layoutY={layoutY} | |
layoutHeight={layoutHeight} | |
nextSectionLayoutY={sectionLayoutYs[0]} | |
scrollTopValue={this.scrollTopValue} | |
> | |
{child} | |
</FastListSectionRenderer> | |
); | |
} | |
break; | |
} | |
case FastListItemType.ROW: { | |
const child = renderRow(section, row); | |
if (child != null) { | |
children.push( | |
<FastListItemRenderer key={key} layoutHeight={layoutHeight}> | |
{child} | |
</FastListItemRenderer> | |
); | |
} | |
break; | |
} | |
case FastListItemType.SECTION_FOOTER: { | |
const child = renderSectionFooter(section); | |
if (child != null) { | |
children.push( | |
<FastListItemRenderer key={key} layoutHeight={layoutHeight}> | |
{child} | |
</FastListItemRenderer> | |
); | |
} | |
break; | |
} | |
} | |
}); | |
return children; | |
} | |
componentDidMount() { | |
if (this.scrollView.current != null) { | |
// @ts-ignore: Types for React Native doesn't include attachNativeEvent | |
this.scrollTopValueAttachment = Animated.attachNativeEvent( | |
this.scrollView.current, | |
"onScroll", | |
[{ nativeEvent: { contentOffset: { y: this.scrollTopValue } } }] | |
); | |
} | |
} | |
componentDidUpdate(prevProps: FastListProps) { | |
if (prevProps.scrollTopValue !== this.props.scrollTopValue) { | |
throw new Error("scrollTopValue cannot changed after mounting"); | |
} | |
} | |
componentWillUnmount() { | |
if (this.scrollTopValueAttachment != null) { | |
this.scrollTopValueAttachment.detach(); | |
} | |
} | |
isEmpty = () => { | |
const { sections } = this.props; | |
const length = sections.reduce((total, rowLength) => { | |
return total + rowLength; | |
}, 0); | |
return length === 0; | |
}; | |
render() { | |
const { | |
/* eslint-disable no-unused-vars */ | |
renderSection, | |
renderRow, | |
renderAccessory, | |
sectionHeight, | |
rowHeight, | |
sections, | |
insetTop, | |
insetBottom, | |
actionSheetScrollRef, | |
renderActionSheetScrollViewWrapper, | |
renderEmpty, | |
/* eslint-enable no-unused-vars */ | |
...props | |
} = this.props; | |
// what is this?? | |
// well! in order to support continuous scrolling of a scrollview/list/whatever in an action sheet, we need | |
// to wrap the scrollview in a NativeViewGestureHandler. This wrapper does that thing that need do | |
const wrapper = renderActionSheetScrollViewWrapper || ((val) => val); | |
const scrollView = wrapper( | |
<ScrollView | |
{...props} | |
ref={(ref) => { | |
this.scrollView.current = ref; | |
if (actionSheetScrollRef) { | |
actionSheetScrollRef.current = ref; | |
} | |
}} | |
removeClippedSubviews={false} | |
scrollEventThrottle={16} | |
onScroll={this.handleScroll} | |
onLayout={this.handleLayout} | |
onMomentumScrollEnd={this.handleScrollEnd} | |
onScrollEndDrag={this.handleScrollEnd} | |
> | |
{this.renderItems()} | |
</ScrollView> | |
); | |
return ( | |
<React.Fragment> | |
{scrollView} | |
{renderAccessory != null ? renderAccessory(this) : null} | |
</React.Fragment> | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment