Gilad Novik
Gilad Novik

Reputation: 4604

Scrolling issues with FlatList when rows are variable height

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:

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);

Upvotes: 16

Views: 8842

Answers (5)

nilavan raj
nilavan raj

Reputation: 11

You can use onScrollToIndexFailed to avoid getItemLayout

onScrollToIndexFailed={info => {
            const wait = new Promise(resolve => setTimeout(resolve, 100));
            wait.then(() => {
              refContainer.current?.scrollToIndex({
                index: pinPosition || 0,
                animated: true
              });
            });
          }}

Upvotes: 0

Andrey Eremenko
Andrey Eremenko

Reputation: 69

Use scrollToOffset() instead:

    export default class List extends React.PureComponent {

        // Gets the total height of the elements that come before
        // element with passed index
        getOffsetByIndex(index) {
            let offset = 0;
            for (let i = 0; i < index; i += 1) {
                const elementLayout = this._layouts[i];
                if (elementLayout && elementLayout.height) {
                    offset += this._layouts[i].height;
                }
            }
            return offset;
        }

        // Gets the comment object and if it is a comment
        // is in the list, then scrolls to it
        scrollToComment(comment) {
            const { list } = this.props;
            const commentIndex = list.findIndex(({ id }) => id === comment.id);
            if (commentIndex !== -1) {
                const offset = this.getOffsetByIndex(commentIndex);
                this._flatList.current.scrollToOffset({ offset, animated: true });
            }
        }

        // Fill the list of objects with element sizes
        addToLayoutsMap(layout, index) {
            this._layouts[index] = layout;
        }

        render() {
            const { list } = this.props;

            return (
                <FlatList
                    data={list}
                    keyExtractor={item => item.id}
                    renderItem={({ item, index }) => {
                        return (
                            <View
                                onLayout={({ nativeEvent: { layout } }) => {
                                    this.addToLayoutsMap(layout, index);
                                }}
                            >
                                <Comment id={item.id} />
                            </View>
                        );
                    }}
                    ref={this._flatList}
                />
            );
        }
    }
  1. When rendering, I get the size of each element of the list and write it into an array:

onLayout={({ nativeEvent: { layout } }) => this._layouts[index] = layout}

  1. When it is necessary to scroll the screen to the element, I summarize the heights of all the elements in front of it and get the amount to which to scroll the screen (getOffsetByIndex method).

  2. I use the scrollToOffset method:

this._flatList.current.scrollToOffset({ offset, animated: true });

(this._flatList is ref of FlatList)

Upvotes: 6

Mohammad Nazari
Mohammad Nazari

Reputation: 3025

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);
                        }
                    }}
                />

        );
    }

Upvotes: 0

wmcbain
wmcbain

Reputation: 1129

So what I think you can do and what you already have the outlets for is to store a collection by the index of the rows layouts onLayout. You'll want to store the attributes that's returned by getItemLayout: {length: number, offset: number, index: number}.

Then when you implement getItemLayout which passes an index you can return the layout that you've stored. This should resolve the issues with scrollToIndex. Haven't tested this, but this seems like the right approach.

Upvotes: 1

ragamufin
ragamufin

Reputation: 4162

Have you tried scrollToEnd?

http://facebook.github.io/react-native/docs/flatlist.html#scrolltoend

As the documentation states, it may be janky without getItemLayout but for me it does work without it

Upvotes: 0

Related Questions