/** * Created by ananya.chandra on 20/09/18. */ import * as React from "react"; import { Animated, StyleProp, ViewStyle } from "react-native"; import { Layout } from "../layoutmanager/LayoutManager"; import { Dimension } from "../dependencies/LayoutProvider"; import RecyclerListViewExceptions from "../exceptions/RecyclerListViewExceptions"; import CustomError from "../exceptions/CustomError"; import { ComponentCompat } from "../../utils/ComponentCompat"; import { WindowCorrection } from "../ViewabilityTracker"; export enum StickyType { HEADER, FOOTER, } export interface StickyObjectProps { stickyIndices: number[] | undefined; getLayoutForIndex: (index: number) => Layout | undefined; getDataForIndex: (index: number) => any; getLayoutTypeForIndex: (index: number) => string | number; getExtendedState: () => object | undefined; getRLVRenderedSize: () => Dimension | undefined; getContentDimension: () => Dimension | undefined; getRowRenderer: () => ((type: string | number, data: any, index: number, extendedState?: object) => JSX.Element | JSX.Element[] | null); overrideRowRenderer?: (type: string | number | undefined, data: any, index: number, extendedState?: object) => JSX.Element | JSX.Element[] | null; renderContainer?: ((rowContent: JSX.Element, index: number, extendState?: object) => JSX.Element | null); getWindowCorrection?: () => WindowCorrection; } export default abstract class StickyObject

extends ComponentCompat

{ protected stickyType: StickyType = StickyType.HEADER; protected stickyTypeMultiplier: number = 1; protected stickyVisiblity: boolean = false; protected containerPosition: StyleProp; protected currentIndex: number = 0; protected currentStickyIndex: number = 0; protected visibleIndices: number[] = []; protected bounceScrolling: boolean = false; private _previousLayout: Layout | undefined; private _previousHeight: number | undefined; private _nextLayout: Layout | undefined; private _nextY: number | undefined; private _nextHeight: number | undefined; private _currentLayout: Layout | undefined; private _currentY: number | undefined; private _currentHeight: number | undefined; private _nextYd: number | undefined; private _currentYd: number | undefined; private _scrollableHeight: number | undefined; private _scrollableWidth: number | undefined; private _windowBound: number | undefined; private _stickyViewOffset: Animated.Value = new Animated.Value(0); private _previousStickyIndex: number = 0; private _nextStickyIndex: number = 0; private _firstCompute: boolean = true; private _smallestVisibleIndex: number = 0; private _largestVisibleIndex: number = 0; private _offsetY: number = 0; private _windowCorrection: WindowCorrection = { startCorrection: 0, endCorrection: 0, windowShift: 0, }; constructor(props: P, context?: any) { super(props, context); } public componentWillReceivePropsCompat(newProps: StickyObjectProps): void { this._updateDimensionParams(); this.calculateVisibleStickyIndex(newProps.stickyIndices, this._smallestVisibleIndex, this._largestVisibleIndex, this._offsetY, this._windowBound); this._computeLayouts(newProps.stickyIndices); this.stickyViewVisible(this.stickyVisiblity, false); } public renderCompat(): JSX.Element | null { // Add the container style if renderContainer is undefined const containerStyle = [{ transform: [{ translateY: this._stickyViewOffset }] }, (!this.props.renderContainer && [{ position: "absolute", width: this._scrollableWidth }, this.containerPosition])]; const content = ( {this.stickyVisiblity ? this._renderSticky() : null} ); if (this.props.renderContainer) { const _extendedState: any = this.props.getExtendedState(); return this.props.renderContainer(content, this.currentStickyIndex, _extendedState); } else { return (content); } } public onVisibleIndicesChanged(all: number[]): void { if (this._firstCompute) { this.initStickyParams(); this._offsetY = this._getAdjustedOffsetY(this._offsetY); this._firstCompute = false; } this._updateDimensionParams(); this._setSmallestAndLargestVisibleIndices(all); this.calculateVisibleStickyIndex(this.props.stickyIndices, this._smallestVisibleIndex, this._largestVisibleIndex, this._offsetY, this._windowBound); this._computeLayouts(); this.stickyViewVisible(this.stickyVisiblity); } public onScroll(offsetY: number): void { offsetY = this._getAdjustedOffsetY(offsetY); this._offsetY = offsetY; this._updateDimensionParams(); this.boundaryProcessing(offsetY, this._windowBound); if (this._previousStickyIndex !== undefined) { if (this._previousStickyIndex * this.stickyTypeMultiplier >= this.currentStickyIndex * this.stickyTypeMultiplier) { throw new CustomError(RecyclerListViewExceptions.stickyIndicesArraySortError); } const scrollY: number | undefined = this.getScrollY(offsetY, this._scrollableHeight); if (this._previousHeight && this._currentYd && scrollY && scrollY < this._currentYd) { if (scrollY > this._currentYd - this._previousHeight) { this.currentIndex -= this.stickyTypeMultiplier; const translate = (scrollY - this._currentYd + this._previousHeight) * (-1 * this.stickyTypeMultiplier); this._stickyViewOffset.setValue(translate); this._computeLayouts(); this.stickyViewVisible(true); } } else { this._stickyViewOffset.setValue(0); } } if (this._nextStickyIndex !== undefined) { if (this._nextStickyIndex * this.stickyTypeMultiplier <= this.currentStickyIndex * this.stickyTypeMultiplier) { throw new CustomError(RecyclerListViewExceptions.stickyIndicesArraySortError); } const scrollY: number | undefined = this.getScrollY(offsetY, this._scrollableHeight); if (this._currentHeight && this._nextYd && scrollY && scrollY + this._currentHeight > this._nextYd) { if (scrollY <= this._nextYd) { const translate = (scrollY - this._nextYd + this._currentHeight) * (-1 * this.stickyTypeMultiplier); this._stickyViewOffset.setValue(translate); } else if (scrollY > this._nextYd) { this.currentIndex += this.stickyTypeMultiplier; this._stickyViewOffset.setValue(0); this._computeLayouts(); this.stickyViewVisible(true); } } else { this._stickyViewOffset.setValue(0); } } } protected abstract hasReachedBoundary(offsetY: number, windowBound?: number): boolean; protected abstract initStickyParams(): void; protected abstract calculateVisibleStickyIndex( stickyIndices: number[] | undefined, smallestVisibleIndex: number, largestVisibleIndex: number, offsetY: number, windowBound?: number): void; protected abstract getNextYd(_nextY: number, nextHeight: number): number; protected abstract getCurrentYd(currentY: number, currentHeight: number): number; protected abstract getScrollY(offsetY: number, scrollableHeight?: number): number | undefined; protected stickyViewVisible(_visible: boolean, shouldTriggerRender: boolean = true): void { this.stickyVisiblity = _visible; if (shouldTriggerRender) { this.setState({}); } } protected getWindowCorrection(props: StickyObjectProps): WindowCorrection { return (props.getWindowCorrection && props.getWindowCorrection()) || this._windowCorrection; } protected boundaryProcessing(offsetY: number, windowBound?: number): void { const hasReachedBoundary: boolean = this.hasReachedBoundary(offsetY, windowBound); if (this.bounceScrolling !== hasReachedBoundary) { this.bounceScrolling = hasReachedBoundary; if (this.bounceScrolling) { this.stickyViewVisible(false); } else { this.onVisibleIndicesChanged(this.visibleIndices); } } } private _updateDimensionParams(): void { const rlvDimension: Dimension | undefined = this.props.getRLVRenderedSize(); if (rlvDimension) { this._scrollableHeight = rlvDimension.height; this._scrollableWidth = rlvDimension.width; } const contentDimension: Dimension | undefined = this.props.getContentDimension(); if (contentDimension && this._scrollableHeight) { this._windowBound = contentDimension.height - this._scrollableHeight; } } private _computeLayouts(newStickyIndices?: number[]): void { const stickyIndices: number[] | undefined = newStickyIndices ? newStickyIndices : this.props.stickyIndices; if (stickyIndices) { this.currentStickyIndex = stickyIndices[this.currentIndex]; this._previousStickyIndex = stickyIndices[this.currentIndex - this.stickyTypeMultiplier]; this._nextStickyIndex = stickyIndices[this.currentIndex + this.stickyTypeMultiplier]; if (this.currentStickyIndex !== undefined) { this._currentLayout = this.props.getLayoutForIndex(this.currentStickyIndex); this._currentY = this._currentLayout ? this._currentLayout.y : undefined; this._currentHeight = this._currentLayout ? this._currentLayout.height : undefined; this._currentYd = this._currentY && this._currentHeight ? this.getCurrentYd(this._currentY, this._currentHeight) : undefined; } if (this._previousStickyIndex !== undefined) { this._previousLayout = this.props.getLayoutForIndex(this._previousStickyIndex); this._previousHeight = this._previousLayout ? this._previousLayout.height : undefined; } if (this._nextStickyIndex !== undefined) { this._nextLayout = this.props.getLayoutForIndex(this._nextStickyIndex); this._nextY = this._nextLayout ? this._nextLayout.y : undefined; this._nextHeight = this._nextLayout ? this._nextLayout.height : undefined; this._nextYd = this._nextY && this._nextHeight ? this.getNextYd(this._nextY, this._nextHeight) : undefined; } } } private _setSmallestAndLargestVisibleIndices(indicesArray: number[]): void { this.visibleIndices = indicesArray; this._smallestVisibleIndex = indicesArray[0]; this._largestVisibleIndex = indicesArray[indicesArray.length - 1]; } private _renderSticky(): JSX.Element | JSX.Element[] | null { const _stickyData: any = this.props.getDataForIndex(this.currentStickyIndex); const _stickyLayoutType: string | number = this.props.getLayoutTypeForIndex(this.currentStickyIndex); const _extendedState: object | undefined = this.props.getExtendedState(); const _rowRenderer: ((type: string | number, data: any, index: number, extendedState?: object) => JSX.Element | JSX.Element[] | null) = this.props.getRowRenderer(); if (this.props.overrideRowRenderer) { return this.props.overrideRowRenderer(_stickyLayoutType, _stickyData, this.currentStickyIndex, _extendedState); } else { return _rowRenderer(_stickyLayoutType, _stickyData, this.currentStickyIndex, _extendedState); } } private _getAdjustedOffsetY(offsetY: number): number { return offsetY + this.getWindowCorrection(this.props).windowShift; } }