cocos2d logo

Pinching and Panning with Cocos2s and UIGestureRecognizer

I have had a hard time lately to get zooming and  scroll to work in a Cocos2d based  app that I am writing. Indeed, there are many tutorials that you can find on the Web, but none that could really give me that crucial hint at how to make everything work out well. So, here go my findings, hoping that it may be useful to other developers that find themselves stuck with this.

First of all let me clarify that there are two kinds of problems in this:

  1. getting Cocos2D to recognize and dispatch multitouch events to your layer class;
  2. defining the geometrical transformation that gets you a smooth zooming or scrolling (taking into account the boundaries of your layer hierarchy).
As to 1., I understand that in principle Cocos2D supports ccTouches*:withEvent& set of methods (where * can be: Began, Moved, Ended); the only problem is it seems it is not easy to get them effectively called. Or, at least, not as straightforward as using the targeted versions of the same methods (which handle single touches). I am not going to use those methods or enter into the topic of how enabling them; rather, I rely on the more advanced gesture recognizer that the iOS SDK offers since version 3.2. I think this is the way to go for doing multi gesture handling, since gesture recognizers make everything easier.
As to 2., I understand that handling geometric transformation can be done in different ways; and I am no specialist in geometric transformation. So, I am only aiming at giving an example of how it can be done in a generic (I hope) fashion.

The Context

First of all, some information about the cocos2d scene that I am trying to zooming and dragging around. It has got a main CCLayer that acts as a container for multiple CCLayers.
This design is necessary because I have a UI layer that I do want to be fixed; a large background layer (some 4000×1000 pixels), where some animations take place; and a interactive layer where the user can interact with a few sprites by moving them around.
Nothing really fancy here, but as the user moves around the sprites, the background also moves according to its own logic; this means that the layers composing my scene get displaced respect to one another.

The Objective

At some point, besides dragging the sprites, zooming and panning also get enabled, so the user can move around the overall scene, or scaling it in and out to see it fully or in part.
The objectives here are:

  • for dragging: move around the whole scene with an inertial effect when the touch ends (so the image keeps scrolling a bit in the same direction of movement); adding a spring effect to make the scene bounce when it is dragged beyond its physical boundaries;
  • for pinching: zooming in and out without displacing (if possible) the pinch center and without the scene to ever reveal the black background behind it.

The Code

First of all, in my container layer’s init method, I create and attach two gesture recognizers:

- (void)init {
_panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanFrom:)];

_pinchGestureRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinchGesture:)];

The handlePanFrom method is also added to the same class:

- (void)handlePanFrom:(UIPanGestureRecognizer*)recognizer {

if (recognizer.state == UIGestureRecognizerStateBegan) {
//-- nothing to do here
} else if (recognizer.state == UIGestureRecognizerStateChanged) {

//-- calculate the nominal displacement of the layer
CGPoint translation = [recognizer translationInView:recognizer.view];
translation = ccp(translation.x, -translation.y);
[recognizer setTranslation:CGPointZero inView:recognizer.view];

self.position = ccp(self.position.x + translation.x, self.position.y);

} else if (recognizer.state == UIGestureRecognizerStateEnded) {

CGPoint velocity = [recognizer velocityInView:recognizer.view];
//-- first, calculate the rect of the layer we want to center
//-- then, calculate the required displacement so that it fills up the screen
CGRect rect = [self boundedRectForLayer:[self fonsLayer]];
CGPoint delta = [self displacementForRect:rect withVelocity:velocity andInertia:0.2];

CCMoveBy* moveBy = [CCMoveBy actionWithDuration:0.2 position:delta];
[self stopAllActions];
[self runAction:[CCEaseElastic actionWithAction:moveBy period:2]];

and finally the code for the pinch gesture handler:

- (void)handlePinchGesture:(UIPinchGestureRecognizer*)gestureRecognizer {

const CGFloat kMaxScale = 1.0;
const CGFloat kMinScale = [self fonsLayer].scaleToFit;
const CGFloat kSpeed = 0.1;

_pinchGestureRecognizer.cancelsTouchesInView = YES;

if(gestureRecognizer.state == UIGestureRecognizerStateBegan) {

//-- let's calculate the anchorPoint based on the pinch center so that zooming in/out is center
CGPoint location = [gestureRecognizer locationInView:[[CCDirector sharedDirector] openGLView]];
CGPoint glLocation = [[CCDirector sharedDirector] convertToGL:location];
CGPoint locationInSelf = [self convertToNodeSpace:glLocation];

if (gestureRecognizer.velocity < 0 &amp;amp;amp;&amp;amp;amp; self.scale > kMinScale)
self.anchorPoint = ccp(locationInSelf.x/self.contentSize.width, locationInSelf.y/self.contentSize.height);

if (gestureRecognizer.state == UIGestureRecognizerStateBegan ||
gestureRecognizer.state == UIGestureRecognizerStateChanged) {

//-- if we have reached the boundaries of the zooming, do nothing
if ((gestureRecognizer.velocity <= 0 &amp;amp;amp;&amp;amp;amp; self.scale <= kMinScale) || (gestureRecognizer.velocity >= 0 &amp;amp;amp;&amp;amp;amp; self.scale >= kMaxScale))

//-- calculate the new scale within its limits
CGFloat newScale = self.scale * (1 + gestureRecognizer.velocity * kSpeed);
newScale = MIN(kMaxScale, MAX(newScale, kMinScale));
self.scale = newScale;

//-- first, calculate the rect of the layer we want to center
//-- then, calculate the required displacement so that it fills up the screen
CGRect rect = [self boundedRectForLayer:[self fonsLayer]];
CGPoint delta = [self displacementForRect:rect];

self.position = ccpAdd(self.position, delta);

} else if (gestureRecognizer.state == UIGestureRecognizerStateEnded) {
//-- nothing to do here; otherwise, calc rect/delta like above, then apply action like when panning

The above code use methods that are critical for the correct behavior of panning or pinching. Here they are:

/////// given a layer, it calculates its nonimal rect, then maps it to the world space;
/////// the calculated rect represents the position of the layer on screen
- (CGRect)boundedRectForLayer:(CCNode*)layer {
CGRect rect;
ret = CGRectMake(0, 0, layer.contentSizeInPixels.width, layer.contentSizeInPixels.height);
return CGRectApplyAffineTransform(rect, [layer nodeToWorldTransform]);

/////// calculates the displacement required to make rect completely cover the screen area,
/////// so that no portion of the background is revealed; if velocity and inertia are given,
/////// the displacement is added a component that allows to ease in or out the movement.
- (CGPoint)displacementForRect:(CGRect)rect withVelocity:(CGPoint)velocity andInertia:(float)inertia {

CGSize winSize = [[CCDirector sharedDirector] winSize];
float hShootEnd = rect.origin.x + rect.size.width - winSize.width;
float hShootStart = rect.origin.x;
CGPoint extraScroll = ccpMult(velocity, inertia);

if (velocity.x > 0)
extraScroll.x = MIN(extraScroll.x, -hShootStart);
extraScroll.x = MAX(extraScroll.x, -hShootEnd);

return ccp(-MIN(hShootEnd, MAX(hShootStart, -extraScroll.x)),
-MIN(rect.origin.y + rect.size.height - winSize.height, MAX(rect.origin.y, 0)));

/////// helper method
- (CGPoint)displacementForRect:(CGRect)rect {
return [self displacementForRect:rect withVelocity:ccp(0,0) andInertia:0];

Final Note

I hope that the code it is auto explicative, given its factorization and the few comments it has.

Copyright © Labs Ramblings

Built on Notes Blog by TDH
Powered by WordPress

That's it - back to the top of page!