You can touch it yourself.
Hammer uses things called Recognisers to listen for (feel?) touch gestures. We'll be using three of them: pinch, pan and tap. We'll need to make and configure an instance of Hammer.Manager, and the same for each of the three Recognisers; then we'll plug them together, and finally we'll add some handler functions to do the actual work.
Creating the Zoomer
The Zoomer component
Let's start by creating a new Reagent component, which contains an image:
We've defined three local atoms, which will store our
Hammer.Manager as well as the current state of the user's interaction (that we've called a
!zoom). We use that state to apply a transform style to the image, using a function called
translate3d instead of playing around with the CSS
left properties, we give the browser the chance to hand off the transformations we'll be applying to a GPU, giving us a significant performance boost. There are three properties we're interested in: the x and y pan positions, and a scaling multiplier.
Now we've got a component that displays an image. The
Hammer.Manager needs to bind to a DOM node, so we have to create it after React has mounted our image component. We can add a
:component-did-mount lifecycle handler, which gets a reference to itself passed in by Reagent.
Note: you'll notice we've used the
js-invoke function to call the manager's functions - the stubs in the
[cljsjs/hammer "2.0.4-5"] jar don't work, so we call the manager directly.
:component-did-mount lifecycle fn:
We've created a new
mc and added to it a new
Hammer.Tap Recogniser called
tap's constructor we've defined a new event called
doubletap, which we've defined to be two of the tap events that the
Hammer.Tap Recogniser knows to look out for Finally we've stored the Manager in an atom. Why? So we can clean it up when the component unmounts with a
:component-will-unmount lifecycle fn:
Great! We've set up Hammer to watch for gestures. We'd like the user to be able to zoom with doubletaps, pan the image by pressing and dragging, and also pinch-to-zoom.
In order for Hammer to detect gestures, we have to give it Recognisers. Here we've created one and at the same time defined a new event called
doubletap, which we define as being two of the built-in tap events. When we see one, we've asked Hammer to call our handler function, which sets the current scaling multiplier to 2 - i.e. we'll double the size of the image. If the scale has already changed, we reset it to 1 instead.
We need to add a
Hammer.Pan Recogniser and attach it to the Manager in the same way:
Here we're allowing the user to pan the image in any direction (by default up and down are disabled, so as to avoid interfering with scroll), and we aren't requiring a threshold of movement to be reached before we respond to the gesture - we want panning to be precise and immediate.
There's a hitch here though: the
translate3d CSS transform takes an absolute value, whereas Hammer's gesture events are giving us deltas between the state at the start of the gesture and now. That's where our
!start-zoom atom comes in - it's a place we can record the current state each time the user starts a new gesture. Hammer allows us to detect the start and end of each gesture as well as the gesture itself, in the case of pan these are called
pan (all the events provided by each Recogniser are listed in the docs). When the user starts panning, we record the current zoom in the
!start-zoom atom; then as they pan around, we can add the deltas from the incoming events to the values stored in there.
Enabling pinch-to-zoom is very similar. It's natural to want to both pinch and pan at the same time, and we could allow this using Hammer's
recognizeWith support, which allows the detection of multiple gestures simultaneously; luckily for us though, Hammer realises that this is a common requirement and the
Hammer.Pinch Recogniser already has a
pinchmove event that does exactly what we want. As with pan, we want to record the starting pan and scale so we can apply the delta from each event of the current gesture:
(.preventDefault %) lines stop the
doubletap gesture from falling through to components underneath our zoomer, and eventually to the browser - this is important for preventing browser zoom on mobile safari, which doesn't respect
zoomer.core namespace looks like this: