import RecycleItemPool from "../utils/RecycleItemPool" ; import { Dimension , BaseLayoutProvider } from "./dependencies/LayoutProvider" ; import CustomError from "./exceptions/CustomError" ; import RecyclerListViewExceptions from "./exceptions/RecyclerListViewExceptions" ; import { Point , LayoutManager } from "./layoutmanager/LayoutManager" ; import ViewabilityTracker , { TOnItemStatusChanged , WindowCorrection } from "./ViewabilityTracker" ; import { ObjectUtil , Default } from "ts-object-utils" ; import TSCast from "../utils/TSCast" ; import { BaseDataProvider } from "./dependencies/DataProvider" ; /***
 * Renderer which keeps track of recyclable items and the currently rendered items. Notifies list view to re render if something changes, like scroll offset
 */ export interface RenderStackItem { dataIndex ? : number ; } export interface StableIdMapItem { key : string ; type : string | number ; } export interface RenderStack { [ key ] : RenderStackItem ; } export interface RenderStackParams { isHorizontal ? : boolean ; itemCount : number ; initialOffset ? : number ; initialRenderIndex ? : number ; renderAheadOffset ? : number ; } export type StableIdProvider = ( index : number ) => string ; export default class VirtualRenderer { private onVisibleItemsChanged : TOnItemStatusChanged | null ; private _scrollOnNextUpdate : ( point : Point ) => void ; private _stableIdToRenderKeyMap : { [ key ] : StableIdMapItem | undefined ; } ; private _engagedIndexes : { [ key ] : number | undefined ; } ; private _renderStack : RenderStack ; private _renderStackChanged : ( renderStack : RenderStack ) => void ; private _fetchStableId : StableIdProvider ; private _isRecyclingEnabled : boolean ; private _isViewTrackerRunning : boolean ; private _markDirty : boolean ; private _startKey : number ; private _layoutProvider : BaseLayoutProvider = TSCast . cast < BaseLayoutProvider > ( null ) ; private _recyclePool : RecycleItemPool = TSCast . cast < RecycleItemPool > ( null ) ; private _params : RenderStackParams | null ; private _layoutManager : LayoutManager | null = null ; private _viewabilityTracker : ViewabilityTracker | null = null ; private _dimensions : Dimension | null ; constructor ( renderStackChanged , scrollOnNextUpdate , fetchStableId , isRecyclingEnabled ) { this . _renderStack = { } ; this . _fetchStableId = fetchStableId ; this . _stableIdToRenderKeyMap = { } ; this . _engagedIndexes = { } ; this . _renderStackChanged = renderStackChanged ; this . _scrollOnNextUpdate = scrollOnNextUpdate ; this . _dimensions = null ; this . _params = null ; this . _isRecyclingEnabled = isRecyclingEnabled ; this . _isViewTrackerRunning = false ; this . _markDirty = false ; this . _startKey = 0 ; this . onVisibleItemsChanged = null ; } public getLayoutDimension ( ) { if ( this . _layoutManager ) { return this . _layoutManager . getContentDimension ( ) ; } return { height : 0 , width : 0 } ; } public updateOffset ( offsetX , offsetY , isActual , correction ) { if ( this . _viewabilityTracker ) { const offset = this . _params && this . _params . isHorizontal ? offsetX : offsetY ; if ( ! this . _isViewTrackerRunning ) { if ( isActual ) { this . _viewabilityTracker . setActualOffset ( offset ) ; } this . startViewabilityTracker ( correction ) ; } this . _viewabilityTracker . updateOffset ( offset , isActual , correction ) ; } } public attachVisibleItemsListener ( callback ) { this . onVisibleItemsChanged = callback ; } public removeVisibleItemsListener ( ) { this . onVisibleItemsChanged = null ; if ( this . _viewabilityTracker ) { this . _viewabilityTracker . onVisibleRowsChanged = null ; } } public getLayoutManager ( ) { return this . _layoutManager ; } public setParamsAndDimensions ( params , dim ) { this . _params = params ; this . _dimensions = dim ; } public setLayoutManager ( layoutManager ) { this . _layoutManager = layoutManager ; if ( this . _params ) { this . _layoutManager . relayoutFromIndex ( 0 , this . _params . itemCount ) ; } } public setLayoutProvider ( layoutProvider ) { this . _layoutProvider = layoutProvider ; } public getViewabilityTracker ( ) { return this . _viewabilityTracker ; } public refreshWithAnchor ( ) { if ( this . _viewabilityTracker ) { let firstVisibleIndex = this . _viewabilityTracker . findFirstLogicallyVisibleIndex ( ) ; this . _prepareViewabilityTracker ( ) ; let offset = 0 ; if ( this . _layoutManager && this . _params ) { firstVisibleIndex = Math . min ( this . _params . itemCount - 1 , firstVisibleIndex ) ; const point = this . _layoutManager . getOffsetForIndex ( firstVisibleIndex ) ; this . _scrollOnNextUpdate ( point ) ; offset = this . _params . isHorizontal ? point . x : point . y ; } this . _viewabilityTracker . forceRefreshWithOffset ( offset ) ; } } public refresh ( ) { if ( this . _viewabilityTracker ) { this . _prepareViewabilityTracker ( ) ; if ( this . _viewabilityTracker . forceRefresh ( ) ) { if ( this . _params && this . _params . isHorizontal ) { this . _scrollOnNextUpdate ( { x : this . _viewabilityTracker . getLastActualOffset ( ) , y : 0 } ) ; } else { this . _scrollOnNextUpdate ( { x : 0 , y : this . _viewabilityTracker . getLastActualOffset ( ) } ) ; } } } } public getInitialOffset ( ) { let offset = { x : 0 , y : 0 } ; if ( this . _params ) { const initialRenderIndex = Default . value < number > ( this . _params . initialRenderIndex , 0 ) ; if ( initialRenderIndex > 0 && this . _layoutManager ) { offset = this . _layoutManager . getOffsetForIndex ( initialRenderIndex ) ; this . _params . initialOffset = this . _params . isHorizontal ? offset . x : offset . y ; } else { if ( this . _params . isHorizontal ) { offset . x = Default . value < number > ( this . _params . initialOffset , 0 ) ; offset . y = 0 ; } else { offset . y = Default . value < number > ( this . _params . initialOffset , 0 ) ; offset . x = 0 ; } } } return offset ; } public init ( ) { this . getInitialOffset ( ) ; this . _recyclePool = new RecycleItemPool ( ) ; if ( this . _params ) { this . _viewabilityTracker = new ViewabilityTracker ( Default . value < number > ( this . _params . renderAheadOffset , 0 ) , Default . value < number > ( this . _params . initialOffset , 0 ) ) ; } else { this . _viewabilityTracker = new ViewabilityTracker ( 0 , 0 ) ; } this . _prepareViewabilityTracker ( ) ; } public startViewabilityTracker ( windowCorrection ) { if ( this . _viewabilityTracker ) { this . _isViewTrackerRunning = true ; this . _viewabilityTracker . init ( windowCorrection ) ; } } public syncAndGetKey ( index , overrideStableIdProvider ? , newRenderStack ? ) { const getStableId = overrideStableIdProvider ? overrideStableIdProvider : this . _fetchStableId ; const renderStack = newRenderStack ? newRenderStack : this . _renderStack ; const stableIdItem = this . _stableIdToRenderKeyMap [ getStableId ( index ) ] ; let key = stableIdItem ? stableIdItem . key : undefined ; if ( ObjectUtil . isNullOrUndefined ( key ) ) { const type = this . _layoutProvider . getLayoutTypeForIndex ( index ) ; key = this . _recyclePool . getRecycledObject ( type ) ; if ( ! ObjectUtil . isNullOrUndefined ( key ) ) { const itemMeta = renderStack [ key ] ; if ( itemMeta ) { const oldIndex = itemMeta . dataIndex ; itemMeta . dataIndex = index ; if ( ! ObjectUtil . isNullOrUndefined ( oldIndex ) && oldIndex !== index ) { delete this . _stableIdToRenderKeyMap [ getStableId ( oldIndex ) ] ; } } else { renderStack [ key ] = { dataIndex : index } ; } } else { key = getStableId ( index ) ; if ( renderStack [ key ] ) { key = this . _getCollisionAvoidingKey ( ) ; } renderStack [ key ] = { dataIndex : index } ; } this . _markDirty = true ; this . _stableIdToRenderKeyMap [ getStableId ( index ) ] = { key , type } ; } if ( ! ObjectUtil . isNullOrUndefined ( this . _engagedIndexes [ index ] ) ) { this . _recyclePool . removeFromPool ( key ) ; } const stackItem = renderStack [ key ] ; if ( stackItem && stackItem . dataIndex !== index ) { console . warn ( "Possible stableId collision @" , index ) ; } return key ; } public handleDataSetChange ( newDataProvider , shouldOptimizeForAnimations ? ) { const getStableId = newDataProvider . getStableId ; const maxIndex = newDataProvider . getSize ( ) - 1 ; const activeStableIds = { } ; const newRenderStack = { } ; for ( const key in this . _renderStack ) { if ( this . _renderStack . hasOwnProperty ( key ) ) { const index = this . _renderStack [ key ] . dataIndex ; if ( ! ObjectUtil . isNullOrUndefined ( index ) ) { if ( index <= maxIndex ) { const stableId = getStableId ( index ) ; activeStableIds [ stableId ] = 1 ; } } } } const oldActiveStableIds = Object . keys ( this . _stableIdToRenderKeyMap ) ; const oldActiveStableIdsCount = oldActiveStableIds . length ; for ( let i = 0 ; i < oldActiveStableIdsCount ; i ++ ) { const key = oldActiveStableIds [ i ] ; if ( ! activeStableIds [ key ] ) { if ( ! shouldOptimizeForAnimations && this . _isRecyclingEnabled ) { const stableIdItem = this . _stableIdToRenderKeyMap [ key ] ; if ( stableIdItem ) { this . _recyclePool . putRecycledObject ( stableIdItem . type , stableIdItem . key ) ; } } delete this . _stableIdToRenderKeyMap [ key ] ; } } for ( const key in this . _renderStack ) { if ( this . _renderStack . hasOwnProperty ( key ) ) { const index = this . _renderStack [ key ] . dataIndex ; if ( ! ObjectUtil . isNullOrUndefined ( index ) ) { if ( index <= maxIndex ) { const newKey = this . syncAndGetKey ( index , getStableId , newRenderStack ) ; const newStackItem = newRenderStack [ newKey ] ; if ( ! newStackItem ) { newRenderStack [ newKey ] = { dataIndex : index } ; } else if ( newStackItem . dataIndex !== index ) { const cllKey = this . _getCollisionAvoidingKey ( ) ; newRenderStack [ cllKey ] = { dataIndex : index } ; this . _stableIdToRenderKeyMap [ getStableId ( index ) ] = { key : cllKey , type : this . _layoutProvider . getLayoutTypeForIndex ( index ) , } ; } } } delete this . _renderStack [ key ] ; } } Object . assign ( this . _renderStack , newRenderStack ) ; for ( const key in this . _renderStack ) { if ( this . _renderStack . hasOwnProperty ( key ) ) { const index = this . _renderStack [ key ] . dataIndex ; if ( ! ObjectUtil . isNullOrUndefined ( index ) && ObjectUtil . isNullOrUndefined ( this . _engagedIndexes [ index ] ) ) { const type = this . _layoutProvider . getLayoutTypeForIndex ( index ) ; this . _recyclePool . putRecycledObject ( type , key ) ; } } } } private _getCollisionAvoidingKey ( ) { return "#" + this . _startKey ++ + "_rlv_c" ; } private _prepareViewabilityTracker ( ) { if ( this . _viewabilityTracker && this . _layoutManager && this . _dimensions && this . _params ) { this . _viewabilityTracker . onEngagedRowsChanged = this . _onEngagedItemsChanged ; if ( this . onVisibleItemsChanged ) { this . _viewabilityTracker . onVisibleRowsChanged = this . _onVisibleItemsChanged ; } this . _viewabilityTracker . setLayouts ( this . _layoutManager . getLayouts ( ) , this . _params . isHorizontal ? this . _layoutManager . getContentDimension ( ) . width : this . _layoutManager . getContentDimension ( ) . height ) ; this . _viewabilityTracker . setDimensions ( { height : this . _dimensions . height , width : this . _dimensions . width , } , Default . value < boolean > ( this . _params . isHorizontal , false ) ) ; } else { throw new CustomError ( RecyclerListViewExceptions . initializationException ) ; } } private _onVisibleItemsChanged = ( all , now , notNow ) : void => { if ( this . onVisibleItemsChanged ) { this . onVisibleItemsChanged ( all , now , notNow ) ; } } ; private _onEngagedItemsChanged = ( all , now , notNow ) : void => { const count = notNow . length ; let resolvedKey ; let disengagedIndex = 0 ; if ( this . _isRecyclingEnabled ) { for ( let i = 0 ; i < count ; i ++ ) { disengagedIndex = notNow [ i ] ; delete this . _engagedIndexes [ disengagedIndex ] ; if ( this . _params && disengagedIndex < this . _params . itemCount ) { resolvedKey = this . _stableIdToRenderKeyMap [ this . _fetchStableId ( disengagedIndex ) ] ; if ( ! ObjectUtil . isNullOrUndefined ( resolvedKey ) ) { this . _recyclePool . putRecycledObject ( this . _layoutProvider . getLayoutTypeForIndex ( disengagedIndex ) , resolvedKey . key ) ; } } } } if ( this . _updateRenderStack ( now ) ) { this . _renderStackChanged ( this . _renderStack ) ; } } ; private _updateRenderStack ( itemIndexes ) { this . _markDirty = false ; const count = itemIndexes . length ; let index = 0 ; let hasRenderStackChanged = false ; for ( let i = 0 ; i < count ; i ++ ) { index = itemIndexes [ i ] ; this . _engagedIndexes [ index ] = 1 ; this . syncAndGetKey ( index ) ; hasRenderStackChanged = this . _markDirty ; } this . _markDirty = false ; return hasRenderStackChanged ; } }