【问题标题】:React Native - How to simulate a RefreshControl in a horizontal scrollable listReact Native - 如何在水平滚动列表中模拟 RefreshControl
【发布时间】:2017-07-19 14:49:26
【问题描述】:

问题

我正在开发一个水平滚动列表,每次滑动时总是滚动一个整个屏幕(即一个“页面”)。当它在第一页并且用户再次向右拉时,我需要刷新整个列表

对于垂直ScrollView,如果用户在视图位于其最顶部位置时向下拉,则会出现标准RefreshControl。但是对于横版来说,没有标准的解决方案。

我的实验

想法

我尝试添加PanResponder 来捕捉触摸和移动事件。请注意,我在我的应用程序中使用了VirtualizedList。基本逻辑是:

  • 我创建了一个隐藏的View(其宽度默认设置为0),其中包含ActivityIndicator,并将此视图作为ListHeaderComponent插入VirtualizedList
  • 我监听VirtualizedListonScroll事件并不断保存最新的滚动位置
  • 当滚动位置接近 0,并且用户向右拉动(通过 PanResponder 捕获的事件手势),然后我使用 ActivityIndi​​cator 为隐藏视图的宽度设置动画以模拟 RefreshControl 行为

结果

但是,PanResponder 无法正确捕获所需的事件。大多数情况下,当我在VirtualizedList 的开头(在Android 设备上)向右拉时,只会出现标准的击球边缘动画(出现渐变蓝色层)。

通过添加一些日志,我发现onPanResponderGrant 偶尔会被触发,但onPanResponderMoveonPanResponderReleaseonPanResponderTerminateonPanResponderTerminationRequest 都不会被触发。

onPanResponderGrant被触发时,隐藏的ActivityIndicator偶尔会部分出现。 “向右拉”效果只会很快出现,并且总是会立即停止,只显示隐藏视图的非常窄的部分。

我猜底层ScrollView 窃取了事件并阻止我的代码响应。但即使我将onPanResponderTerminationRequest 设置为始终返回false 仍然无济于事。即使我直接将VirtualizedList 替换为ScrollView 也会产生相同的结果。

有谁知道当 ScrollView 滚动到顶部时如何正确捕获触摸和移动事件?还是有另一种可行的方法来实现RefreshControl 的水平版本?

实验代码

import React from 'react'
import {
  Text, View, ActivityIndicator, ScrollView, VirtualizedList,
  Dimensions, PanResponder, Animated, Easing
} from 'react-native'


let DATA = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20];

class Test extends React.PureComponent {

  constructor(props) {
    super(props);

    this.state = {
      refreshIndicatorWidth: new Animated.Value(0),
    };

    this.onScroll = evt => this._scrollPos = evt.nativeEvent.contentOffset.x;
    this.onPullToRefresh = dist => Animated.timing(this.state.refreshIndicatorWidth, {
      toValue: dist,
      duration: 0,
      easing: Easing.linear,
    }).start();
    this.resetRefreshStatus = () => {
      Animated.timing(this.state.refreshIndicatorWidth, {
        toValue: 0,
      }).start();
    };

    const shouldRespondPan = ({dx, dy}) => (this._scrollPos||0) < 50 && dx > 10 && Math.abs(dx) > Math.abs(dy);
    this._panResponder = PanResponder.create({
      onMoveShouldSetPanResponder: (evt, gestureState) => {
        return shouldRespondPan(gestureState)
      },
      onMoveShouldSetPanResponderCapture: (evt, gestureState) => {
        return shouldRespondPan(gestureState)
      },
      onPanResponderGrant: (evt, gestureState) => {
        console.warn('----pull-Grant: ' + JSON.stringify(gestureState));
      },
      onPanResponderMove: (evt, gestureState) => {
        console.warn('----pull-Move: ' + JSON.stringify(gestureState));
        this.onPullToRefresh(gestureState.dx)
      },
      onPanResponderTerminationRequest: () => {
        console.warn('----pull-TerminationRequest: ' + JSON.stringify(gestureState));
        return false
      },
      onPanResponderRelease: (evt, gestureState) => {
        console.warn('----pull-Release: ' + JSON.stringify(gestureState));
        this.props.refreshMoodList();
      },
      onPanResponderTerminate: () => {
        console.warn('----pull-Terminate: ' + JSON.stringify(gestureState));
        this.resetRefreshStatus();
      },
    });

    this.renderRow = this.renderRow.bind(this);
  }

  componentDidUpdate(prevProps, prevState, prevContext) {
    console.warn('----updated: ' + JSON.stringify(this.props));
    this.resetRefreshStatus();
  }

  renderRow({item, index}) {
    return (
      <View style={{flex: 1, alignItems: 'center', justifyContent: 'center', width: Dimensions.get('window').width, height: '100%'}}>
        <Text>{item}</Text>
      </View>
    );
  }

  render() {
    /******* Test with a VirtualizedList *******/
    return (
      <VirtualizedList
        {...this._panResponder.panHandlers}
        data={DATA}
        ListEmptyComponent={<View/>}
        getItem={getItem}
        getItemCount={getItemCount}
        keyExtractor={keyExtractor}
        renderItem={this.renderRow}
        horizontal={true}
        pagingEnabled={true}
        showsHorizontalScrollIndicator={false}
        ListHeaderComponent={(
          <Animated.View style={{flex: 1, height: '100%', width: this.state.refreshIndicatorWidth, justifyContent: 'center', alignItems: 'center'}}>
            <ActivityIndicator size="large"/>
          </Animated.View>
        )}
        onScroll={this.onScroll}
      />
    );
    /******* Test with a ScrollView *******/
    return (
      <View style={{height:Dimensions.get('window').height}}>
        <ScrollView
          {...this._panResponder.panHandlers}
          horizontal={true}
        >
          <View style={{width: Dimensions.get('window').width * 2, height: 100, backgroundColor: 'green'}}>
            <Text>123</Text>
          </View>
        </ScrollView>
      </View>
    );
  }
}

const getItem = (data, index) => data[index];
const getItemCount = data => data.length;
const keyExtractor = (item, index) => 'R' + index;

export default Test;

临时解决方法(不是那么完美)

  • 直接在onPanResponderGrant内部触发刷新事件(以及动画):这样,我仍然无法跟踪触摸动作。所以滚动窗格不能跟随触摸点。

  • onPanResponderMove中的操作移动到onMoveShouldSetPanResponder附加条件(例如添加我自己的“授予”条件判断):这样我可以跟踪触摸动作,但是由于我没有真正的“授权”,我无法捕获*Release/*Terminate/*End 事件(即,我不知道触摸事件何时结束,因此永远不知道何时触发刷新事件!)

完美解决方案

等待您的想法...

【问题讨论】:

    标签: react-native horizontalscrollview react-native-scrollview


    【解决方案1】:

    您可以使用contentOffset 来确定用户是否在水平列表中“拉动刷新”。以下内容适用于FlatListScrollView

    onScroll={(e) => {
      const xOffset = e.nativeEvent.contentOffset.x;
    
      // logic goes here to handle a negative value
    
    }}
    scrollEventThrottle={16}
    

    当 xOffset 达到某个值时,您可以有条件地做某事或设置一个标志。希望这能让你开始!

    【讨论】:

    • 您还可以更改scrollEventThrottle 以确定onScroll 被触发的频率,以免丢失动画帧
    • 感谢您的建议。这应该适用于 iOS 设备,但对于 Andriod,*List/ListView/ScrollView 在到达其顶部后只会停止滚动,因此不会出现负滚动位置。这就是为什么我一直在努力实现自己的PanResponder
    • @DeNG 提到的不能在 android 上工作
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-04-28
    • 2020-03-26
    • 2018-08-15
    • 2022-01-22
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多