React Native动态更改响应器

10
我正在使用react-native进行Android开发。我有一个视图,如果用户长按,我想显示一个可以拖动的动画视图。我使用PanResponder实现了这一点,效果很好。
但我想做的是当用户长按时,用户应该能够继续同一次触摸/按下并拖动新显示的Animated.View。
如果您熟悉Google Drive应用程序,则具有类似功能。当用户长按列表中的任何项目时,它会显示可拖动项。用户可以立即拖动该项。

enter image description here

我认为,如果在可拖动项开始显示后动态更改Responder,则可以使其正常工作。
问题是,React Native是否提供了动态更改响应器的方法?
到目前为止,我尝试过以下方法:
- 我尝试通过更改onStartShouldSetPanResponderCaptureonMoveShouldSetPanResponderCaptureonMoveShouldSetPanResponderonPanResponderTerminationRequest的逻辑来实现,以便在可拖动项开始显示时,容器视图不会捕获开始和移动事件,并接受终止请求,同时对于它的应该捕获事件返回true。 - 一个可行的解决方法是将可拖动项显示在容器的顶部,不透明度较低,并将其捕获设置为false。当用户长按它时,我会更改它的不透明度,以便清晰地显示它。使用这种方法,用户可以继续触摸以拖动该项。但是,容器实际上是一个列表行。因此,我需要创建许多可拖动项,因为用户可以长按任何一行。

但我认为这不是一个好的解决方案,如果我能改变回答者,那就太好了。


相关链接:https://github.com/facebook/react-native/issues/7941 - mquandalle
1个回答

9

简单回答

据我所知,你不能动态地改变视图的响应者。

onStartShouldSetPanResponderCapture这样的方法不适用于你试图拖动的子视图,原因是这些方法在触摸开始时被触发,并且根据定义,在触摸开始时实现onStartShouldSetPanResponderCapture的子视图在此时尚不存在。

但是,凭借手势响应器方法不必在子视图上实现:

解决方案

从实现中退后一步,实际所需的功能是应用程序中的某个组件需要成为手势响应器。当手势响应器移动时,您会收到触摸事件。此时,您可以使用setNativeProps在子视图上反映出手势中的更改。

因此,如果要移动子视图,则无需将该子视图实际上作为响应者。您只需使父级成为响应者,然后从父级更新子属性。

下面我已经实现了一个示例应用程序,并对正在进行的步骤进行了逐步说明:

  1. 您有一个渲染ListView的组件。该ListView是您的手势响应器。列表视图中的每个单元格都具有响应长按的TouchableOpacity

  2. 当发生长按事件(行触发了onLongPress属性)时,您会使用浮动视图重新呈现父组件。这个视图的绝对位置由您的父组件拥有的两个属性this._previousLeftthis._previousTop控制。

  3. 现在,这个浮动子视图不关心响应触摸。父级已经在响应。所有孩子关心的只是它的两个位置属性。因此,要移动浮动子组件,您只需使用由ListView提供的_handlePanResponderMove函数在子View组件的setNativeProps上更新其topleft属性。

总结

当处理触摸时,你不需要被移动的组件实际上是监听触摸事件的组件。被移动的组件只需要由任何正在监听触摸事件的组件更新其位置属性。

这是Google Drive应用程序中描述的长按/拖动手势的完整代码:

import React, { PropTypes } from 'react';
import {
  AppRegistry,
  ListView,
  PanResponder,
  StyleSheet,
  Text,
  TouchableOpacity,
  View,
} from 'react-native';

class LongPressDrag extends React.Component {

  constructor() {
    super();

    this._panResponder = PanResponder.create({
      onStartShouldSetPanResponder: this._handleStartShouldSetPanResponder.bind(this),
      onMoveShouldSetPanResponder: this._handleMoveShouldSetPanResponder.bind(this),
      onPanResponderMove: this._handlePanResponderMove.bind(this),
      onPanResponderRelease: this._handlePanResponderEnd.bind(this),
      onPanResponderTerminate: this._handlePanResponderEnd.bind(this),
    });
    this._previousLeft = 0;
    this._previousTop = 0;
    this._floatingStyles = {
      style: {
        left: this._previousLeft,
        top: this._previousTop,
        position: 'absolute',
        height: 40,
        width: 100,
        backgroundColor: 'white',
        justifyContent: 'center',
      }
    };

    const rows = Array(11).fill(11).map((a, i) => i);
    this.state = {
      dataSource: new ListView.DataSource({
        rowHasChanged: (row1, row2) => row1 !== row2,
      }).cloneWithRows(rows),
      nativeEvent: undefined,
      //Pan Responder can screw with scrolling.  See https://github.com/facebook/react-native/issues/1046
      scrollEnabled: true,
    }
  }

  getDragElement() {
    if (!this.state.nativeEvent) {
      return null;
    }
    return (
      <View
        style={[this._floatingStyles.style,
          {top: this._previousTop, left: this._previousLeft}
        ]}
        ref={(floating) => {
          this.floating = floating;
        }}
      >
        <Text style={{alignSelf: 'center'}}>Floating Item</Text>
      </View>
    )
  }

  render() {
    return (
      <View>
        <ListView
          dataSource={this.state.dataSource}
          renderRow={this.renderRow.bind(this)}
          style={styles.container}
          scrollEnabled={this.state.scrollEnabled}
          {...this._panResponder.panHandlers}
        />
        {this.getDragElement.bind(this)()}
      </View>
    )
  }

  renderRow(num) {
    return (
      <TouchableOpacity
        style={styles.cell}
        onLongPress={this.handleLongPress.bind(this)}
        onPressIn={this.handlePressIn.bind(this)}
      >
        <Text style={styles.title}>{`Row ${num}`}</Text>
      </TouchableOpacity>
    );
  }

  handleLongPress(event) {
    console.log(event);
    this.setState({
      nativeEvent: event.nativeEvent,
      scrollEnabled: false,
    })
  }

  handlePressIn(event) {
    this._previousLeft = event.nativeEvent.pageX - 50;
    this._previousTop = event.nativeEvent.pageY - 20;
  }

  _updateNativeStyles() {
    this.floating && this.floating.setNativeProps({style: {left: this._previousLeft, top: this._previousTop}});
  }

  _handleStartShouldSetPanResponder(e, gestureState) {
    return true;
  }

  _handleMoveShouldSetPanResponder(e, gestureState) {
    return true;
  }

  _handlePanResponderMove(event, gestureState) {
    this._previousLeft = event.nativeEvent.pageX - 50;
    this._previousTop = event.nativeEvent.pageY - 20;
    this._updateNativeStyles();
  }

  _handlePanResponderEnd(e, gestureState) {
    this._previousLeft += gestureState.dx;
    this._previousTop += gestureState.dy;
    this.setState({ nativeEvent: undefined, scrollEnabled: true})
  }

}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    marginTop: 20,
  },
  cell: {
    flex: 1,
    height: 60,
    backgroundColor: '#d3d3d3',
    borderWidth: 3,
    borderColor: 'white',
    justifyContent: 'center',
  },
  title: {
    paddingLeft: 20,
  },
});

AppRegistry.registerComponent('LongPressDrag', () => LongPressDrag);

这对我来说在RN 0.29上是有效的。我相信这里可以做很多优化,但我只是想在短暂的一天内试图阐述一般概念。

希望这能帮到你!


我刚在 Android(Nexus 5X 模拟器)上运行了它,它正常工作。与 iOS 相比,onLongPress 的触发时间只是需要非常长的时间。因此,也许你需要尝试调整 delayLongPress 属性或其他内容。但是,对我来说,该代码片段在两个平台上都可以正常工作。 - Michael Helvey
今天我再次使用RN 0.31-rc.0 进行测试,长按事件仍然没有被触发。我怀疑这是React Native中的一个bug,如果下周左右还没有修复,我打算开始制作一个最小限度的复现模型。 - mquandalle
哦,好的,知道了。 - Michael Helvey
@MichaelHelvey,抱歉回复晚了,我因为其他工作忙碌,无法及时查看。我认为“当您处理触摸时,您不需要移动的组件实际上是正在侦听触摸事件的组件”真的很关键。这非常有帮助,非常感谢! - Abhay
@mquandalle,关于长按不起作用的问题,我之前在Chrome调试时也遇到过这个问题。但是关闭调试后,长按功能对我来说就正常了。 - Abhay
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接