在 FlatList 中当前顶行上方插入新项目,而不造成自动滚动到顶部

Cha*_*eth 5 react-native

如何将新项目推送到 FlatList 的数据,同时保持更新前的顶行在更新后可见,并且 FlatList 不应滚动到顶部到上面推送的新项目?

问题:当使用需要渲染当前顶行上方的行的新数据更新组件状态时,FlatList 将重新渲染并从顶部重新启动 contentOffset.y。

示例:假设我有以下 FlatList 初始加载数据

this.state = {
    data: [
        {title: '2019-03-10', events: []}, 
        {title: '2019-03-11', events: []}, 
        {title: '2019-03-12', events: []}, 
    ]

}
Run Code Online (Sandbox Code Playgroud)

顶部可见行是标题为“2019-03-10”的项目,然后假设我拉动刷新并想要推送这个新数据 ->

const newData = [
    {title: '2019-03-07', events: []}, 
    {title: '2019-03-08', events: []}, 
    {title: '2019-03-09', events: []}, 
]
Run Code Online (Sandbox Code Playgroud)

进入这样的状态 ->

this.setState({
    data: this.mergeAndSortData(this.state.data, newData)
})
Run Code Online (Sandbox Code Playgroud)

现在我的数据按标题的升序日期排序,因此我将新数据添加到当前顶部可见行的数据上方,并且所有行的索引都会更改,这会强制 FlatList 重新加载,并通过重置 contentOffset.y为 0,当前顶部可见行的标题为“2019-03-07”。

首选:我想要一种使用新数据进行更新并将前一个顶部可见行保留在原处的方法。

使这变得更加复杂的另一个条件是每一行可以是任何给定的高度,没有为所有行设置高度。

尝试过:这是我迄今为止尝试过的

  1. 组件更新后设置initialScrollIndex状态。

    结果:没有做任何事情,我猜initialScrollIndex只在初始加载时起作用,甚至在重新加载FlatList数据后也不起作用。

  2. 在更新之前和更新之后获取当前滚动偏移 y,计算 FlatList 需要为新项目添加多少估计高度,并调用该估计高度的 this.list.scrollToOffset。

    结果:必须让滚动超时才能执行任何操作,这会导致每次更新和滚动后 FlatList 闪烁。(这是唯一有效但不可用的东西)。

  3. 使用scrollToIndex 并将基于项目数据的估计高度传递给 FlatList 上的 getItemLayout 属性。

    结果:没有移动到预定的行。

  4. 在 while 循环中调用scrollToOffset 并逐渐向下滚动 FlatList,直到我进入 onViewableItemsChanged 回调,即我想要可见的行进入视图。

    结果: onViewableItemsChanged 在 while 循环中没有被调用,我无法停止导致无限循环。

  5. 为每个行项目设置一个引用,并在更新测量行偏移量的新数据后,我希望通过引用可见并滚动到该偏移量。

    结果:由于某种原因,前一行的 y 偏移量总是远小于实际偏移量,也许是因为我正在测量接近重新布局或其他我错过的东西。

这是我的一些代码->

const isCloseToBottom = ({ layoutMeasurement, contentOffset, contentSize }) => {
  const paddingToBottom = 80
  return (
    layoutMeasurement.height + contentOffset.y >=
    contentSize.height - paddingToBottom
  )
}

const isCloseToTop = ({ layoutMeasurement, contentOffset, contentSize }) => {
  const paddingToTop = 80
  return contentOffset.y <= paddingToTop
}

var initMonthRange = {
  start: moment()
    .startOf('month')
    .startOf('week')
    .format('YYYY-MM-DD')
    .toString(),
  end: moment()
    .endOf('month')
    .endOf('week')
    .format('YYYY-MM-DD')
    .toString()
}

export default class AllEventsView extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      events: this.getGroupedEvents(props),
      isFetchingFuture: false,
      isFetchingPast: false,
      isFetching: false,
      currentMonth: moment(),
    }

    this.offset = 0
    this.currentVisibleIndex = 0
    this.currentVisibleDate = this.getFirstDate()
    this.scrollToRef = this.strToMoment(this.currentVisibleDate)

    this.heights = []

    this._onViewableItemsChanged = this._onViewableItemsChanged.bind(this)

    // this.viewabilityConfig = {
    //   waitForInteraction: true,
    //   viewAreaCoveragePercentThreshold: 95
    // }

    this.itemsRefs = {}

    this.isDynamicScroll = false
  }

  itemHeightForData(item) {
    let output = 0

    if (item) {
      let titleHeight = 45
      let paddingBottom = 25 // list cont
      let marginBottom = 10 // event cont
      let rowHeight = 102

      // 82 without row height

      var { data } = item
      if (data) {
        data.map(value => {
          output += rowHeight // + marginBottom
        })
      }

      output += titleHeight + paddingBottom + marginBottom
    } else {
      output = 182
    }

    return output
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps !== this.props) {
      var { loading, events } = nextProps
      if (
        events &&
        events.length > 0 &&
        !_.isEqual(events, this.props.events) &&
        !_.isEqual(events, this.state.events)
      ) {
        this.setState({
          isFetching: loading,
          isFetchingPast: loading,
          isFetchingFuture: loading
        })

        this.updateEvents(nextProps)
      }
    }
  }

  componentDidUpdate(prevProps, prevState) {
    var { events } = prevState
    if (events && !_.isEqual(events, this.state.events)) {
      if (
        this.strToMoment(events[0].title).isAfter(this.state.events[0].title)
      ) {
        // new data is in the passed
        this.scrollToSelected()
        // this.scrollTillCurrentDate()
        //this.scrollToDate()
      } else if (
        this.strToMoment(events[events.length - 1].title).isBefore(
          this.state.events[this.state.events.length - 1].title
        )
      ) {
        // new data is in the future
      }
    }
  }

  momentToStr(date) {
    return date.format('YYYY-MM-DD').toString()
  }

  strToMoment(str) {
    return moment(str)
  }

  getDateForIndex(index) {
    var output

    var events = this.state.events
    if (events && events.length > index) {
      output = events[index].title
    }

    return output
  }

  getIndexForDate(date, newEvents) {
    var output

    var array = newEvents || this.state.events

    if (array) {
      array.map((value, index) => {
        var { title } = value
        if (title === date) {
          output = index
        }
      })
    }

    return output
  }

  getFirstDate() {
    return this.strToMoment(this.getDateForIndex(0))
  }

  getCurrentIndex(newEvents) {
    var index = this.getIndexForDate(
      this.momentToStr(this.currentVisibleDate),
      newEvents
    )

    return index
  }

  updateScrollTo(newEvents) {
    var index = this.getIndexForDate(
      this.momentToStr(this.currentVisibleDate),
      newEvents
    )
    if (index > -1) {
      this.currentVisibleIndex = index

      this.setState({
        currentVisibleIndex: index
      })
    }

    //this.scrollToRef = this.strToMoment(this.currentVisibleDate)
  }

  updateEvents(nextProps) {
    var newEvents = this.getGroupedEvents(nextProps)

    var index = this.getCurrentIndex(newEvents)
    this.currentVisibleIndex = index

    this.setState({
      currentVisibleIndex: index,
      events: newEvents
    })
  }

  /*scrollToDate() {
    var date = this.momentToStr(this.currentVisibleDate)
    if (date) {
      if (this.itemsRefs[date]) {
        this.itemsRefs[date].measure((ox, oy, width, height, px, py) => {
          console.log(ox, oy, width, height, px, py)

          this.list.scrollToOffset({
            offset: py + oy + height,
            animated: false
          })
        })
      }
    }
  }*/

  getOffset() {
    let scrollPosition = 0
    for (let i = 0; i <= this.state.currentVisibleIndex; i++) {
      var itemHeight = this.itemHeightForData(this.state.events[i])
      scrollPosition += itemHeight
    }

    return scrollPosition
  }

  scrollToSelected() {
    if (this.list) {
      this.isDynamicScroll = true
      var scrollPosition = this.getOffset()

      setTimeout(() => {
         this.list.scrollToOffset({
           offset: scrollPosition,
           animated: false
         })
        this.isDynamicScroll = false
      }, 80)
    }
  }

  // scrollTillCurrentDate() {
  //   if (this.list) {
  //     var maxOffset = this.getOffset() * 2
  //     var initOffset = 182
  //     // var beforeDate = moment(this.currentVisibleDate).isBefore(
  //     //   this.state.clonedDate
  //     // )
  //     while (initOffset < maxOffset) {
  //       this.list.scrollToOffset({
  //         offset: initOffset,
  //         animated: false
  //       })

  //       initOffset += 182
  //     }
  //   }
  // }

  _keyExtractor = (item, index) => item.title

  onDayPress(day) {
    this.setState({
      currentMonth: day
    })

    var { onDayPress } = this.props
    if (onDayPress) {
      onDayPress(day)
    }
  }

  fetchPast() {
    if (!this.state.isFetching) {
      var { setStartDate } = this.props
      if (setStartDate) {
        var newStart = moment(initMonthRange.start)
          .subtract(1, 'month')
          .startOf('month')
          .startOf('week')

        var newDay = {
          dateString: newStart
        }

        this.onDayPress(newDay)

        if (newStart.isBefore(initMonthRange.start)) {
          initMonthRange.start = newStart
          setStartDate(newStart.format('YYYY-MM-DD').toString())
        }
      }
    }
  }

  fetchFuture() {
    if (!this.state.isFetching) {
      var { setEndDate } = this.props
      if (setEndDate) {
        var newEnd = moment(initMonthRange.end)
          .add(1, 'month')
          .endOf('month')
          .endOf('week')

        var newDay = {
          dateString: newEnd
        }

        this.onDayPress(newDay)

        if (newEnd.isAfter(initMonthRange.end)) {
          initMonthRange.end = newEnd
          setEndDate(newEnd.format('YYYY-MM-DD').toString())
        }
      }
    }
  }

  setVisibleIndex(index, key) {
    var output = 0

    if (index) {
      output = index
    } else {
      var tryIndex = this.getIndexForDate(key)
      if (tryIndex) {
        output = tryIndex
      }
    }

    return output
  }

  setVisibleDate(key, item) {
    return moment(key || item.title)
  }

  _onViewableItemsChanged({ viewableItems, changed }) {
    if (viewableItems && viewableItems.length > 0) {
      var { isViewable, index, key, item } = viewableItems[0]
      if (isViewable) {
        var visibleIndex = this.setVisibleIndex(index, key)
        var visibleDay = this.setVisibleDate(key, item)

        this.currentVisibleDate = visibleDay
      }
    }
  }

  _onScroll(event) {
    var currentOffset = event.nativeEvent.contentOffset.y
    var direction = currentOffset > this.offset ? 'down' : 'up'
    this.offset = currentOffset

    if (!this.isDynamicScroll) {
      if (direction === 'up' && isCloseToTop(event.nativeEvent)) {
        this.fetchPast()
      }

      if (direction === 'down' && isCloseToBottom(event.nativeEvent)) {
        this.fetchFuture()
      }
    }
  }

  onListTouch() {
    this.isDynamicScroll = false
  }

  getHeightForIndex(data, index) {
    // return this.heights[index] not usable for preloaded rows
    return this.itemHeightForData(this.state.events[index])
  }

  onRowLayoutChange(ind, event) {
    this.heights[ind] = event.nativeEvent.layout.height
  }

  _renderItem({ item, index }) {
    var { title, data } = item

    return (
      <View
        onLayout={this.onRowLayoutChange.bind(this, index)}
        ref={i => (this.itemsRefs[title] = i)}
      >
        <EventsListView
          navigation={this.props.navigation}
          error={this.props.error}
          loading={this.props.loading}
          events={data}
          selectedDate={title}
        />
      </View>
    )
  }

  getGroupedEvents(props) {
    var output = []

    var groupedEvents = {}

    var { events } = props || this.props
    if (events) {
      events.map(value => {
        var { startDate } = value
        if (startDate) {
          var dateKey = moment(startDate)
            .format('YYYY-MM-DD')
            .toString()

          var containingEvents = []
          if (groupedEvents[dateKey]) {
            if (groupedEvents[dateKey] && groupedEvents[dateKey].length > 0) {
              containingEvents = groupedEvents[dateKey]
            }
          }

          containingEvents.push(value)

          groupedEvents[dateKey] = containingEvents
        }
      })
    }

    Object.keys(groupedEvents)
      .sort((a, b) => {
        return (
          moment(a)
            .toDate()
            .getTime() -
          moment(b)
            .toDate()
            .getTime()
        )
      })
      .map(key => {
        output.push({ title: key, data: groupedEvents[key] })
      })

    return output
  }

  _renderSeparator = () => <View style={style.itemSeparator} />

  render() {
    var { loading } = this.props

    return (
      <View style={{ flex: 1 }}>
        {this.state.events && this.state.events.length > 0 ? (
          <FlatList
            style={{ flex: 1 }}
            ref={c => (this.list = c)}
            data={this.state.events}
            extraData={this.state}
            keyExtractor={this._keyExtractor}
            renderItem={this._renderItem.bind(this)}
            onScroll={this._onScroll.bind(this)}
            onViewableItemsChanged={this._onViewableItemsChanged}
            showsVerticalScrollIndicator={false}
            scrollEventThrottle={200}
            getItemLayout={(data, index) => ({
              length: this.getHeightForIndex(index),
              offset: this.getHeightForIndex(index) * index,
              index
            })}
            onMoveShouldSetResponderCapture={() => {
              this.onListTouch()
              return false
            }}
            ItemSeparatorComponent={this._renderSeparator}
          />
        ) : (
          <View
            style={[
              style.nothingYet,
              globalStyle.shadowedRowView,
              { marginRight: 0, marginLeft: 0 }
            ]}
          >
            <Text style={style.nothingYetText}>{`${
              loading ? 'Loading' : 'Nothing Yet'
            }...`}</Text>
          </View>
        )}
      </View>
    )
  }
}
Run Code Online (Sandbox Code Playgroud)

我想要一种有效的方法在新更新之前滚动到上一个可见的顶行。

非常感谢。

PS 我本来打算使用https://github.com/wix/react-native-calendars议程视图,但他们不支持向上滚动以显示获取的新项目,我想我遇到的问题与本机相同编程 iOS 和 Android,这相当简单,例如https://developer.apple.com/documentation/uikit/uitableview/1614879-insertrows将特定数据添加到特定索引或简单地https://developer.apple.com/ Documentation/uikit/uitableview/1614997-scrolltorowatindexpath滚动到所需的可见行。