Skip to content

Instantly share code, notes, and snippets.

@Ryiski
Forked from derekstavis/FastList.tsx
Created July 11, 2023 12:38
Show Gist options
  • Save Ryiski/54bc4b017760d8a02cdbe3698fe29305 to your computer and use it in GitHub Desktop.
Save Ryiski/54bc4b017760d8a02cdbe3698fe29305 to your computer and use it in GitHub Desktop.

Revisions

  1. @derekstavis derekstavis revised this gist Oct 27, 2020. 1 changed file with 62 additions and 53 deletions.
    115 changes: 62 additions & 53 deletions FastList.tsx
    Original 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";
    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,
    type: FastListItemType;
    key: number;
    layoutY: number;
    layoutHeight: number;
    section: number;
    row: number;
    };

    interface ScrollEvent {
    nativeEvent: NativeScrollEvent
    nativeEvent: NativeScrollEvent;
    }

    interface FastListComputerProps {
    @@ -105,7 +112,7 @@ export class FastListComputer {
    compute(
    top: number,
    bottom: number,
    prevItems: FastListItem[],
    prevItems: FastListItem[]
    ): {
    height: number;
    items: FastListItem[];
    @@ -146,8 +153,8 @@ export class FastListComputer {
    item.layoutY - spacerHeight,
    spacerHeight,
    item.section,
    item.row,
    ),
    item.row
    )
    );
    spacerHeight = 0;
    }
    @@ -195,7 +202,7 @@ export class FastListComputer {
    0,
    spacerLayoutHeight,
    prevSection.section,
    0,
    0
    );
    items = [spacer, prevSection];
    }
    @@ -206,8 +213,8 @@ export class FastListComputer {
    FastListItemType.SECTION,
    layoutY,
    sectionHeight,
    section,
    ),
    section
    )
    );
    }

    @@ -222,8 +229,8 @@ export class FastListComputer {
    layoutY,
    rowHeight,
    section,
    row,
    ),
    row
    )
    );
    }
    }
    @@ -238,8 +245,8 @@ export class FastListComputer {
    layoutY,
    rowHeight,
    section,
    row,
    ),
    row
    )
    );
    }
    }
    @@ -254,8 +261,8 @@ export class FastListComputer {
    FastListItemType.SECTION_FOOTER,
    layoutY,
    sectionFooterHeight,
    section,
    ),
    section
    )
    );
    }
    }
    @@ -278,8 +285,8 @@ export class FastListComputer {
    FastListItemType.SPACER,
    height - spacerHeight,
    spacerHeight,
    sections.length,
    ),
    sections.length
    )
    );
    }

    @@ -293,7 +300,7 @@ export class FastListComputer {

    computeScrollPosition(
    targetSection: number,
    targetRow: 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;
    }>;
    }> = {};
    items: Partial<
    {
    [key in FastListItemType]: Partial<{
    [key: string]: FastListItem;
    }>;
    }
    > = {};
    pendingItems: Partial<{
    [key: string]: FastListItem[];
    }> = {};
    @@ -370,7 +379,7 @@ export class FastListItemRecycler {
    }

    itemsForType(
    type: FastListItemType,
    type: FastListItemType
    ): [
    {
    [key: string]: FastListItem;
    @@ -388,7 +397,7 @@ export class FastListItemRecycler {
    layoutY: number,
    layoutHeight: number,
    section: number = 0,
    row: 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,
    pendingItems
    );
    }

    @@ -411,7 +420,7 @@ export class FastListItemRecycler {
    items: {
    [key: string]: FastListItem;
    },
    pendingItems: 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[],
    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,
    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,
    section: number
    ) => React.ReactElement<any> | null | undefined;
    renderRow: (
    section: number,
    row: number,
    row: number
    ) => React.ReactElement<any> | null | undefined;
    renderSectionFooter: (
    section: number,
    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,
    scrollTop: number
    ): FastListState => {
    if (containerHeight === 0) {
    return {
    @@ -597,7 +606,7 @@ function getFastListState(
    insetTop,
    insetBottom,
    }: FastListProps,
    { batchSize, blockStart, blockEnd, items: prevItems }: FastListState,
    { batchSize, blockStart, blockEnd, items: prevItems }: FastListState
    ): FastListState {
    if (batchSize === 0) {
    return {
    @@ -626,7 +635,7 @@ function getFastListState(
    ...computer.compute(
    blockStart - batchSize,
    blockEnd + batchSize,
    prevItems || [],
    prevItems || []
    ),
    };
    }
    @@ -659,7 +668,7 @@ export class FastList extends React.PureComponent<

    state = getFastListState(
    this.props,
    computeBlock(this.containerHeight, this.scrollTop),
    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,
    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,
    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>,
    </FastListItemRenderer>
    );
    }
    break;
    @@ -833,7 +842,7 @@ export class FastList extends React.PureComponent<
    children.push(
    <FastListItemRenderer key={key} layoutHeight={layoutHeight}>
    {child}
    </FastListItemRenderer>,
    </FastListItemRenderer>
    );
    }
    break;
    @@ -851,7 +860,7 @@ export class FastList extends React.PureComponent<
    scrollTopValue={this.scrollTopValue}
    >
    {child}
    </FastListSectionRenderer>,
    </FastListSectionRenderer>
    );
    }
    break;
    @@ -862,7 +871,7 @@ export class FastList extends React.PureComponent<
    children.push(
    <FastListItemRenderer key={key} layoutHeight={layoutHeight}>
    {child}
    </FastListItemRenderer>,
    </FastListItemRenderer>
    );
    }
    break;
    @@ -873,7 +882,7 @@ export class FastList extends React.PureComponent<
    children.push(
    <FastListItemRenderer key={key} layoutHeight={layoutHeight}>
    {child}
    </FastListItemRenderer>,
    </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 } } }],
    [{ nativeEvent: { contentOffset: { y: this.scrollTopValue } } }]
    );
    }
    }
    @@ -954,7 +963,7 @@ export class FastList extends React.PureComponent<
    onScrollEndDrag={this.handleScrollEnd}
    >
    {this.renderItems()}
    </ScrollView>,
    </ScrollView>
    );
    return (
    <React.Fragment>
    @@ -963,4 +972,4 @@ export class FastList extends React.PureComponent<
    </React.Fragment>
    );
    }
    }
    }
  2. @derekstavis derekstavis revised this gist Oct 26, 2020. 1 changed file with 59 additions and 345 deletions.
    404 changes: 59 additions & 345 deletions FastList.tsx
    Original file line number Diff line number Diff line change
    @@ -1,321 +1,35 @@
    import lodash from "lodash";
    import { forEachObjIndexed } from "ramda";
    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;
    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",
    }

    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;
    }
    type FastListItem = {
    type: FastListItemType,
    key: number,
    layoutY: number,
    layoutHeight: number,
    section: number,
    row: number,
    };

    return {
    scrollTop,
    sectionHeight: this.getHeightForSection(targetSection),
    };
    }
    interface ScrollEvent {
    nativeEvent: NativeScrollEvent
    }

    interface FastListComputerProps {
    headerHeight: HeaderHeight;
    footerHeight: FooterHeight;
    @@ -428,7 +142,7 @@ export class FastListComputer {
    if (spacerHeight > 0) {
    items.push(
    recycler.get(
    FastListItemTypes.SPACER,
    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(FastListItemTypes.HEADER, layoutY, 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 === FastListItemTypes.SECTION
    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(
    FastListItemTypes.SPACER,
    FastListItemType.SPACER,
    0,
    spacerLayoutHeight,
    prevSection.section,
    @@ -489,7 +203,7 @@ export class FastListComputer {
    if (isBelowVisibility(sectionHeight)) {
    push(
    recycler.get(
    FastListItemTypes.SECTION,
    FastListItemType.SECTION,
    layoutY,
    sectionHeight,
    section,
    @@ -504,7 +218,7 @@ export class FastListComputer {
    if (isVisible(rowHeight)) {
    push(
    recycler.get(
    FastListItemTypes.ROW,
    FastListItemType.ROW,
    layoutY,
    rowHeight,
    section,
    @@ -520,7 +234,7 @@ export class FastListComputer {
    if (isVisible(rowHeight)) {
    push(
    recycler.get(
    FastListItemTypes.ROW,
    FastListItemType.ROW,
    layoutY,
    rowHeight,
    section,
    @@ -537,7 +251,7 @@ export class FastListComputer {
    if (isVisible(sectionFooterHeight)) {
    push(
    recycler.get(
    FastListItemTypes.SECTION_FOOTER,
    FastListItemType.SECTION_FOOTER,
    layoutY,
    sectionFooterHeight,
    section,
    @@ -551,7 +265,7 @@ export class FastListComputer {
    if (footerHeight > 0) {
    layoutY = height;
    if (isVisible(footerHeight)) {
    push(recycler.get(FastListItemTypes.FOOTER, layoutY, footerHeight));
    push(recycler.get(FastListItemType.FOOTER, layoutY, footerHeight));
    }
    }

    @@ -561,7 +275,7 @@ export class FastListComputer {
    if (spacerHeight > 0) {
    items.push(
    recycler.get(
    FastListItemTypes.SPACER,
    FastListItemType.SPACER,
    height - spacerHeight,
    spacerHeight,
    sections.length,
    @@ -638,14 +352,14 @@ export class FastListComputer {
    export class FastListItemRecycler {
    static _LAST_KEY: number = 0;

    items: {
    [key: number]: {
    items: Partial<{
    [key in FastListItemType]: Partial<{
    [key: string]: FastListItem;
    };
    } = {};
    pendingItems: {
    [key: number]: FastListItem[];
    } = {};
    }>;
    }> = {};
    pendingItems: Partial<{
    [key: string]: FastListItem[];
    }> = {};

    constructor(items: FastListItem[]) {
    items.forEach((item) => {
    @@ -713,10 +427,10 @@ export class FastListItemRecycler {
    }

    fill() {
    lodash.forEach(FastListItemTypes, (type) => {
    forEachObjIndexed((type) => {
    const [items, pendingItems] = this.itemsForType(type);
    this._fill(items, pendingItems);
    });
    }, FastListItemType);
    }

    _fill(
    @@ -727,14 +441,14 @@ export class FastListItemRecycler {
    ) {
    let index = 0;

    lodash.forEach(items, ({ key }) => {
    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: LayoutEvent) => 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 default class FastList extends React.PureComponent<
    export class FastList extends React.PureComponent<
    FastListProps,
    FastListState
    > {
    @@ -1030,7 +744,7 @@ export default class FastList extends React.PureComponent<
    }
    };

    handleLayout = (event: LayoutEvent) => {
    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 === FastListItemTypes.SECTION) {
    if (type === FastListItemType.SECTION) {
    sectionLayoutYs.push(layoutY);
    }
    });

    const children = [] as JSX.Element[];
    items.forEach(({ type, key, layoutY, layoutHeight, section, row }) => {
    switch (type) {
    case FastListItemTypes.SPACER: {
    case FastListItemType.SPACER: {
    const child = (
    <FastListItemRenderer key={key} layoutHeight={layoutHeight} />
    );
    children.push(child);
    break;
    }
    case FastListItemTypes.HEADER: {
    case FastListItemType.HEADER: {
    const child = renderHeader();
    if (child != null) {
    children.push(
    @@ -1113,7 +827,7 @@ export default class FastList extends React.PureComponent<
    }
    break;
    }
    case FastListItemTypes.FOOTER: {
    case FastListItemType.FOOTER: {
    const child = renderFooter();
    if (child != null) {
    children.push(
    @@ -1124,7 +838,7 @@ export default class FastList extends React.PureComponent<
    }
    break;
    }
    case FastListItemTypes.SECTION: {
    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 FastListItemTypes.ROW: {
    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 FastListItemTypes.SECTION_FOOTER: {
    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>
    );
    }
    }
    }
  3. @derekstavis derekstavis revised this gist Dec 28, 2019. 2 changed files with 1252 additions and 865 deletions.
    865 changes: 0 additions & 865 deletions FastList.js
    Original file line number Diff line number Diff line change
    @@ -1,865 +0,0 @@
    // @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>
    );
    }
    }
    1,252 changes: 1,252 additions & 0 deletions FastList.tsx
    Original 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>
    );
    }
    }
  4. @vishnevskiy vishnevskiy created this gist Nov 8, 2019.
    865 changes: 865 additions & 0 deletions FastList.js
    Original 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>
    );
    }
    }