Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FlatList] Scrolling issues with FlatList when rows are variable height #13727

Closed
giladno opened this issue May 1, 2017 · 62 comments
Closed
Labels
Resolution: Locked This issue was locked by the bot.

Comments

@giladno
Copy link
Contributor

giladno commented May 1, 2017

I'm using a FlatList where each row can be of different height (and may contain a mix of both text and zero or more images from a remote server).

I cannot use getItemLayout because I don't know the height of each row (nor the previous ones) to be able to calculate.

The problem I'm facing is that I cannot scroll to the end of the list (it jumps back few rows when I try) and I'm having issues when trying to use scrollToIndex (I'm guessing due to the fact I'm missing getItemLayout).

I wrote a sample project to demonstrate the problem (simply try to scroll to the end with your finger - it won't allow you)

import React, { Component } from 'react';
import { AppRegistry, StyleSheet, Text, View, Image, FlatList } from 'react-native';
import autobind from 'autobind-decorator';

const items = count => [...Array(count)].map((v, i) => ({
    key: i,
    index: i,
    image: 'https://dummyimage.com/600x' + (((i % 4) + 1) * 50) + '/000/fff',
}));

class RemoteImage extends Component {
    constructor(props) {
        super(props);
        this.state = {
            style: { flex: 1, height: 0 },
        };
    }

    componentDidMount() {
        Image.getSize(this.props.src, (width, height) => {
            this.image = { width, height };
            this.onLayout();
        });
    }

    @autobind
    onLayout(event) {
        if (event) {
            this.layout = {
                width: event.nativeEvent.layout.width,
                height: event.nativeEvent.layout.height,
            };
        }
        if (!this.layout || !this.image || !this.image.width)
            return;
        this.setState({
            style: {
                flex: 1,
                height: Math.min(this.image.height,
                    Math.floor(this.layout.width * this.image.height / this.image.width)),
            },
        });
    }
    render() {
        return (
            <Image
                onLayout={this.onLayout}
                source={{ uri: this.props.src }}
                style={this.state.style}
                resizeMode='contain'
            />
        );
    }
}

class Row extends Component {
    @autobind
    onLayout({ nativeEvent }) {
        let { index, item, onItemLayout } = this.props;
        let height = Math.max(nativeEvent.layout.height, item.height || 0);
        if (height != item.height)
            onItemLayout(index, { height });
    }

    render() {
        let { index, image } = this.props.item;
        return (
            <View style={[styles.row, this.props.style]}>
                <Text>Header {index}</Text>
                <RemoteImage src = { image } />
                <Text>Footer {index}</Text>
            </View>
        );
    }
}

export default class FlatListTest extends Component {
    constructor(props) {
        super(props);
        this.state = { items: items(50) };
    }

    @autobind
    renderItem({ item, index }) {
        return <Row
        item={item}
        style={index&1 && styles.row_alternate || null}
        onItemLayout={this.onItemLayout}
        />;
    }

    @autobind
    onItemLayout(index, props) {
        let items = [...this.state.items];
        let item = { ...items[index], ...props };
        items[index] = { ...item, key: [item.height, item.index].join('_') };
        this.setState({ items });
    }

    render() {
        return (
            <FlatList
                    ref={ref => this.list = ref}
                    data={this.state.items}
                    renderItem={this.renderItem}
                />
        );
    }
}

const styles = StyleSheet.create({
    row: {
        padding: 5,
    },
    row_alternate: {
        backgroundColor: '#bbbbbb',
    },
});

AppRegistry.registerComponent('FlatListTest', () => FlatListTest);
@leonidkuznetsov18
Copy link

@giladno why you use @autoBind? maybe => its very simple js binding!

@giladno
Copy link
Contributor Author

giladno commented May 3, 2017

@ugiacoman
Copy link

@giladno I'm facing the same issue. Any fixes?

Example: https://gfycat.com/SaltyBitterGermanspitz

@leonidkuznetsov18
Copy link

@giladno Very interesting information! Thk)

@giladno
Copy link
Contributor Author

giladno commented May 3, 2017

@ugiacoman no fix for me yet. I want to try the new 0.44 version to compare with.

@ugiacoman
Copy link

@giladno v0.44 Partially fixed it for me :)

@giladno
Copy link
Contributor Author

giladno commented May 3, 2017

@ugiacoman please explain "partially" :)

@ugiacoman
Copy link

@giladno So the FlatList correctly calculates the viewport so I can scroll all the way down. However if I add another component within the same Flexbox, it simply pushes it down and I end up not being able to scroll all the way. Now that I think about it, that may not be a bug. I need to do more testing to figure out the precise requirements so that I can ensure it renders correctly.

@ariona
Copy link

ariona commented May 10, 2017

Hi @giladno any update for this? I am kinda facing the same problem, the current solution is to init the initialNumToRender to number of the item index i want to reach. and scrollToIndex when componentDidmounted. But it's kinda slow

@giladno
Copy link
Contributor Author

giladno commented May 10, 2017

@ariona Sadly, no solution yet for me :(

@Ashoat
Copy link
Contributor

Ashoat commented May 11, 2017

I have this problem on a project of mine. My solution has been to dynamically calculate the height of each cell as a function of the data it contains, and to include the height information in the data blob I pass to the list so that I can implement getItemLayout. I block the rendering of the list on height calculation for literally every cell, which may be prohibitive for some folks depending on list length. The hardest part has been calculating the height of text, since that can depend on font, word wrapping, etc. On iOS native this can be achieved via the sizeWithAttributes: method, but there's no equivalent in React Native, so I pulled together a higher-order component that invisibly renders Text with a given style to the screen in batches, waits for onLayout to get the height, and then returns the text height via a callback prop.

@giladno
Copy link
Contributor Author

giladno commented May 11, 2017

@Ashoat Could you please share the code where you render the component and calculate its height? Thanks.

@Ashoat
Copy link
Contributor

Ashoat commented May 11, 2017

@Gildano: np, here you go - https://gist.github.com/Ashoat/c283a40b052ae1e4571e944abcef0fd1

It's a React component called TextHeightMeasurer. You hand it:

  • textToMeasure - an array of strings whose heights you want to measure
  • allHeightsMeasuredCallback - a callback to call when measurement is completed
  • style - the font (size, style, decoration, etc.) and content width should be the same as it will be when rendered in the list

Besides text, it should be possible to identify the height of everything else programmatically.

@giladno
Copy link
Contributor Author

giladno commented May 11, 2017

@Ashoat much appreciated buddy!!

@giladno
Copy link
Contributor Author

giladno commented May 12, 2017

@Ashoat Could you please contact me in private (check my profile page)? I have a consulting job for you.

@parkerproject
Copy link

@giladno v0.44 doesn't fix it, I just updated and still having the same issue.

@giladno
Copy link
Contributor Author

giladno commented May 23, 2017

@parkerproject Yeah, that's pretty bad. I'm willing to pay for a bug fix :(

@ippa
Copy link

ippa commented May 23, 2017

Didn't have the exact same issue as you did, but I did have the same kind of data (images + random lengths of text).

When I switched to FlatList i ran into various perf-problems I couldn't get out of. Switching back to ListView solved it for me. Too bad, since the FlatList api is so much nicer.

@giladno
Copy link
Contributor Author

giladno commented May 23, 2017

@ippa Thanks, I was originally using ListView and it was ok, the problem is that it doesn't support jump to item, which is crucial in my case.

@lprhodes
Copy link
Contributor

lprhodes commented Jun 1, 2017

@Ashoat TextHeightMeasurer looks like it'll be handy thanks! How do you use it within an app though? Is it meant to replace a component or do you iterate through records and render the TextHeightMeasurer component off screen then display the list once the measurements are ready?

@Ashoat
Copy link
Contributor

Ashoat commented Jun 2, 2017

It's sorta like StatusBar, it doesn't render anything so you can place it anywhere. I place it in the same View my FlatList is in.

The version in the gist I linked above probably has a bug or two, here's the latest version: https://github.com/Ashoat/squadcal/blob/master/native/text-height-measurer.react.js

@lprhodes
Copy link
Contributor

lprhodes commented Jun 2, 2017

Nice, thanks.

I ended up using onLayout then storing the dimensions of each cell.

I then used onViewableItemsChanged to determine whether a cell should be visible or not and stored this information too.

Finally, the render method checks whether the cell should be visible or not. If not it returns an empty view with the previously stored version (otherwise cells would jump around).

Additionally onViewableItemsChanged works out the direction that the scroll view is moving based on which cells are being removed. It then adds make sure there's always X cells visible before of the first cell and X cells visible after the last cell.

Doing this means I can now scroll through a lot more cells and the user only sees the blank cells in rare cases (a really low res image could be used to help with design).

I'll release it as a component next week some time.

@sahrens
Copy link
Contributor

sahrens commented Jun 2, 2017

@ippa what perf issues did you have migrating off ListView?

@sahrens
Copy link
Contributor

sahrens commented Jun 2, 2017

@giladno: does FlatList have any more problems than ListView for you, or is it mostly just the scrollToIndex with variable height problem?

@parkerproject
Copy link

parkerproject commented Jun 2, 2017

This issue is more to do with the styling. My approach was to add footer component to the Flatlist, though it might not be the optimal solution, it gets the job done for now
ListFooterComponent={() => ( <View style={{ height: 150 }} /> )}

You can adjust the height to meet you needs

@sahrens
Copy link
Contributor

sahrens commented Jun 2, 2017

I'm guessing there is an issue with the layout of your screen that's not FlatList specific. Try mucking with the style prop. A complete code snippet for the whole page might help others identify the bug.

@hramos hramos changed the title Scrolling issues with FlatList when rows are variable height [FlatList] Scrolling issues with FlatList when rows are variable height Jun 2, 2017
@OrganicCat
Copy link

This may be a dumb question, but is there a reason you're not using "scrollToEnd()"?

I had a pretty big issue with this until I attached that. I was adding dynamically sized components, and it wasn't going all the way to the bottom. Note, that it doesn't fire properly under "componentDidUpdate()" so I created my own method which fires it:

scrollToBottom() {
    // Settimeout needed here to wait for the page module to be added so scroll works properly
    setTimeout(this.refs.listView.scrollToEnd, 0)
}

I'm not sure if this is NEEDED or I'm just missing something, but it's far easier than calculating each individual component size (unless you need to go to a specific component, not the bottom).

@eballeste
Copy link

@lprhodes would love too see your onViewableItemsChanged logic!

I'm currently experiencing jumps due to objects reverting to their collapsed state once they are off screen (via onViewableItemsChanged) after the fact that a scrollToIndex was triggered by a user expanding some other, viewable, collapsed item.

@eballeste
Copy link

nevermind fixed the jumping by slowing down the collapsing item animation and setting
viewabilityConfig={{ viewAreaCoveragePercentThreshold: 35 }}
on the flatlist so that expanded items start collapsing while partially off the screen as opposed to completely off screen, now it looks way better.

@victorbadila
Copy link

same issue, and this seems to be the standard behaviour. in the example page https://facebook.github.io/react-native/docs/using-a-listview.html if you add a lot of items to the FlatList example you will see that scrolling doesn't work properly, you scroll and then it gets back up to the beginning.
@hramos it's not nice to see you close a lot of react-native related issues due to inactivity, what's the point of this? at least have the documentation up to date, if this is the standard behaviour it should say so in the docs.

@sahrens
Copy link
Contributor

sahrens commented Oct 5, 2017

Sorry, looking at the original issue again I see there might be an additional issue - you might be hitting the first caveat described in the docs. Quoting from https://facebook.github.io/react-native/docs/flatlist.html:

Internal state is not preserved when content scrolls out of the render window. Make sure all your data is captured in the item data or external stores like Flux, Redux, or Relay.

So when your items scroll out of the render window, they lose their state which included the height from the async getImage callback (or onLayout). When they are back in the window (note fast scrolling will shrink the window and then it will try to grow in both directions once scrolling stops) they will render their images with zero height at first, then get the height again, and then adjust the height and push all the items below them down.

There are many ways to get around this. One would be a simple synchronous cache of src -> height that you would read from when initializing your state. But you already did the work of storing the height in the state of the FlatListTest component, you just never use it - you could pass this.props.item.height back in as a prop to your RemoteImage component and use that to initialize the state as well.

@sahrens
Copy link
Contributor

sahrens commented Oct 5, 2017

I think @eballeste is probably having that render window issue as well.

BUT, as for the bottom content not being reachable, I still think it probably has to do with the layout of the rest of the app. @victorbadila mentioned it's reproable in the demo app, but it also repros with a plain ScrollView and ListView, not just FlatList. I'm guessing that something is messed up a shared component like the navigation library, or something like that. The proper fix will depend on the rest of the app, but you can also just add some marginBottom.

@eballeste
Copy link

@sahrens, nope! I'm good, I found the viewOffset parameter for the FlatList.scrollToIndex method. My feed feels really good.

@sahrens
Copy link
Contributor

sahrens commented Oct 6, 2017

Great to hear!

@Knight704
Copy link

I use onLayout callback and inside it scrollToEnd work as expected, without getItemLayout implementation

@ariona
Copy link

ariona commented Nov 16, 2017

I use that method too but when i want to navigate to 200th item it has long delay.

For now i use react-native-recyclerview-list but it only available for android.

@xavieramoros
Copy link

I'm having the same issue: a flatlist with variable height items and scrollToIndex not scrolling to the right position.

But I noticed something interesting, the FlatList scrolls to the right position to then adjust to the wrong position:

Here is an example of the behaviour:
flatlistscrolling
https://exp.host/@xavieramoros/snack-SyN4FJikz
source code:
https://github.com/xavieramoros/flatlist-initialScrollIndexIssue/blob/master/App.js
Expo 22 - RN 0.49

If the FlatList is able to scroll (even if briefly) to the right index, couldn't we just find a way make it stay there?

I tried adding:

viewabilityConfig={{ viewAreaCoveragePercentThreshold: 35 }}
as @eballeste suggested, didn't work for me.

My last option is to previously calculate the height of each item (what @Ashoat suggested), but it's hard since in my original project I'm rendering facebook posts, tweets, instagram posts, blogs posts all with variable text and images.

@eballeste
Copy link

eballeste commented Nov 17, 2017 via email

@pcattori
Copy link

pcattori commented Nov 27, 2017

Solved this by specifying both getItemLayout and abandoning initialScrollIndex in favor of calling scrollToOffset within onLayout. (In may case, I was looking for a performant/correct way to initialize the FlatList scrolled to some index)

I had a list of ~300 elements with variable heights. I knew ahead of time that certain rows would be of certain heights, so i pre-computed each row height (for length property) and cumulative row height (for offset property) to use in getItemLayout. then i used onLayout to call scrollToOffset.

Was getting (1) blank screens, (2) jittery movement, (3) flashes of the correct UI suddenly disappearing etc... before this fix.

const cumulativeRowHeights = [
  0,
  ...rowHeights.reduce(function(soFar, current) {
    const next = (soFar[soFar.length - 1] || 0) + current;
    soFar.push(next);
    return soFar;
  }, [])
].slice(0, data.length);

const getItemLayout = (data, index) => {
        const length = rowHeights[index];
        const offset = cumulativeRowHeights[index];
        return {length, offset, index};
}

const onLayout = () => this.listRef.scrollToOffset({offset: cumulativeRowHeights[desiredIndex]})

...

<FlatList
    {...otherProps}
    ref={(ref) => { this.listRef = ref; }}
    getItemLayout={getItemLayout}
    onLayout={onLayout}
>
   ...
</FlatList>

I then use this.listRef.scrollToOffset({offset: cumulativeRowHeights[desiredIndex]}) again in the onPress for a button that allows users to return to the special index.

@xavieramoros
Copy link

xavieramoros commented Nov 27, 2017

Sorry for the late reply, what you mention @eballeste is very similar to what @pcattori uses. It's easy to calculate the offset when you know the height of the elements. I've updated the snack with both approaches (scrollToIndex and scrollToOffset) working correctly :
https://exp.host/@xavieramoros/snack-SyN4FJikz

But in my (real) case, my problem is that calculating the height of the elements is quite difficult . I was trying to find another way but I'll have to stick to these two for now.

EDIT: I've also added to the expo @Ashoat text height measurer component, works wonderfully as well.

EDIT2: Source code is here: https://github.com/xavieramoros/flatlist-initialScrollIndexIssue
Thanks all!

@RoarRain
Copy link

Hi @xavieramoros Can I look at the https://exp.host/@xavieramoros/snack-SyN4FJikz source code? thx!

@wellyshen
Copy link

I have the same issue.

@Sir-hennihau
Copy link

Any fixes on this yet?

@victorbadila
Copy link

from what I have seen it sometimes happens and sometimes does not, even though in the examples where I use it the code is really the same. once you get it to "work" in a specific example it will continue to work forever, so the behaviour seems to be pretty much deterministic. not sure what triggers it to not work in other examples.

@Sir-hennihau
Copy link

not sure what triggers it to not work in other examples.

Unfortunate if you can not get it running even once :D.

@xavieramoros
Copy link

@wenkangzhou
Copy link

I have the same issue.

@afshin-hoseini
Copy link

afshin-hoseini commented Apr 2, 2018

This worked for me:

https://stackoverflow.com/a/48249803/1500515

Wrap Flatlist in a view with style={{flex:1}}

@pgqueme
Copy link

pgqueme commented Apr 4, 2018

Someone pointed out on another issue (lost the link) that your FlatList should not be a child of ScrollView, as ScrollView loads every child view, even those who are not showing right now.

I managed to fix my issue with the following steps:

  • Take my FlatList out of it's ScrollView parent
  • Add initialNumToRender={this.state.data.length} to FlatList, in order to load all my cells on first load (I know this may cause performance issues, but so far so good on ~700 records, only Text)
  • Scroll to index with this.myFlatList.scrollToIndex({animated: true, index: myIndex});

Works way smoother and straightforward than scrollToOffset. In any case someone is wondering how to do it with scrollToOffset, let me know.

@velevtzvetlin
Copy link

velevtzvetlin commented Apr 18, 2018

Still haven't been able to find a way to solve this problem.

@anasidrissi
Copy link

anasidrissi commented Apr 27, 2018

this my be usefull if any one have problem with scrolling in flatlist
with this i made a grid inside flatlist

<FlatList numColumns={3} columnWrapperStyle={{
                flex:1,
                flexDirection:'row',
                flexWrap:'wrap',
            }}

@chinalwb
Copy link

I read this in VirtualizedList.js

    const scrollProps = {
      ...this.props,
      onContentSizeChange: this._onContentSizeChange,
     ...

And

  _onContentSizeChange = (width: number, height: number) => {
    if (
      width > 0 &&
      height > 0 &&
      this.props.initialScrollIndex != null &&
      this.props.initialScrollIndex > 0 &&
      !this._hasDoneInitialScroll
    ) {
      this.scrollToIndex({
        animated: false,
        index: this.props.initialScrollIndex,
      });
      this._hasDoneInitialScroll = true;
    }
    if (this.props.onContentSizeChange) {
      this.props.onContentSizeChange(width, height);
    }
    this._scrollMetrics.contentLength = this._selectLength({height, width});
    this._scheduleCellsToRenderUpdate();
    this._maybeCallOnEndReached();
  };

Attention that when onContentSizeChange, it calls: this.scrollToIndex({animated, index}) first then it calls this.props.onContentSizeChange(width, height);. Would that mean, we can call scrollToIndex in our own onContentSizeChange to reach the same effect? I assumed so and I did that, the result is it works but user can still see the top item and then jump to the correct comment index (mine is a chat app, when user load prev page to read history comments, I need to prepend the loaded comments and locate the user back to where he was).

@itome
Copy link

itome commented Jun 6, 2018

I have the same issue. https://github.com/godness84/react-native-recyclerview-list can solve this issue only for android.
But I still don't know how to implement this feature in iOS.

@MN767
Copy link

MN767 commented Jul 31, 2018

I did not find any way to use getItemLayout when the rows have variable heights , So you can not use initialScrollIndex .

But I have a solution that may be a bit slow:
You can use scrollToIndex , but when your item is rendered . So you need initialNumToRender .
You have to wait for the item to be rendered and after use scrollToIndex so you can not use scrollToIndex in componentDidMount .

The only solution that comes to my mind is using scrollToIndex in onViewableItemsChanged . Take note of the example below :
In this example, we want to go to item this.props.index as soon as this component is run

constructor(props){

        this.goToIndex = true;

    }

render() {
        return (

                <FlatList
                    ref={component => {this.myFlatList = component;}}
                    data={data}
                    renderItem={({item})=>this._renderItem(item)}
                    keyExtractor={(item,index)=>index.toString()}
                    initialNumToRender={this.props.index+1}
                    onViewableItemsChanged={({ viewableItems }) => {
                        if (this.goToIndex){
                            this.goToIndex = false;
                            setTimeout(() => { this.myFlatList.scrollToIndex({index:this.props.index}); }, 10);
                        }
                    }}
                />

        );
    }

@facebook facebook locked as resolved and limited conversation to collaborators Aug 24, 2018
@react-native-bot react-native-bot added the Resolution: Locked This issue was locked by the bot. label Aug 24, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Resolution: Locked This issue was locked by the bot.
Projects
None yet
Development

No branches or pull requests