UNPKG

react-native-big-list

Version:

High-performance, virtualized list for React Native. Efficiently renders large datasets with recycler API for smooth scrolling and low memory usage. Ideal for fast, scalable, customizable lists on Android, iOS, and web.

403 lines (382 loc) 10.8 kB
import { BigListItemType } from "./BigListItem"; import BigListItemRecycler from "./BigListItemRecycler"; import { isNumeric } from "./utils"; export default class BigListProcessor { /** * Constructor. * @param {ScrollView} scrollView * @param {array[]|object|null|undefined} sections * @param {array[]|object|null|undefined} sectionsData * @param {number|function|null|undefined} headerHeight * @param {number|function|null|undefined} footerHeight * @param {number|function|null|undefined} sectionHeaderHeight * @param {number|function|null|undefined} itemHeight * @param {number|function|null|undefined} sectionFooterHeight * @param {number|function|null|undefined} insetTop * @param {number|function|null|undefined} insetBottom * @param {number|null|undefined} numColumns */ constructor({ scrollView, sections, sectionsData, headerHeight, footerHeight, sectionHeaderHeight, itemHeight, sectionFooterHeight, insetTop, insetBottom, numColumns, }) { this.headerHeight = headerHeight; this.footerHeight = footerHeight; this.sectionHeaderHeight = sectionHeaderHeight; this.itemHeight = itemHeight; this.sectionFooterHeight = sectionFooterHeight; this.sections = sections; this.sectionsData = sectionsData; this.insetTop = insetTop; this.insetBottom = insetBottom; this.uniform = isNumeric(itemHeight); this.scrollView = scrollView; this.numColumns = numColumns || 1; } /** * Get item height. * @returns {number|*} */ getItemHeight(section, index) { const { itemHeight, sectionsData } = this; const sectionData = sectionsData && sectionsData[section] ? sectionsData[section] : null; return isNumeric(itemHeight) ? Number(itemHeight) : itemHeight(section, index, sectionData); } /** * Get header height. * @returns {number|*} */ getHeaderHeight() { const { headerHeight } = this; return isNumeric(headerHeight) ? Number(headerHeight) : headerHeight(); } /** * Get footer height. * @returns {number|*} */ getFooterHeight() { const { footerHeight } = this; return isNumeric(footerHeight) ? Number(footerHeight) : footerHeight(); } /** * Get section height. * @returns {number|*} */ getSectionHeaderHeight(section) { const { sectionHeaderHeight } = this; return isNumeric(sectionHeaderHeight) ? Number(sectionHeaderHeight) : sectionHeaderHeight(section); } /** * Get section footer height. * @returns {number|*} */ getSectionFooterHeight(section) { const { sectionFooterHeight } = this; return isNumeric(sectionFooterHeight) ? Number(sectionFooterHeight) : sectionFooterHeight(section); } /** * Process list items. * @param {number} top * @param {number} bottom * @param {array} prevItems * @returns {{items: [], height: *}} */ process(top, bottom, prevItems) { const { sections } = this; const items = []; const recycler = new BigListItemRecycler(prevItems); let position; let counter = -1; // Counter of items per row pushed let height = this.insetTop; let spacerHeight = height; /** * The width of the row is the entire line. * @param {object} item * @returns {boolean} */ const isFullRow = (item) => { // Only items can be rendered with column format, so all others are full row return item.type !== BigListItemType.ITEM; }; /** * Is visible below. * @param {object} item * @returns {boolean} */ const isVisibleBelow = (item) => { const { height: itemHeight } = item; counter = -1; if (height > bottom) { spacerHeight += itemHeight; return false; } else { return true; } }; /** * Is the item visible. * @param {object} item * @param {bool} force * @returns {boolean} */ const isVisible = (item, force = false) => { // Check section headers visibility below if (item.type === BigListItemType.SECTION_HEADER) { return isVisibleBelow(item); } // Dimensions const { height: itemHeight } = item; const fullRow = isFullRow(item); const prevHeight = height; // Increase or reset counter counter = fullRow ? -1 : counter + 1; if (fullRow || counter % this.numColumns === 0) { height += itemHeight; } // Check if is visible if (force || (height > top && prevHeight < bottom)) { return true; } else { if (fullRow || counter % this.numColumns === 0) { spacerHeight += itemHeight; } return false; } }; /** * Get recycled views and push items. * @param {object} itemsArray */ const push = (...itemsArray) => { itemsArray.forEach((item) => { items.push(recycler.get(item)); }); }; /** * Push spacer. * @param {object} item */ const pushSpacer = (item) => { if (spacerHeight > 0) { push({ type: BigListItemType.SPACER, position: item.position - spacerHeight, height: spacerHeight, section: item.section, index: item.index, }); spacerHeight = 0; } }; /** * Push the item when is visible. * @param {object} item * @param {bool} force */ const pushItem = (item, force = false) => { if (isVisible(item, force)) { pushSpacer(item); push(item); } }; /** * Calculate spacer height. */ const getSpacerHeight = () => { let itemsCounter = -1; return items.reduce((totalHeight, item, i) => { if (i !== items.length - 1) { const fullRow = isFullRow(item); itemsCounter = fullRow ? 0 : itemsCounter + 1; if (fullRow || itemsCounter % this.numColumns === 0) { return totalHeight + item.height; } } return totalHeight; }, 0); }; // Header const headerHeight = this.getHeaderHeight(); if (headerHeight > 0) { position = height; pushItem( { type: BigListItemType.HEADER, position: position, height: headerHeight, }, true, ); } // Sections for (let section = 0; section < sections.length; section++) { const rows = sections[section]; if (rows === 0) { continue; } // Section Header const sectionHeaderHeight = this.getSectionHeaderHeight(section); position = height; height += sectionHeaderHeight; if ( section > 1 && items.length > 0 && items[items.length - 1].type === BigListItemType.SECTION_HEADER ) { // Top Spacer const initialSpacerHeight = getSpacerHeight(); const prevSection = items[items.length - 1]; items.splice(0, items.length); push( { type: BigListItemType.HEADER, position: position, height: headerHeight, }, { type: BigListItemType.SPACER, position: 0, height: initialSpacerHeight - headerHeight, section: prevSection.section, index: 0, }, prevSection, ); } pushItem({ type: BigListItemType.SECTION_HEADER, position: position, height: sectionHeaderHeight, section: section, }); // Items let itemHeight = this.getItemHeight(section); for (let index = 0; index < rows; index++) { if (!this.uniform) { itemHeight = this.getItemHeight(section, index); } position = height; pushItem({ type: BigListItemType.ITEM, position: position, height: itemHeight, section: section, index: index, }); } // Section Footer const sectionFooterHeight = this.getSectionFooterHeight(section); if (sectionFooterHeight > 0) { position = height; pushItem({ type: BigListItemType.SECTION_FOOTER, position: position, height: sectionFooterHeight, section: section, }); } } // Footer const footerHeight = this.getFooterHeight(); if (footerHeight > 0) { position = height; pushItem( { type: BigListItemType.FOOTER, position: position, height: footerHeight, }, true, ); } // Bottom Spacer height += this.insetBottom; spacerHeight += this.insetBottom; if (spacerHeight > 0) { push({ type: BigListItemType.SPACER, position: height - spacerHeight, height: spacerHeight, section: sections.length, }); } recycler.fill(); return { height, items, }; } /** * Scroll to position. * @param {int} targetSection * @param {int} targetIndex * @param {boolean} animated */ scrollToPosition(targetSection, targetIndex, animated) { const { sections, insetTop } = this; // Header + inset let scrollTop = insetTop + this.getHeaderHeight(); let section = 0; let foundIndex = false; while (section <= targetSection) { const rows = Math.ceil(sections[section] / this.numColumns); if (rows === 0) { section += 1; continue; } // Section header scrollTop += this.getSectionHeaderHeight(section); // Items if (this.uniform) { const uniformHeight = this.getItemHeight(section); if (section === targetSection) { scrollTop += uniformHeight * Math.ceil(targetIndex / this.numColumns); foundIndex = true; } else { scrollTop += uniformHeight * rows; } } else { for (let index = 0; index < rows; index++) { if ( section < targetSection || (section === targetSection && index < targetIndex) ) { scrollTop += this.getItemHeight( section, Math.ceil(index / this.numColumns), ); } else if (section === targetSection && index === targetIndex) { foundIndex = true; break; } } } // Section footer if (!foundIndex) { scrollTop += this.getSectionFooterHeight(section); } section += 1; } this.scrollView.scrollTo({ x: 0, y: Math.max(0, scrollTop - this.getSectionHeaderHeight(targetSection)), animated, }); return true; } }