【react-native】react-native如何实现瀑布流列表

react-native如何实现瀑布流

前言

在之前的react-native项目中,有瀑布流列表的需求,查阅了很多资料、网上看了很多种实现,其中看到了一个思路:用ScrollView嵌套VirtualizedList实现,如图红色表示ScrollView,蓝色表示VirtualizedList,看上去是一个列表,实则是3个列表,
VirtualizedList的滚动事件全部委托给ScrollView进行处理

代码

import React, { Component, DetailedReactHTMLElement, LegacyRef } from 'react'
import {
  View,
  ScrollView,
  StyleSheet,
  RefreshControl,
  VirtualizedList,
  LayoutChangeEvent,
  NativeSyntheticEvent,
  NativeScrollEvent,
  StyleProp,
  ViewStyle
} from 'react-native'

export default class WaterfallList<T extends Record<string, any>> extends Component<
  WaterfallListProps<T>,
  WaterfallListState<T>
> {
  static defaultProps = {
    scrollEventThrottle: 50,
    numColumns: 1,
    renderScrollComponent: (props: WaterfallListProps) => {
      if (props.onRefresh && props.refreshing != null) {
        return (
          <ScrollView
            {...props}
            nestedScrollEnabled
            refreshControl={<RefreshControl refreshing={props.refreshing} onRefresh={props.onRefresh} />}
          />
        )
      }
      return <ScrollView {...props} />
    }
  }

  state = _reconstructData<T>(this.props)
  _listRefs: Array<VirtualizedList<T> | null> = []
  _scrollRef: ScrollView | null | undefined

  UNSAFE_componentWillReceiveProps(newProps: WaterfallListProps) {
    this.setState(_reconstructData<T>(newProps))
  }

  _onLayout = (event: LayoutChangeEvent) => {
    // @ts-ignore
    this._listRefs.forEach(list => list && list?._onLayout && list._onLayout(event))
  }

  _onContentSizeChange = (width: number, height: number) => {
    // @ts-ignore
    this._listRefs.forEach(list => list && list._onContentSizeChange && list._onContentSizeChange(width, height))
  }

  _onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
    if (this.props.onScroll) {
      this.props.onScroll(event)
    }
    // @ts-ignore
    this._listRefs.forEach(list => list && list._onScroll && list._onScroll(event))
  }

  _onScrollBeginDrag = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
    if (this.props.onScrollBeginDrag) {
      this.props.onScrollBeginDrag(event)
    }
    // @ts-ignore
    this._listRefs.forEach(list => list && list._onScrollBeginDrag && list._onScrollBeginDrag(event))
  }

  _onScrollEndDrag = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
    if (this.props.onScrollEndDrag) {
      this.props.onScrollEndDrag(event)
    }
    // @ts-ignore
    this._listRefs.forEach(list => list && list._onScrollEndDrag && list._onScrollEndDrag(event))
  }

  _onMomentumScrollEnd = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
    if (this.props.onMomentumScrollEnd) {
      this.props.onMomentumScrollEnd(event)
    }
    // @ts-ignore
    this._listRefs.forEach(list => list && list._onMomentumScrollEnd && list._onMomentumScrollEnd(event))
  }

  _getItemLayout = (columnIndex: number, rowIndex: number) => {
    const column = this.state.columns[columnIndex]
    let offset = 0
    for (let ii = 0; ii < rowIndex; ii += 1) {
      offset += column.heights[ii]
    }
    return { length: column.heights[rowIndex], offset, index: rowIndex }
  }

  _getItemCount = (data: Array<T>) => data.length

  _getItem = (data: Array<T>, index: number) => data[index]

  _captureScrollRef: LegacyRef<ScrollView> = ref => (this._scrollRef = ref)

  render() {
    const { renderItem, ListHeaderComponent, ListEmptyComponent, keyExtractor, onEndReached, ...props } = this.props
    let headerElement
    if (ListHeaderComponent) {
      headerElement = <ListHeaderComponent />
    }
    let emptyElement
    if (ListEmptyComponent) {
      emptyElement = <ListEmptyComponent />
    }

    const content = (
      <View style={styles.contentContainer}>
        {this.state.columns.map(col => (
          <VirtualizedList<T>
            {...props}
            ref={ref => (this._listRefs[col.index] = ref)}
            key={`$col_${col.index}`}
            data={col.data}
            getItemCount={this._getItemCount}
            getItem={this._getItem}
            getItemLayout={(data, index) => this._getItemLayout(col.index, index)}
            renderItem={({ item, index }) => renderItem({ item, index, column: col.index })}
            renderScrollComponent={() => <View style={styles.column} />}
            keyExtractor={keyExtractor}
            onEndReached={onEndReached}
            onEndReachedThreshold={this.props.onEndReachedThreshold}
            removeClippedSubviews={false}
          />
        ))}
      </View>
    )

    return React.cloneElement(
      this.props.renderScrollComponent(this.props),
      {
        ref: this._captureScrollRef,
        removeClippedSubviews: false,
        onContentSizeChange: this._onContentSizeChange,
        onLayout: this._onLayout,
        onScroll: this._onScroll,
        onScrollBeginDrag: this._onScrollBeginDrag,
        onScrollEndDrag: this._onScrollEndDrag,
        onMomentumScrollEnd: this._onMomentumScrollEnd
      },
      headerElement,
      emptyElement && this.props.data.length === 0 ? emptyElement : content
    )
  }
}

function _reconstructData<ItemT = any>(params: { numColumns: number; data: Array<any>; getHeightForItem: Function }) {
  const { numColumns, data, getHeightForItem } = params

  const columns: Array<Column<ItemT>> = Array.from({
    length: numColumns
  }).map((col, i) => ({
    index: i,
    totalHeight: 0,
    data: [],
    heights: []
  }))

  data.forEach((item, index) => {
    const height = getHeightForItem({ item, index })
    //判断数据项应该进的列
    const column = columns.reduce((prev, cur) => (cur.totalHeight < prev.totalHeight ? cur : prev), columns[0])
    column.data.push(item)
    column.heights.push(height)
    column.totalHeight += height
  })
  return { columns }
}

export type WaterfallListProps<ItemT = any> = {
  data: Array<ItemT>
  numColumns: number
  renderItem: ({ item, index, column }: { item: ItemT; index: number; column: number }) => React.ReactElement
  getHeightForItem: ({ item, index }: { item: ItemT; index: number }) => number
  ListHeaderComponent?: React.ComponentType<any>
  ListEmptyComponent?: React.ComponentType<any>
  keyExtractor?: (item: ItemT, index: number) => string
  onEndReached?: (info: { distanceFromEnd: number }) => void
  contentContainerStyle?: StyleProp<ViewStyle>
  onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void
  onScrollBeginDrag?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void
  onScrollEndDrag?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void
  onMomentumScrollEnd?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void
  onEndReachedThreshold?: number
  scrollEventThrottle: number
  renderScrollComponent: (props: Object) => DetailedReactHTMLElement<any, ScrollView>
  refreshing?: boolean
  onRefresh?: (() => void) | undefined
}

type Column<ItemT = any> = {
  index: number
  totalHeight: number
  data: Array<ItemT>
  heights: Array<number>
}

type WaterfallListState<ItemT = any> = {
  columns: Array<Column<ItemT>>
}

const styles = StyleSheet.create({
  contentContainer: {
    flex: 1,
    flexDirection: 'row'
  },
  column: {
    flex: 1
  }
})

使用

import React, { PureComponent } from 'react'
import { StyleSheet, View, Text } from 'react-native'
import { NavigationInjectedProps } from 'react-navigation'
import { Scene, WaterfallList } from '@comps'

type IData = {
  id: string
  height: number
  color: string
}

const COLORS = ['green', 'blue', 'red']
const DATA: Array<IData> = Array.from({ length: 200 }).map((_, i) => ({
  id: `item_${i}`,
  height: Math.round(Math.random() * 100 + 50),
  color: COLORS[i % COLORS.length]
}))

class Waterfall extends React.Component<NavigationInjectedProps, WaterfallState> {
  constructor(props: NavigationInjectedProps) {
    super(props)
    this.state = {
      isRefreshing: false
    }
  }

  _refreshRequest = () => {
    this.setState({ isRefreshing: true })
    setTimeout(() => {
      this.setState({ isRefreshing: false })
    }, 1000)
  }

  render() {
    const { navigation } = this.props
    return (
      <Scene title="瀑布流" navigation={navigation}>
        <WaterfallList<IData>
          onRefresh={this._refreshRequest}
          refreshing={this.state.isRefreshing}
          data={DATA}
          renderItem={({ item }) => <Cell item={item} />}
          getHeightForItem={({ item }) => item.height + 2}
          numColumns={3}
          keyExtractor={item => item.id}
        />
      </Scene>
    )
  }
}

export default Waterfall

type WaterfallState = {
  isRefreshing: boolean
}

class Cell extends PureComponent<{ item: { id: string; height: number; color: string } }> {
  render() {
    const { item } = this.props
    return (
      <View style={[styles.cell, { height: item.height, backgroundColor: item.color }]}>
        <Text>{item.id}</Text>
      </View>
    )
  }
}

const styles = StyleSheet.create({
  cell: {
    margin: 1,
    alignItems: 'center',
    justifyContent: 'center'
  }
})

问题

rn不推荐ScrollView嵌套VirtualizedList,版本0.64.0以前只是warning,在此之后变成了error, 64以后用要这种方式需要改下源码了😅😅😅😅

0.64.0以前
<ScrollView.Context.Consumer>
  {scrollContext => {
    if (
      scrollContext != null &&
      !scrollContext.horizontal === !this.props.horizontal &&
      !this._hasWarned.nesting &&
      this.context.virtualizedList == null
    ) {
      // TODO (T46547044): use React.warn once 16.9 is sync'd: https://github.com/facebook/react/pull/15170
      console.warn(
        'VirtualizedLists should never be nested inside plain ScrollViews with the same ' +
          'orientation - use another VirtualizedList-backed container instead.',
      );
      this._hasWarned.nesting = true;
    }
    return innerRet;
  }}
</ScrollView.Context.Consumer>
0.64.0以后
<ScrollView.Context.Consumer>
  {scrollContext => {
    if (
      scrollContext != null &&
      !scrollContext.horizontal ===
        !horizontalOrDefault(this.props.horizontal) &&
      !this._hasWarned.nesting &&
      this.context == null
    ) {
      // TODO (T46547044): use React.warn once 16.9 is sync'd: https://github.com/facebook/react/pull/15170
      console.error(
        'VirtualizedLists should never be nested inside plain ScrollViews with the same ' +
          'orientation because it can break windowing and other functionality - use another ' +
          'VirtualizedList-backed container instead.',
      );
      this._hasWarned.nesting = true;
    }
    return innerRet;
  }}
</ScrollView.Context.Consumer>

结束语

  • 👀 目前专注于前端
  • ⚙️ 在react、react-native开发方面有丰富的经验
  • 🔭 最近在学习安卓,有自己的开源安卓项目,集成react-native热更新功能
  • 我❤️ 思考、学习、编码和健身
  • 如果文章对您有帮助,三连支持一下~O(∩_∩)O谢谢!
  • 7
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GuoguoDad~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值