Flipkart / recyclerlistview

High performance listview for React Native and web!
Apache License 2.0
5.25k stars 427 forks source link

onVisibleIndicesChanged stops working after X list items [Android and iOS] #334

Open pribeh opened 5 years ago

pribeh commented 5 years ago

On a recyclerlistview, we're noticing that after the first 8 items are loaded each subsequent item no longer fires onVisibleIndicesChanged (on both iOS and Android).

We are noticing a few funny things using full screen width and height for list items so we're not sure if that might impact onVisibleIndicesChanged or not. Any tips for debugging would be most welcome. Why eight? we don't know. Sometimes it'll start working again but more often than not it won't fire after the first 8.

The list we have is designed to be a vertical swiping experience. Each item is swiped to by the user. This is to accommodate full screen captured video and images.

Data/layout provider

    @computed get _dataProvider() {
        return this.dataProviderSchema.cloneWithRows(this.props.MainStore.userPosts)
    }

    _layoutProvider = new LayoutProvider(
        (index) => {
            return 'LIST';
        },
        (type, dim, index) => {
            switch (type) {
                case 'LIST':
                    dim.width = screen.width;
                    dim.height = screen.height;
                    break;
                default:
                    dim.width = 0;
                    dim.heigh = 0;
            }
        }
    )

    dataProviderSchema = new DataProvider((activity1, activity2) => {
        return activity1._id != activity2._id
    })

OnVisibleIndicesChanged logic:

    //triggered when visible posts in list changes
    _onVisibleIndicesChanged = (all, now, notNow) => {
        if (notNow && notNow.length > 0 && this._dataProvider._data[notNow[0]].type.startsWith("video")) {
            const pauseVideoFunction = this.activityComponentVideoPauseFunc[notNow[0]];
            if (pauseVideoFunction) pauseVideoFunction();
        } 

        for (const index of now) {  
            if (this._dataProvider._data[index].type.startsWith("video")) {
                const startVideoFunction = this.activityComponentVideoRunFunc[index];
                if (startVideoFunction) startVideoFunction();
            } else if (this._dataProvider._data[index].type === "image/png") {
                logEvent("view_post", {
                    "type": "image",
                    "post_id": this._dataProvider._data[index]._id
                })
            }
        }
        if (notNow && notNow.length > 0 && this._dataProvider._data[notNow[0]]) {
            const closeBoardFunction = this.activityComponentBoardCloseFunc[notNow[0]];
            if (closeBoardFunction) closeBoardFunction();
        } 
    }

Rowrenderer (the activitycomponent has height:screen.height set).

    rowRenderer = (type, item, index) => {
        return (
            <Animatable.View style={{ flex: 1, }} duration={150} useNativeDriver={true} animation="fadeInUp">
                <ActivityComponent
                    activity={item}
                    onCommentsPress={() => this._onPressComments(item)}
                    onPressProfile={() => this._onPressProfile(item)}
                    onDelete={(postId) => this._onDelete(postId)}
                    lineCount={this.lineCount[index]}
                    pauseVideo={this.pauseVideo}
                    startVideo={this.startVideo}
                    rowIndex={index}
                    gemsBarOptionsListClosed={this.closeBoard}
                />
            </Animatable.View>
        )
    };

List

<View style={{flex:1}}>
    <RecyclerListView
        onEndReached={this._onEndReached}
        dataProvider={this._dataProvider}
        layoutProvider={this._layoutProvider}
        rowRenderer={this.rowRenderer}
        onVisibleIndicesChanged={this._onVisibleIndicesChanged}
        ref={list => (this.props.MainStore.todayListRef = list)}
        snapToInterval={screen.height}
        decelerationRate={0.83}
        snapToAlignment={"center"}
        automaticallyAdjustContentInsets={true}
        overScrollMode="never"
        contentInset={{ top: - safeAreaInsetTop, left: 0, bottom: 0, right: 0, }}
        contentOffset={{ x: 0, y: safeAreaInsetTop }}
    />
</View>

Notable libs

"mobx": "^5.9.4",
"mobx-react": "^5.4.3",
"react": "^16.8.6",
"react-native": "^0.59.6",
"react-native-animatable": "^1.3.2",
"react-native-gesture-handler": "^1.1.0",
"react-native-navigation": "^2.18.2",
"recyclerlistview": "^2.0.0-beta.4",
naqvitalha commented 5 years ago

Will it be possible to provide a repro on expo? That will help me look into this quickly. On my end it doesn't happen for such items. There was a recent bug discovered to items with 0 height but that doesn't seem relevant here. If you want to debug yourself look inside ViewabilityTracker module. And see why the events are not being raised.

peacechen commented 4 years ago

I observed skipped onVisibleIndicesChanged events in a library that has complex full-page items rendered in RecyclerListView. I suspect it may be due to performance issues when the JavaScript bridge is saturated and delays scroll updates too much.

The progression of debugging: I started with an unoptimized, complex full page component. These were the data rows rendered in RecyclerListView (in my case columns because the ScrollView was set to horizontal).

I optimized the components via shouldComponentUpdate and other techniques. With the more efficient code, the simulator has not missed events so far. The slow Nexus 6 still misses events unfortunately. While this could be an Android issue, the Pixel 3 seems to rule that out since it works well. If there was a slow enough iOS device, it likely would miss events too.

paddy57 commented 4 years ago

@naqvitalha onVisibleIndicesChanged triggers two times if scroll, I have 10 items in the array and I have full page item, I have set forceNonDeterministicRendering={true},

<RecyclerListView
      layoutProvider={this._layoutProvider}
      dataProvider={this.state.dataProvider}
      rowRenderer={this._rowRenderer}
      isHorizontal={true}
      pagingEnabled={true}
      onVisibleIndicesChanged={this._onVisibleIndicesChanged}
      forceNonDeterministicRendering={true}
    />
 this._layoutProvider = new LayoutProvider(
      index => {
        return ViewTypes.FULL
      },
      (type, dim) => {
        switch (type) {
          case ViewTypes.HALF_LEFT:
            dim.width = width / 2;
            dim.height = 160;
            break;
          case ViewTypes.HALF_RIGHT:
            dim.width = width / 2;
            dim.height = 160;
            break;
          case ViewTypes.FULL:
            dim.width = 100;
            dim.height = 100;
            break;
          default:
            dim.width = 0;
            dim.height = 0;
        }
      }
    );
_rowRenderer(type, data, index) {

    return (
      <View style={{ backgroundColor: index % 2 === 0 ? 'blue' : 'red', width: Dimensions.get("window").width , height: (Dimensions.get("window").height)  }}><Text>Data: {data}</Text></View>
    )
  }
_onVisibleIndicesChanged = (all, now, notNow) => {
    console.log('_onVisibleIndicesChanged',all, now, notNow)
  }

console logs

_onVisibleIndicesChanged (5) [0, 1, 2, 3, 4] (5) [0, 1, 2, 3, 4]
_onVisibleIndicesChanged [0] []
AkshatBhaskar03 commented 4 months ago

Hey @paddy57 did you get any solution for two time console? Also do you know how can we achieve this case : "I want those indexes which is visible atleast more than 50%"