-
-
Save Ryiski/54bc4b017760d8a02cdbe3698fe29305 to your computer and use it in GitHub Desktop.
Revisions
-
derekstavis revised this gist
Oct 27, 2020 . 1 changed file with 62 additions and 53 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -1,6 +1,13 @@ 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); @@ -18,16 +25,16 @@ enum FastListItemType { } type FastListItem = { type: FastListItemType; key: number; layoutY: number; layoutHeight: number; section: number; row: number; }; interface ScrollEvent { nativeEvent: NativeScrollEvent; } interface FastListComputerProps { @@ -105,7 +112,7 @@ export class FastListComputer { compute( top: number, bottom: number, prevItems: FastListItem[] ): { height: number; items: FastListItem[]; @@ -146,8 +153,8 @@ export class FastListComputer { item.layoutY - spacerHeight, spacerHeight, item.section, item.row ) ); spacerHeight = 0; } @@ -195,7 +202,7 @@ export class FastListComputer { 0, spacerLayoutHeight, prevSection.section, 0 ); items = [spacer, prevSection]; } @@ -206,8 +213,8 @@ export class FastListComputer { FastListItemType.SECTION, layoutY, sectionHeight, section ) ); } @@ -222,8 +229,8 @@ export class FastListComputer { layoutY, rowHeight, section, row ) ); } } @@ -238,8 +245,8 @@ export class FastListComputer { layoutY, rowHeight, section, row ) ); } } @@ -254,8 +261,8 @@ export class FastListComputer { FastListItemType.SECTION_FOOTER, layoutY, sectionFooterHeight, section ) ); } } @@ -278,8 +285,8 @@ export class FastListComputer { FastListItemType.SPACER, height - spacerHeight, spacerHeight, sections.length ) ); } @@ -293,7 +300,7 @@ export class FastListComputer { computeScrollPosition( targetSection: number, targetRow: number ): { scrollTop: number; sectionHeight: number; @@ -343,7 +350,7 @@ export class FastListComputer { }; } } /** * FastListItemRecycler is used to recycle FastListItem objects between * recomputations of the list. By doing this we ensure that components @@ -352,11 +359,13 @@ export class FastListComputer { export class FastListItemRecycler { static _LAST_KEY: number = 0; items: Partial< { [key in FastListItemType]: Partial<{ [key: string]: FastListItem; }>; } > = {}; pendingItems: Partial<{ [key: string]: FastListItem[]; }> = {}; @@ -370,7 +379,7 @@ export class FastListItemRecycler { } itemsForType( type: FastListItemType ): [ { [key: string]: FastListItem; @@ -388,7 +397,7 @@ export class FastListItemRecycler { layoutY: number, layoutHeight: number, section: number = 0, row: number = 0 ): FastListItem { const [items, pendingItems] = this.itemsForType(type); return this._get( @@ -398,7 +407,7 @@ export class FastListItemRecycler { section, row, items, pendingItems ); } @@ -411,7 +420,7 @@ export class FastListItemRecycler { items: { [key: string]: FastListItem; }, pendingItems: FastListItem[] ) { const itemKey = `${type}:${section}:${row}`; let item = items[itemKey]; @@ -437,7 +446,7 @@ export class FastListItemRecycler { items: { [key: string]: FastListItem; }, pendingItems: FastListItem[] ) { let index = 0; @@ -457,7 +466,7 @@ export class FastListItemRecycler { pendingItems.length = 0; } } const FastListSectionRenderer = ({ layoutY, layoutHeight, @@ -523,7 +532,7 @@ const FastListItemRenderer = ({ export interface FastListProps { renderActionSheetScrollViewWrapper?: ( wrapper: React.ReactNode ) => React.ReactNode; actionSheetScrollRef?: { current: React.ReactNode | null | undefined }; onScroll?: (event: ScrollEvent) => any; @@ -532,14 +541,14 @@ export interface FastListProps { 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; @@ -570,7 +579,7 @@ interface FastListState { const computeBlock = ( containerHeight: number, scrollTop: number ): FastListState => { if (containerHeight === 0) { return { @@ -597,7 +606,7 @@ function getFastListState( insetTop, insetBottom, }: FastListProps, { batchSize, blockStart, blockEnd, items: prevItems }: FastListState ): FastListState { if (batchSize === 0) { return { @@ -626,7 +635,7 @@ function getFastListState( ...computer.compute( blockStart - batchSize, blockEnd + batchSize, prevItems || [] ), }; } @@ -659,7 +668,7 @@ export class FastList extends React.PureComponent< state = getFastListState( this.props, computeBlock(this.containerHeight, this.scrollTop) ); static getDerivedStateFromProps(props: FastListProps, state: FastListState) { @@ -680,7 +689,7 @@ export class FastList extends React.PureComponent< scrollToLocation = ( section: number, row: number, animated: boolean = true ) => { const scrollView = this.scrollView.current; if (scrollView != null) { @@ -726,7 +735,7 @@ export class FastList extends React.PureComponent< (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); @@ -822,7 +831,7 @@ export class FastList extends React.PureComponent< children.push( <FastListItemRenderer key={key} layoutHeight={layoutHeight}> {child} </FastListItemRenderer> ); } break; @@ -833,7 +842,7 @@ export class FastList extends React.PureComponent< children.push( <FastListItemRenderer key={key} layoutHeight={layoutHeight}> {child} </FastListItemRenderer> ); } break; @@ -851,7 +860,7 @@ export class FastList extends React.PureComponent< scrollTopValue={this.scrollTopValue} > {child} </FastListSectionRenderer> ); } break; @@ -862,7 +871,7 @@ export class FastList extends React.PureComponent< children.push( <FastListItemRenderer key={key} layoutHeight={layoutHeight}> {child} </FastListItemRenderer> ); } break; @@ -873,7 +882,7 @@ export class FastList extends React.PureComponent< children.push( <FastListItemRenderer key={key} layoutHeight={layoutHeight}> {child} </FastListItemRenderer> ); } break; @@ -890,7 +899,7 @@ export class FastList extends React.PureComponent< this.scrollTopValueAttachment = Animated.attachNativeEvent( this.scrollView.current, "onScroll", [{ nativeEvent: { contentOffset: { y: this.scrollTopValue } } }] ); } } @@ -954,7 +963,7 @@ export class FastList extends React.PureComponent< onScrollEndDrag={this.handleScrollEnd} > {this.renderItems()} </ScrollView> ); return ( <React.Fragment> @@ -963,4 +972,4 @@ export class FastList extends React.PureComponent< </React.Fragment> ); } } -
derekstavis revised this gist
Oct 26, 2020 . 1 changed file with 59 additions and 345 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -1,321 +1,35 @@ 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; @@ -428,7 +142,7 @@ export class FastListComputer { if (spacerHeight > 0) { items.push( recycler.get( FastListItemType.SPACER, item.layoutY - spacerHeight, spacerHeight, item.section, @@ -447,7 +161,7 @@ export class FastListComputer { if (headerHeight > 0) { layoutY = height; if (isVisible(headerHeight)) { push(recycler.get(FastListItemType.HEADER, layoutY, headerHeight)); } } @@ -467,7 +181,7 @@ export class FastListComputer { 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) { @@ -477,7 +191,7 @@ export class FastListComputer { }, 0); const prevSection = items[items.length - 1]; const spacer = recycler.get( FastListItemType.SPACER, 0, spacerLayoutHeight, prevSection.section, @@ -489,7 +203,7 @@ export class FastListComputer { if (isBelowVisibility(sectionHeight)) { push( recycler.get( FastListItemType.SECTION, layoutY, sectionHeight, section, @@ -504,7 +218,7 @@ export class FastListComputer { if (isVisible(rowHeight)) { push( recycler.get( FastListItemType.ROW, layoutY, rowHeight, section, @@ -520,7 +234,7 @@ export class FastListComputer { if (isVisible(rowHeight)) { push( recycler.get( FastListItemType.ROW, layoutY, rowHeight, section, @@ -537,7 +251,7 @@ export class FastListComputer { if (isVisible(sectionFooterHeight)) { push( recycler.get( FastListItemType.SECTION_FOOTER, layoutY, sectionFooterHeight, section, @@ -551,7 +265,7 @@ export class FastListComputer { if (footerHeight > 0) { layoutY = height; if (isVisible(footerHeight)) { push(recycler.get(FastListItemType.FOOTER, layoutY, footerHeight)); } } @@ -561,7 +275,7 @@ export class FastListComputer { if (spacerHeight > 0) { items.push( recycler.get( FastListItemType.SPACER, height - spacerHeight, spacerHeight, sections.length, @@ -638,14 +352,14 @@ export class FastListComputer { 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) => { @@ -713,10 +427,10 @@ export class FastListItemRecycler { } fill() { forEachObjIndexed((type) => { const [items, pendingItems] = this.itemsForType(type); this._fill(items, pendingItems); }, FastListItemType); } _fill( @@ -727,14 +441,14 @@ export class FastListItemRecycler { ) { 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; @@ -814,7 +528,7 @@ export interface FastListProps { 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: ( @@ -917,7 +631,7 @@ function getFastListState( }; } export class FastList extends React.PureComponent< FastListProps, FastListState > { @@ -1030,7 +744,7 @@ export default class FastList extends React.PureComponent< } }; handleLayout = (event: LayoutChangeEvent) => { const { nativeEvent } = event; const { contentInset } = this.props; @@ -1087,22 +801,22 @@ export default class FastList extends React.PureComponent< 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( @@ -1113,7 +827,7 @@ export default class FastList extends React.PureComponent< } break; } case FastListItemType.FOOTER: { const child = renderFooter(); if (child != null) { children.push( @@ -1124,7 +838,7 @@ export default class FastList extends React.PureComponent< } break; } case FastListItemType.SECTION: { sectionLayoutYs.shift(); const child = renderSection(section); if (child != null) { @@ -1142,7 +856,7 @@ export default class FastList extends React.PureComponent< } break; } case FastListItemType.ROW: { const child = renderRow(section, row); if (child != null) { children.push( @@ -1153,7 +867,7 @@ export default class FastList extends React.PureComponent< } break; } case FastListItemType.SECTION_FOOTER: { const child = renderSectionFooter(section); if (child != null) { children.push( @@ -1249,4 +963,4 @@ export default class FastList extends React.PureComponent< </React.Fragment> ); } } -
derekstavis revised this gist
Dec 28, 2019 . 2 changed files with 1252 additions and 865 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -1,865 +0,0 @@ This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,1252 @@ import lodash from "lodash"; import * as React from "react"; import { Animated, ScrollView, View, ViewStyle } from "react-native"; 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( 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), }; } } 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( 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), }; } } /** * 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: { [key: number]: { [key: string]: FastListItem; }; } = {}; pendingItems: { [key: number]: 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() { lodash.forEach(FastListItemTypes, (type) => { const [items, pendingItems] = this.itemsForType(type); this._fill(items, pendingItems); }); } _fill( items: { [key: string]: FastListItem; }, pendingItems: 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; } } 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: LayoutEvent) => 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 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 } | 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: 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(); } 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 === FastListItemTypes.SECTION) { sectionLayoutYs.push(layoutY); } }); const children = [] as JSX.Element[]; items.forEach(({ type, key, layoutY, layoutHeight, section, row }) => { switch (type) { case FastListItemTypes.SPACER: { const child = ( <FastListItemRenderer key={key} layoutHeight={layoutHeight} /> ); children.push(child); 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) { // @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> ); } } -
vishnevskiy created this gist
Nov 8, 2019 .There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,865 @@ // @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> ); } }