-
-
Save vishnevskiy/f4ba74adf5cf1d269b860fab86e8feef to your computer and use it in GitHub Desktop.
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
// @flow | |
import * as React from 'react'; | |
// $FlowFixMe | |
import {Animated, View, ScrollView} from 'react-native'; | |
import lodash from 'lodash'; | |
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); | |
export const FastListItemTypes = { | |
SPACER: 0, | |
HEADER: 1, | |
FOOTER: 2, | |
SECTION: 3, | |
ROW: 4, | |
SECTION_FOOTER: 5, | |
}; | |
type FastListItemType = $Values<typeof FastListItemTypes>; | |
export type FastListItem = { | |
type: FastListItemType, | |
key: number, | |
layoutY: number, | |
layoutHeight: number, | |
section: number, | |
row: number, | |
... | |
}; | |
export type ScrollEvent = { | |
nativeEvent: $ReadOnly<{| | |
contentInset: $ReadOnly<{| | |
bottom: number, | |
left: number, | |
right: number, | |
top: number, | |
|}>, | |
contentOffset: $ReadOnly<{| | |
y: number, | |
x: number, | |
|}>, | |
contentSize: $ReadOnly<{| | |
height: number, | |
width: number, | |
|}>, | |
layoutMeasurement: $ReadOnly<{| | |
height: number, | |
width: number, | |
|}>, | |
targetContentOffset?: $ReadOnly<{| | |
y: number, | |
x: number, | |
|}>, | |
velocity?: $ReadOnly<{| | |
y: number, | |
x: number, | |
|}>, | |
zoomScale?: number, | |
responderIgnoreScroll?: boolean, | |
|}>, | |
... | |
}; | |
export type LayoutEvent = { | |
nativeEvent: $ReadOnly<{| | |
layout: $ReadOnly<{| | |
x: number, | |
y: number, | |
width: number, | |
height: number, | |
|}>, | |
|}>, | |
... | |
}; | |
/** | |
* 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. | |
*/ | |
class FastListItemRecycler { | |
static _LAST_KEY: number = 0; | |
_items: {[FastListItemType]: {[string]: FastListItem, ...}, ...} = {}; | |
_pendingItems: {[FastListItemType]: Array<FastListItem>, ...} = {}; | |
constructor(items: Array<FastListItem>) { | |
items.forEach(item => { | |
const {type, section, row} = item; | |
const [items] = this._itemsForType(type); | |
items[`${type}:${section}:${row}`] = item; | |
}); | |
} | |
_itemsForType(type: FastListItemType): [{[string]: FastListItem, ...}, Array<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: {[string]: FastListItem, ...}, | |
pendingItems: Array<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() { | |
lodash.forEach(FastListItemTypes, type => { | |
const [items, pendingItems] = this._itemsForType(type); | |
this._fill(items, pendingItems); | |
}); | |
} | |
_fill(items: {[string]: FastListItem, ...}, pendingItems: Array<FastListItem>) { | |
let index = 0; | |
lodash.forEach(items, ({key}) => { | |
const item = pendingItems[index]; | |
if (item == null) { | |
return false; | |
} | |
item.key = key; | |
index++; | |
}); | |
for (; index < pendingItems.length; index++) { | |
pendingItems[index].key = ++FastListItemRecycler._LAST_KEY; | |
} | |
pendingItems.length = 0; | |
} | |
} | |
type FastListComputerProps = {| | |
headerHeight: HeaderHeight, | |
footerHeight: FooterHeight, | |
sectionHeight: SectionHeight, | |
rowHeight: RowHeight, | |
sectionFooterHeight: SectionFooterHeight, | |
sections: Array<number>, | |
insetTop: number, | |
insetBottom: number, | |
|}; | |
class FastListComputer { | |
headerHeight: HeaderHeight; | |
footerHeight: FooterHeight; | |
sectionHeight: SectionHeight; | |
rowHeight: RowHeight; | |
sectionFooterHeight: SectionFooterHeight; | |
sections: Array<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: Array<FastListItem> | |
): { | |
height: number, | |
items: Array<FastListItem>, | |
... | |
} { | |
const {sections} = this; | |
let height = this.insetTop; | |
let spacerHeight = height; | |
let items = []; | |
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(FastListItemTypes.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(FastListItemTypes.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 === FastListItemTypes.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(FastListItemTypes.SPACER, 0, spacerLayoutHeight, prevSection.section, 0); | |
items = [spacer, prevSection]; | |
} | |
if (isBelowVisibility(sectionHeight)) { | |
push(recycler.get(FastListItemTypes.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(FastListItemTypes.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(FastListItemTypes.ROW, layoutY, rowHeight, section, row)); | |
} | |
} | |
} | |
const sectionFooterHeight = this.getHeightForSectionFooter(section); | |
if (sectionFooterHeight > 0) { | |
layoutY = height; | |
if (isVisible(sectionFooterHeight)) { | |
push(recycler.get(FastListItemTypes.SECTION_FOOTER, layoutY, sectionFooterHeight, section)); | |
} | |
} | |
} | |
const footerHeight = this.getHeightForFooter(); | |
if (footerHeight > 0) { | |
layoutY = height; | |
if (isVisible(footerHeight)) { | |
push(recycler.get(FastListItemTypes.FOOTER, layoutY, footerHeight)); | |
} | |
} | |
height += this.insetBottom; | |
spacerHeight += this.insetBottom; | |
if (spacerHeight > 0) { | |
items.push(recycler.get(FastListItemTypes.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), | |
}; | |
} | |
} | |
const FastListSectionRenderer = ({ | |
layoutY, | |
layoutHeight, | |
nextSectionLayoutY, | |
scrollTopValue, | |
children, | |
}: { | |
layoutY: number, | |
layoutHeight: number, | |
nextSectionLayoutY?: number, | |
scrollTopValue: Animated.Value, | |
children: React.Node, | |
... | |
}): React.Node => { | |
const inputRange: Array<number> = [-1, 0]; | |
const outputRange: Array<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={[ | |
child.props.style, | |
{ | |
zIndex: 10, | |
height: layoutHeight, | |
transform: [{translateY}], | |
}, | |
]}> | |
{React.cloneElement(child, { | |
style: {flex: 1}, | |
})} | |
</Animated.View> | |
); | |
}; | |
const FastListItemRenderer = ({ | |
layoutHeight: height, | |
children, | |
}: { | |
layoutHeight: number, | |
children?: React.Node, | |
... | |
}): React.Node => <View style={{height}}>{children}</View>; | |
export type FastListProps = { | |
renderActionSheetScrollViewWrapper?: React.Node => React.Node, | |
actionSheetScrollRef?: {current: ?React.Node, ...}, | |
onScroll?: (event: ScrollEvent) => any, | |
onScrollEnd?: (event: ScrollEvent) => any, | |
onLayout?: (event: LayoutEvent) => any, | |
renderHeader: () => ?React.Element<any>, | |
renderFooter: () => ?React.Element<any>, | |
renderSection: (section: number) => ?React.Element<any>, | |
renderRow: (section: number, row: number) => ?React.Element<any>, | |
renderSectionFooter: (section: number) => ?React.Element<any>, | |
renderAccessory?: (list: FastList) => React.Node, | |
renderEmpty?: () => ?React.Element<any>, | |
headerHeight: HeaderHeight, | |
footerHeight: FooterHeight, | |
sectionHeight: SectionHeight, | |
sectionFooterHeight: SectionFooterHeight, | |
rowHeight: RowHeight, | |
sections: Array<number>, | |
insetTop: number, | |
insetBottom: number, | |
scrollTopValue?: Animated.Value, | |
contentInset: { | |
top?: number, | |
left?: number, | |
right?: number, | |
bottom?: number, | |
... | |
}, | |
... | |
}; | |
type FastListState = { | |
batchSize: number, | |
blockStart: number, | |
blockEnd: number, | |
height: number, | |
items: Array<FastListItem>, | |
... | |
}; | |
function computeBlock(containerHeight: number, scrollTop: number): $Shape<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}: $Shape<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 default 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, ...}; | |
scrollView: {current: ?ScrollView, ...} = React.createRef(); | |
state = getFastListState(this.props, computeBlock(this.containerHeight, this.scrollTop)); | |
static getDerivedStateFromProps(props: FastListProps, state: FastListState) { | |
return getFastListState(props, state); | |
} | |
getItems(): Array<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: LayoutEvent) => { | |
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(); | |
} | |
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 = []; | |
items.forEach(({type, layoutY}) => { | |
if (type === FastListItemTypes.SECTION) { | |
sectionLayoutYs.push(layoutY); | |
} | |
}); | |
const children = []; | |
items.forEach(({type, key, layoutY, layoutHeight, section, row}) => { | |
switch (type) { | |
case FastListItemTypes.SPACER: { | |
children.push(<FastListItemRenderer key={key} layoutHeight={layoutHeight} />); | |
break; | |
} | |
case FastListItemTypes.HEADER: { | |
const child = renderHeader(); | |
if (child != null) { | |
children.push( | |
<FastListItemRenderer key={key} layoutHeight={layoutHeight}> | |
{child} | |
</FastListItemRenderer> | |
); | |
} | |
break; | |
} | |
case FastListItemTypes.FOOTER: { | |
const child = renderFooter(); | |
if (child != null) { | |
children.push( | |
<FastListItemRenderer key={key} layoutHeight={layoutHeight}> | |
{child} | |
</FastListItemRenderer> | |
); | |
} | |
break; | |
} | |
case FastListItemTypes.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 FastListItemTypes.ROW: { | |
const child = renderRow(section, row); | |
if (child != null) { | |
children.push( | |
<FastListItemRenderer key={key} layoutHeight={layoutHeight}> | |
{child} | |
</FastListItemRenderer> | |
); | |
} | |
break; | |
} | |
case FastListItemTypes.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) { | |
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((length, rowLength) => { | |
return length + 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> | |
); | |
} | |
} |
Does anybody know what is
$Values
? Thanks
That's a Flow construct, it's the same as T[keyof T] in typescript
Get my fork if you're looking for a TypeScript variant:
https://gist.github.com/derekstavis/d8f6a00e14a4259224aafafab14ffd3e
I have since moved to FlashList instead. Much, much better performance.
Get my fork if you're looking for a TypeScript variant: https://gist.github.com/derekstavis/d8f6a00e14a4259224aafafab14ffd3e
I have since moved to FlashList instead. Much, much better performance.
Yeah, it's faster and easier to use
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Does anybody know what is
$Values
? Thanks