Проблемы с прокруткой с помощью FlatList, когда строки имеют переменную высоту

Я использую FlatList, где каждая строка может иметь разную высоту (и может содержать сочетание как текста, так и ноль или более изображений с удаленного сервера).

Я не могу использовать getItemLayout, потому что я не знаю высоты каждой строки (или предыдущих), чтобы иметь возможность вычислять.

Проблема, с которой я сталкиваюсь, заключается в том, что я не могу прокрутить список до конца (он перескакивает несколько строк при попытке), и у меня возникают проблемы при попытке использовать scrollToIndex (я предполагаю, что из-за тот факт, что мне не хватает getItemLayout).

Я написал образец проекта, чтобы продемонстрировать проблему:

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

Ответ 1

Итак, что я думаю, что вы можете сделать, и то, что у вас уже есть, это хранить коллекцию по индексу макетов строк onLayout. Вы захотите сохранить атрибуты, которые возвращены getItemLayout: {length: number, offset: number, index: number}.

Затем, когда вы реализуете getItemLayout, который передает индекс, вы можете вернуть сохраненный вами макет. Это должно решить проблемы с scrollToIndex. Не проверял это, но это похоже на правильный подход.

Ответ 2

Вместо этого используйте scrollToOffset():

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) При рендеринге я получаю размер каждого элемента списка и записываю его в массив:

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

2) Когда необходимо прокрутить экран до элемента, я суммирую высоту всех элементов перед ним и получаю сумму, до которой нужно прокрутить экран (метод getOffsetByIndex).

3) Я использую метод scrollToOffset:

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

(this._flatList является ссылкой на FlatList)

Ответ 4

Я не нашел способа использовать getItemLayout когда строки имеют переменную высоту, поэтому вы не можете использовать initialScrollIndex.

Но у меня есть решение, которое может быть немного медленным: вы можете использовать scrollToIndex, но когда ваш элемент отображается. Так что вам нужен initialNumToRender. Вам нужно дождаться отображения элемента и после использования scrollToIndex чтобы вы не могли использовать scrollToIndex в componentDidMount.

Единственное решение, которое приходит мне в голову - это использовать scrollToIndex в onViewableItemsChanged. Обратите внимание на пример ниже: в этом примере мы хотим перейти к элементу this.props.index как только этот компонент будет запущен

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

        );
    }