Q: Capturing global gestures (How to build the AppStore "BUY" button) I would like to have an AppStore-like buy process in my app.


*

*On first touch, the BUY button changes, asking the user to confirm.

*If the user touches anywhere else on the screen, the BUY button changes back to normal. The regular touch was intercepted.


I'm not sure what is the best way to do (2) in react-native.
I can intercept touches using the responder system (i.e. PanResponder) for the whole screen. But then the touch will never reach the BUY button (the TouchableWithHighlight control).
The touch event contains the node id of the element being touched, which could be compared with the desired element, but this is very hard, because the touchable area may contain more than one node.
How could this be approached?

A: I have currently settled on this approach:


*

*A global intercept component can be set to "block gestures except this view".

*The screen coordinates of that view are calculated.

*Touches within this screen rect are passed through.


This works sort of well. Since scrolling does not need to work while in intercept mode, we don't have to consider the target position on the screen changing. However, animations might be problematic.
var EventEmitter = require('events').EventEmitter
UIManager = require('NativeModules').UIManager;

var CaptureAll = React.createClass({
  _panResponder: {},
  _isBlocking: false,
  _allowedRect: null,

  componentWillMount: function() {
    this._panResponder = PanResponder.create({
      onStartShouldSetPanResponderCapture: this._handleStartShouldSetPanResponderCapture,
      onMoveShouldSetPanResponderCapture: this._handleMoveShouldSetPanResponderCapture,
      onPanResponderGrant: this._handlePanResponderGrant,
      onPanResponderMove: this._handlePanResponderMove,
      onPanResponderRelease: this._handlePanResponderEnd,
      onPanResponderTerminate: this._handlePanResponderEnd,
    });

    this.eventEmitter = new EventEmitter();
  },

  render: function() {
    return (
      <View style={this.props.style} {...this._panResponder.panHandlers}>{this.props.children}</View>
    );
  },

  _handleStartShouldSetPanResponderCapture: function(e: Object, gestureState: Object): boolean {
    // If we are not blocking, do not intercept
    if (!this.isBlocking)
      return false;

    // Let the touch through if it is in the allowed rect
    if (this.allowedRect && e.touchHistory.indexOfSingleActiveTouch) {
      var touch = e.touchHistory.touchBank[e.touchHistory.indexOfSingleActiveTouch];
      var r = this.allowedRect;
      if (touch.currentPageX >= r.x && touch.currentPageX <= (r.x + r.w)
        && touch.currentPageY >= r.y && touch.currentPageY <= (r.y + r.h)
      )
        return false;
    }

    // Intercept and block this touch
    this.eventEmitter.emit('block', { panEvent: e });
    return true;
  },

  _handleMoveShouldSetPanResponderCapture: function(e: Object, gestureState: Object): boolean {
    return this._handleStartShouldSetPanResponderCapture(e, gestureState);
  },

  _handlePanResponderGrant: function(e: Object, gestureState: Object) {},
  _handlePanResponderMove: function(e: Object, gestureState: Object) {},
  _handlePanResponderEnd: function(e: Object, gestureState: Object) {},

  blockExceptRect: function(x, y, w, h) {
    this.isBlocking = true;
    this.allowedRect = {x, y, w, h}
  },

  blockExceptComponent: function(component) {
    // Do not wait for the callback to block
    this.isBlocking = true;
    this.allowedRect = null;

    let handle = React.findNodeHandle(component);
    UIManager.measure(
      handle,
      (x, y, w, h, px, py) => {
        this.blockExceptRect(px, py, w, h);
      });
  },

  unblock: function() {
    this.isBlocking = false;
    this.allowedRect = null;
  },

  addListener: function(callback) {
    this.eventEmitter.addListener('block', callback);
  }
});

