当行是可变高度时,FlatList 的滚动问题

Posted

技术标签:

【中文标题】当行是可变高度时,FlatList 的滚动问题【英文标题】:Scrolling issues with FlatList when rows are variable height 【发布时间】:2017-09-28 06:43:52 【问题描述】:

我正在使用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);

【问题讨论】:

Gilad Novik,你解决这个问题了吗?我也有同样的问题。 @Kholiavko 很遗憾,我还没有找到解决方案。最新的 RN 版本仍然存在这个问题 :( 【参考方案1】:

改用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
                />
            );
        
    
    渲染时,我获取列表中每个元素的大小并将其写入数组:

onLayout=( nativeEvent: layout ) =&gt; this._layouts[index] = layout

    当需要将屏幕滚动到元素时,我会汇总它前面所有元素的高度,并得到滚动屏幕的量(getOffsetByIndex方法)。

    我使用的是 scrollToOffset 方法:

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

(this._flatList 是 Fl​​atList 的引用)

【讨论】:

感谢您抽出宝贵时间分享您的代码。您能否更新您的答案以更好地描述您在做什么? 这行得通!这种方式滚动从 ScrollView 内的 FlatList 工作。非常感谢您分享这个 sn-p @AndreyEremenko 如果您想要到达的项目和它之前的项目已经布置好,则此方法有效。但事实并非如此,您提出什么解决方案?【参考方案2】:

当行的高度可变时,我没有找到任何使用getItemLayout 的方法,所以你不能使用initialScrollIndex

但我有一个可能有点慢的解决方案: 您可以使用 scrollToIndex ,但是当您的项目被渲染时。所以你需要 initialNumToRender 。 您必须等待项目被渲染并在使用 scrollToIndex 之后,所以您不能在 componentDidMount 中使用 scrollToIndex

我想到的唯一解决方案是在 onViewableItemsChanged 中使用 scrollToIndex 。请注意以下示例: 在此示例中,我们希望在此组件运行后立即转到项目 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);
                        
                    
                />

        );
    

【讨论】:

【参考方案3】:

所以我认为你可以做的以及你已经拥有的出口是通过行布局onLayout 的索引来存储集合。您需要存储returned by getItemLayout:length: number, offset: number, index: number 的属性。

然后,当您实现 getItemLayout which passes an index 时,您可以返回您存储的布局。这应该可以解决scrollToIndex 的问题。尚未对此进行测试,但这似乎是正确的方法。

【讨论】:

这也是我的想法,但我无法让它发挥作用。 GetItemLayout 在 onlayout 之前被调用,每行返回并在父组件中存储一个值【参考方案4】:

你试过scrollToEnd吗?

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

正如文档所述,没有getItemLayout 可能会很麻烦,但对我来说,没有它它确实可以工作

【讨论】:

以上是关于当行是可变高度时,FlatList 的滚动问题的主要内容,如果未能解决你的问题,请参考以下文章

如何在带有 Flutter 的可滚动视图中拥有具有可变高度内容的 TabView?

过度滚动时显示标题的Android Listview

排毒:“找不到 UI 元素。”尝试滚动 FlatList 时

当我滚动 Flatlist 时,内存使用率越来越高,当我停止滚动时内存没有释放(React Native)

当 SectionList/Flatlist 滚动/渲染项目时 UI 线程似乎被阻塞(React Native)

Listview滚动到具有可变高度的小部件