Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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);
like image 963
Gilad Novik Avatar asked Apr 30 '17 17:04

Gilad Novik


People also ask

How do you adjust the height of a FlatList?

To change the height of a FlatList in React Native, we can set the height of the View that we wrap around the FlatList . to set the height of the View to 300px to make the height of the FlatList 300px.

How do I scroll to particular index in FlatList React Native?

We would use Ref. scrollToIndex() inbuilt function of FlatList here. To use this function first we have to make a reference of our FlatList component then we can call this function.

How do you optimize a FlatList performance?

Use basic components​ The more complex your components are, the slower they will render. Try to avoid a lot of logic and nesting in your list items. If you are reusing this list item component a lot in your app, create a component only for your big lists and make them with as little logic and nesting as possible.


2 Answers

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.

like image 59
wmcbain Avatar answered Oct 12 '22 20:10

wmcbain


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)

like image 42
Andrey Eremenko Avatar answered Oct 12 '22 22:10

Andrey Eremenko