Fixing ghost panning – a reboot

In my last post about gesture decorators I came up with a rather crude solution to fix the ghost panning issue. It turns out that there is a much cleaner, much more elegant solution to this problem.

Why does ghost panning appear at all? It’s because our wrapping elements take up as much space as possible – due to flexbox. So what if we could force the wrappers to effectively display like an inline item, so that it would only take up as much space as necessary?

Enter alignSelf: 'flex-start' #

After attending React Europe and chatting with @vjeux it turns out we have a much cleaner method of getting our wrappers to behave properly.

By adding alignSelf: 'flex-start' to the wrapper’s style, we achieve exactly what is written above – an inline version of our element, which automatically resizes itself to its content’s size.

This enables us to remove the cruft which has been added with the last iteration and substitute it with a very tiny piece. But it does not only clean up our code, it allows to really compose our decorators together, as there is no need for any size updates to bubble up the tree. Our decorators just wrap themselves around the components as tight as possible.

Old vs new #

Here is a quick comparison of the changed parts for the panning decorator using the old and the new method:

Constructor #

before #

constructor(props, context) {
  super(props, context);

  this.lastX = 0;
  this.lastY = 0;

  this.state = {
    absoluteChangeX: 0,
    absoluteChangeY: 0,
    changeX: 0,
    changeY: 0,
    decoratedViewWidth: null,
    decoratedViewHeight: null
  };
}

after #

constructor(props, context) {
  super(props, context);

  this.lastX = 0;
  this.lastY = 0;

  this.state = {
    absoluteChangeX: 0,
    absoluteChangeY: 0,
    changeX: 0,
    changeY: 0
  };
}

componentDidMount #

before #

componentDidMount() {
  setTimeout(() => {
    UIManager.measure(React.findNodeHandle(this.refs.decorated), (x, y, w, h) => {
      this.setState({
        decoratedViewHeight: h,
        decoratedViewWidth: w
      });
    }, 0);
  });
}

after #

poof – gone!

render #

before #

render() {
  const {
    onPanBegin,
    onPan,
    onPanEnd,
    panningDecoratorStyle,
    ...props
  } = this.props;

  const state = {
    decoratedViewWidth: width,
    decoratedViewHeight: height,
    ...rest
  } = this.state;

  const style = {
    ...panningDecoratorStyle,
    width,
    height
  };

  return (
    <View {...this._panResponder.panHandlers} style={style}>
      <BaseComponent ref="decorated" {...props} {...rest} />
    </View>
  );
}

after #

render() {
  const {
    onPanBegin,
    onPan,
    onPanEnd,
    panningDecoratorStyle,
    ...props
  } = this.props;

  const style = {
    ...panningDecoratorStyle,
    alignSelf: 'flex-start'
  };

  return (
    <View {...this._panResponder.panHandlers} style={style}>
      <BaseComponent {...props} {...this.state} />
    </View>
  );
}

As you can see, this tiny change reduces our complexity and gets rid of any need for a ref or a hacky setTimeout.

Conclusion #

This is a major win – simpler code, more flexibility!

I have just released a gesture decorator package which contains two decorators ready for use. The package is a work in progress and I have a few ideas to make the gesture decorators way more usable/reusable than they are now. But it is too early to go into details. For now: enjoy!

Gesture detection in React Native – fixing unexpected panning

In my previous post on gesture recognition I described how to use the PanResponder to detect a panning gesture and how to use it to transform a view’s position.

That method had an unfortunate flaw though: because we are wrapping the decorated component inside a View, it can happen that touches are registered where they shouldn’t be: outside of the wrapped view. This happens because the wrapper view – by default – stretches across the whole screen due to flexbox. And when we translate the wrapped view with a transform, the wrapper view stays in place, which leads to this “ghost-touch” behavior.

Thankfully the solution to this problem is not very hard. Instead of transforming the wrapped view, we transform the wrapper view instead!

Updating the panning decorator #

We have to make a few adjustments to our decorator for this. Here they are:

New default values #

constructor(props, context) {
  super(props, context);

  this.lastX = 0;
  this.lastY = 0;

  this.state = {
    absoluteChangeX: 0,
    absoluteChangeY: 0,
    changeX: 0,
    changeY: 0,
    decoratedViewWidth: null,
    decoratedViewHeight: null
  };
}

We have to add the decoratedViewWidth and decoratedViewHeight properties to our state. Those will be used to adjust the size of the wrapper view to fit the wrapped view.

Detecting the wrapped view’s dimensions #

componentDidMount() {
  setTimeout(() => {
    UIManager.measure(React.findNodeHandle(this.refs.decorated), (x, y, w, h) => {
      this.setState({
        decoratedViewHeight: h,
        decoratedViewWidth: w
      });
    }, 0);
  });
}

As soon as the component is mounted, we measure the wrapped component. The ref we are using for this will be assigned in render. After receiving the dimensions we set them in our state. The measurement has to be done within a setTimeout because else measure() will only return zero values.

Updating the render function #

render() {
  const {
    onPanBegin,
    onPan,
    onPanEnd,
    panningDecoratorStyle,
    ...props
  } = this.props;

  const state = {
    decoratedViewWidth: width,
    decoratedViewHeight: height,
    ...rest
  } = this.state;

  const style = {
    ...panningDecoratorStyle,
    width,
    height
  };

  return (
    <View {...this._panResponder.panHandlers} style={style}>
      <BaseComponent ref="decorated" {...props} {...rest} />
    </View>
  );
}

We now support a new prop – panningDecoratorStyle. This prop can be used to style the wrapper view. The width and height properties will be overwritten though, as we set them to our previously measured values.

The updated decorator #

Here is the whole code for the updated decorator:

'use strict';

import React, { Component, View, PanResponder, NativeModules } from 'react-native';
const { UIManager } = NativeModules;

export default BaseComponent => {
  return class extends Component {

    constructor(props, context) {
      super(props, context);

      this.lastX = 0;
      this.lastY = 0;

      this.state = {
        absoluteChangeX: 0,
        absoluteChangeY: 0,
        changeX: 0,
        changeY: 0,
        decoratedViewWidth: null,
        decoratedViewHeight: null
      };
    }

    componentDidMount() {
      setTimeout(() => {
        UIManager.measure(React.findNodeHandle(this.refs.decorated), (x, y, w, h) => {
          this.setState({
            decoratedViewHeight: h,
            decoratedViewWidth: w
          });
        });
      }, 0);
    }

    componentWillMount() {
      this._panResponder = PanResponder.create({

        onStartShouldSetPanResponder: ({ nativeEvent: { touches } }, { x0, y0 }) => {
          const shouldSet = touches.length === 1;

          if (shouldSet) {
            const { onPanBegin } = this.props;
            onPanBegin && onPanBegin({
              originX: x0,
              originY: y0
            });
          }

          return shouldSet;
        },

        onMoveShouldSetPanResponder: ({ nativeEvent: { touches } }) => {
          return touches.length === 1;
        },

        onPanResponderMove: (evt, { dx, dy }) => {
          const { onPan } = this.props;
          const newState = {
            absoluteChangeX: this.lastX + dx,
            absoluteChangeY: this.lastY + dy,
            changeX: dx,
            changeY: dy
          };

          this.setState(newState);

          onPan && onPan(newState);
        },

        onPanResponderTerminationRequest: () => true,
        onPanResponderTerminate: this.handlePanResponderRelease,
        onPanResponderRelease: this.handlePanResponderRelease
      });
    }

    handlePanResponderRelease = () => {
      const { onPanEnd } = this.props;
      this.lastX = this.state.absoluteChangeX;
      this.lastY = this.state.absoluteChangeY;
      onPanEnd && onPanEnd();
    }

    render() {
      const {
        onPanBegin,
        onPan,
        onPanEnd,
        panningDecoratorStyle,
        ...props
      } = this.props;

      const state = {
        decoratedViewWidth: width,
        decoratedViewHeight: height,
        ...rest
      } = this.state;

      const style = {
        ...panningDecoratorStyle,
        width,
        height
      };

      return (
        <View {...this._panResponder.panHandlers} style={style}>
          <BaseComponent ref="decorated" {...props} {...rest} />
        </View>
      );
    }
  };
}

Translating the view #

Instead of transforming the wrapped view, we will transform the wrapper. We create a new component for this:

class TransformOnPan extends Component {

  constructor(props, context) {
    super(props, context);
    this.state = {
      transform: null
    }
  }

  onPan = ({ absoluteChangeX, absoluteChangeY, changeX, changeY}) => {
    this.setState({
      transform: [
        {translateX: absoluteChangeX},
        {translateY: absoluteChangeY}
      ]
    });
  }

  render() {
    const { transform } = this.state;
    return (
      <DecoratedWithPanning
        panningDecoratorStyle={{transform}}
        onPan={this.onPan}
      />
    );
  }
}

This component takes care of rendering our decorated component and registers a handler for onPan. Whenever this callback is called, we will update this.state.transform and re-render, while passing the transform to panningDecoratorStyle.

With the above changes we can now transform a view using translate and avoid ghost-panning.

Here is a before and after comparison:

ghost panning before fix no ghost panning after fix

An alternate solution for detecting the child view’s size #

In the above solution we relied on UIManager and did some setTimeout trickery to get the view size. Instead of doing that we could also pass a method to onLayout of the wrapped view and update our view’s size using the event data:

onLayout = ({ nativeEvent: { layout : { width, height } } }) => {
  this.setState({
    decoratedViewHeight: height,
    decoratedViewWidth: width
  });
}

The upside is that we do not need to use setTimeout or a ref to access the child view. The downside is that we have to pass the onLayout method to the wrapped view:

return (
  <View {...this._panResponder.panHandlers} style={style}>
    <BaseComponent {...props} {...rest} onLayout={this.onLayout} />
  </View>
);

That itself is not a problem, but the user now has to pass this prop as their view’s onLayout callback. If this assignment is omitted, the decorator will again not work properly.

Conclusion #

Both these methods help us to get rid of ghost-panning and both are valid. Using the UIManager seems a bit hacky, but does not require any additional action from the user. onLayout seems cleaner, but requires the user to be more careful when using the decorator. Use whichever method you are more comfortable with.

This issue makes it tricky to compose multiple gesture decorators together. Each decorator has to resize itself, which can lead to race conditions. This is something that needs further investigation. But for a single decorator, this fix will work.

Gesture detection in React Native

Gestures are one of the most prevalent things on mobile. Without them our apps wouldn’t be able to function – nobody could navigate anywhere! React Native comes with basic gesture support and allows us to use TouchableOpacity and friends to create views which are tappable. But what about other gestures like panning or pinch-zooming? Those we have to implement on our own – but that’s not as bad as it sounds. In fact, implementing the two mentioned gestures is quite straight forward, so that’s what we are going to do! (This post will only show the panning gesture, pinch-zooming will follow in a future post)

We will discuss the following topics:

PanResponder and the gesture responder system #

The PanResponder and the gesture responder system are what we will be using to handle touches. The difference between those two is that the PanResponder provides a bit of sugar on top of responder system, mainly the gestureState argument which gets passed to the event handlers in addition to the event.

There are eleven handlers that we can attach to our view in order to be notified of events during the touch lifecycle – and we will be needing most of them! Check the docs which I’ve linked above for more details about each of them.

Finding a way to implement our gestures #

In order to implement our gestures, it would make sense to make them as generic as possible, so we can reuse them with different components. Using a higher order component or decorator is a good way to achieve this. It allows us to create an api like this:

MyComponent = makePannable(MyComponent);

Not bad, but how about combining multiple gestures:

MyComponent = makePannable(makePinchZoomable(MyComponent));

That is pretty nice, isn’t it? We can easily enhance our component with multiple gestures, as long as the gestures itself to not interfere with each other.

Since React Native switched over to Babel for code-transpilation, we can make use of the proposed ES7 decorator syntax:

@makePannable
@makePinchZoomable
class MyComponent extends React.Component {
 /* ... */ 
}

Now if that’s not a concise and clean way to enhance your components with gestures, I don’t know what is.

What is a decorator?

In case you are not familiar with the term, here is a quick explanation. A decorator, also called higher-order component, is a component, which wraps another component to enhance it in some way. The default pattern for a decorator is:

const myDecorator = BaseComponent => {
  return class extends Component {

    render() {
      return <BaseComponent {...this.props} {...this.state} />;
    }
  }
}

It is a function, which takes a component class as its argument and returns a new component class. The new class renders the original component, but is able to modify the props being passed down. The decorator component can apply whatever logic it pleases (firing requests in componentDidMount, listening to flux stores etc.).

Now that we know that too, let’s start implementing!

Building the panning decorator #

Here is the full code:

'use strict';

import React, { Component, View, PanResponder } from 'react-native';

export default BaseComponent => {
  return class extends Component {

    constructor(props, context) {
      super(props, context);

      this.lastX = 0;
      this.lastY = 0;

      this.state = {
        absoluteChangeX: 0,
        absoluteChangeY: 0,
        changeX: 0,
        changeY: 0
      };
    }

    componentWillMount() {
      this._panResponder = PanResponder.create({

        onStartShouldSetPanResponder: ({ nativeEvent: { touches } }, { x0, y0 }) => {
          const shouldSet = touches.length === 1;

          if (shouldSet) {
            const { onPanBegin } = this.props;
            onPanBegin && onPanBegin({
              originX: x0,
              originY: y0
            });
          }

          return shouldSet;
        },

        onMoveShouldSetPanResponder: ({ nativeEvent: { touches } }) => {
          return touches.length === 1;
        },

        onPanResponderMove: (evt, { dx, dy }) => {
          const { onPan } = this.props;
          const newState = {
            absoluteChangeX: this.lastX + dx,
            absoluteChangeY: this.lastY + dy,
            changeX: dx,
            changeY: dy
          };

          this.setState(newState);

          onPan && onPan(newState);
        },

        onPanResponderTerminationRequest: () => true,
        onPanResponderTerminate: this.handlePanResponderRelease,
        onPanResponderRelease: this.handlePanResponderRelease
      });
    }

    handlePanResponderRelease = () => {
      const { onPanEnd } = this.props;
      this.lastX = this.state.absoluteChangeX;
      this.lastY = this.state.absoluteChangeY;
      onPanEnd && onPanEnd();
    }

    render() {
      const {
        onPanBegin,
        onPan,
        onPanEnd,
        ...props
      } = this.props;

      return (
        <View {...this._panResponder.panHandlers}>
          <BaseComponent {...props} {...this.state} />
        </View>
      );
    }
  };
}

And now step by step:

Setting up the defaults #

constructor(props, context) {
  super(props, context);

  this.lastX = 0;
  this.lastY = 0;

  this.state = {
    absoluteChangeX: 0,
    absoluteChangeY: 0,
    changeX: 0,
    changeY: 0
  };
}

lastX and lastY will hold the absolute changes to x and y since the first pan occurred. These two values are not in state, as they are only for internal use. We need them to compute absoluteChangeX and absoluteChangeY correctly.

changeX and changeY will hold the distance the touch travelled since the last update. absoluteChangeX and absoluteChangeY on the other hand will contain the absolute change from the origin since the first pan interaction with the view. We will use the absolute values to apply a transform style to translate the view.

Creating the PanResponder instance #

In componentDidMount we create a PanResponder instance and assign it to this._panResponder so we can reference it later.

Claiming the responder status #

onStartShouldSetPanResponder: ({ nativeEvent: { touches } }, { x0, y0 }) => {
  const shouldSet = touches.length === 1;

  if (shouldSet) {
    const { onPanBegin } = this.props;
    onPanBegin && onPanBegin({
      originX: x0,
      originY: y0
    });
  }

  return shouldSet;
},

onMoveShouldSetPanResponder: ({ nativeEvent: { touches } }) => {
  return touches.length === 1;
},

If we want to become the responder for the, we have to return true from onStartShouldSetPanResponder. We only want to respond when we have a single touch, so we check the length of the touches array.

Depending on shouldSet, we try to call the onBegin callback with the initial coordinates.

If we want to receive updates when the touch moves, we need to return true from onMoveShouldSetPanResponder. We use the same check as before.

Processing touch movement #

onPanResponderMove: (evt, { dx, dy }) => {
  const { onPan } = this.props;
  const newState = {
    absoluteChangeX: this.lastX + dx,
    absoluteChangeY: this.lastY + dy,
    changeX: dx,
    changeY: dy
  };

  this.setState(newState);

  onPan && onPan(newState);
},

Each time the touch moves, this method will be called. We grab the dx and dy values from the second argument (the handy gestureState), which represent the distances the touch has moved in x and y direction since the last event.

We now calculate the absolute change using the previous position. The dx and dy values are just added verbatim to our new state object.

After setting the state, we then try to call the onPan callback with the new state. This is done in order to expose an api into which we can hook from the outside. We could use it for example to trigger a condition based on the absolute change values.

Allowing termination and handling touch termination / release #

    onPanResponderTerminationRequest: () => true,
    onPanResponderTerminate: this.handlePanResponderRelease,
    onPanResponderRelease: this.handlePanResponderRelease
  });
}

handlePanResponderRelease = () => {
  const { onPanEnd } = this.props;
  this.lastX = this.state.absoluteChangeX;
  this.lastY = this.state.absoluteChangeY;
  onPanEnd && onPanEnd();
}

In onPanResponderTerminationRequest we just return true – we don’t want to hog the touch in case another view wants to be the responder. This might happens when another view further up the hierarchy wants to become a responder to the touch.

We delegate onPanResponderRelease and onPanResponderTerminate to handlePanResponderRelease. There we assign the current absolute changes for the x and y positions to lastX and lastY respectively, so we can reference them again in the next panning action. In addition to that we call the onPanEnd callback, if available.

Rendering the component #

render() {
  const {
    onPanBegin,
    onPan,
    onPanEnd,
    ...props
  } = this.props;

  return (
    <View {...this._panResponder.panHandlers}>
      <BaseComponent {...props} {...this.state} />
    </View>
  );
}

We wrap the passed in BaseComponent with a view, which receives all handlers from our PanResponder instance using the spread operator. The BaseComponent receives all props – minus the callbacks – and our internal state as props.

The decorated/wrapped component can now do whatever it wants with the information it received from the decorator. Most likely it will be used to pan an element using the transform property of a style-object, but it could be used for anything really. (Color tweening, custom sliders, etc. come to mind).

Using the panning decorator #

Now that we have a fully working decorator, this is how we would use it:

@makePannable
class MyComponent {

  render() {
    const {
      absoluteChangeX, absoluteChangeY
    } = this.props;
    const transform = [
      {translateX: absoluteChangeX},
      {translateY: absoluteChangeY}
    ];

    return (
      <View style={{
        flex:1,
        overflow: 'hidden',
        backgroundColor:'#f00',
        transform,
      }}>
        <Text style={styles.text}>Round, round pan me around!</Text>
      </View>
    );
  }

}

And the result would be similar to this, expect much smoother (note that the onPan callback is used to change the background color of the main view based on the sub-views position):

panning a uiview around

Conclusion #

Using the PanResponder to track simple panning actions is a powerful tool for implementing your own gesture recognizers/decorators.

In the next post I will cover how to create a decorator which supports pinch-zooming.

Please note: due to the fact that we are wrapping a component within a view, there can be issues with this approach. Especially if a component does not fill up the whole screen and is moved using transform, it can result in the ability to move the view even though we are touching next to it. This is because the wrapping view is not being moved.

A possible solution to the problem would be to enhance the decorator to allow styling of the wrapper node and move the transformation code into a parent component. If I figure out a good way to do this I will publish the solution in another post.

Update: The article with a solution to the ghost-panning can be found here

Thanks to @notbrent for proof-reading.

Listening to application urls in React Native

Note: there is an example application available on GitHub

One great feature of mobile platforms is deep linking into your application from custom URLs, using custom URL schemes. We wanted this for the React Native Playground iOS app, to share applications easily from any medium, like emails or tweets, using the rnplay:// scheme.

Fortunately, React Native bundles a library to handle incoming requests via URL schemes. Let’s get to it!

  • Define our URL scheme in XCode
  • Modify AppDelegate.m to pass incoming requests to React Native
  • Listen for and handle requests in JavaScript

Defining a custom URL scheme

Custom url schemes are defined in your Info.plist file. Not too long ago you would have had to manually add keys there. Luckily you don’t have to anymore! Instead you can add URL types comfortably through the XCode interface.

Select your project in the left sidebar. Then select the target you want to add URL types to. Switch to the “Info” panel and open the “URL Types” section. Copy your bundle identifier into the “Identifier” and your desired URL scheme into the “URL Schemes” field. You can ignore the other fields (we do not need an icon and “roles” are only used on OSX). You should now have something similar to this:

url types editor

Now your application is ready to respond to custom urls! (In the example above it would respond to any urls starting with myapplication://.

Modifying your application delegate file

This is only necessary to handle URLs passed to a launched application. Since apps often run in the background, we should do this. In your Appdelegate.m add this to the top:

Then add this to the end of the file:

This will make sure, that whenever somebody visits a link – with an URL scheme your application registered for – you will be properly notified about it and can take action. Even if your application is already launched, hooray!

Depending on your project file header path configuration it is possible, that XCode will complain that it cannot find RCTLinkingManager.h. But fear not! There is an easy solution to the problem. In XCode, select your project in the left sidebar. In the main panel navigate to “Build Settings”. Now scroll down until you find “Header Search Paths”, or alternatively use the search box at the top right. When you found them, double click on the value and using the “+” button at the bottom add a new path with the value $(SRCROOT)/node_modules/react-native/Libraries and set it to “recursive”. That should stop XCode from crying about not finding the headers. This is how it should look:

header search paths config

No more changes to your Appdelegate.m are required. Your file should look similar to this now:

Hooking up the JS side of things

Check out the final code, then let’s walk through it step by step.

I’m using ES6 class syntax here – you’re welcome to use React.createClass if you prefer.

We require the LinkingIOS module first.

We want to handle incoming requests as soon as our component is mounted, so we add a listener for the url event.

To keep things tidy, we remove the handler again when the component gets unmounted, even though it’s unlikely that will happen to our root component.

The real meat is in _processURL. We grab the url prop of the event, strip our custom url scheme, and finally separate the path from the query string.

Using the qs module (from npm), we parse the query string, and log the path and params to the console. Now we have everything we need to process our custom urls.

Testing our URL scheme

Let’s try launching this application, then backgrounding it by pressing the Home button.

Open Safari and visit myapplication://some-path?param=value. We’re sent back to our app. In the Xcode console, we should see some-path and {param: ‘value’}. Yay!

But what if our application hasn’t been launched yet? Let’s try it by exiting our app completely. To do this, double tap the Home button and swipe-up on our app.

Now repeat the Safari step.

Our app launches, but just sits there displaying the home screen – no log in our console.

This happens because no url event is triggered on application start. Let’s update our code to handle this case.

Take a look at the componentDidMount method on line 16. If the application has been launched via a URL, we can retrieve it using LinkingIOS.popInitialURL() and pass the URL to our handler. Since our event handler expends an event object with a url property on it, we wrap the url accordingly. In case you are wondering: {url} is the the ES6 object short notation and is equivalent to writing {url: url}.

Now you should be ready to use all the crazy url schemes you want. Try it!

Thanks to @jsierles for proof-reading.