4/11/11

Implement pan/zoom feature with multi-touches screen (Part 2)


In last post, we already know how to make a proper zoom internally with OpenGL. This post will help you build a natural zooming on multi touches screen.
We will first, define how zooming gesture works with some UI tests, based on that we generalize a Mathematical problem, solve it, then go to some implementation note

If you are not interested in pan/zoom, you may find some useful info about Multitouches UI Tests, and tips for handling UITouch objects.

UI Tests
Put your fingers in Maps application, then feel the map's movement when you move your fingers. It's really intuitive and natural. You can enlarge then make it small, then large again as many times as you can, you can move the map up, move it down easily with your finger.

So, how to brief this cool feeling in detail?
A natural pan/zoom function like Maps app should pass these UI tests:

Let's say we put 2 fingers on screen at A and B, we move our finger to new points on screen A' and B'. Maps application zoom/pan have these properties:
1. Zoomed with appropriate scale with respect to how opened your finger is
The image is enlarged with a scale A'B'/AB
2. In Maps, if you put your first finger on location A, another finger on location B, after preforming pan/zoom, both locations are still right under your finger, no matter how many time you zoom out, then in.
2 point O and X are still under 2 fingers after zooming
3. Hold a finger on screen, move another, the map's zoomed, 2 marks is still under your finger.
4. If 2 fingers touch the screen at different timestamp, you still can zoom.
5. Pan and zoom at the same time
6. Twist 2 finger around to form a circle, the map is not panned or zoomed
2 point O and X are still in the same place

A simple conclusion: a natural zooming will keep 2 marks stay under your fingers in most cases


Now, pan/zoom seems more clear, but how to put these tests into code. We generalize the previous conclusion into Maths:
Equivalent Mathematical problem for pan/zoom feature.

On a plan, given 2 pair of points A, B and A', B'. How to use transformations (translation, resizing, rotation) to move A to A' and B to B'?
Try to solve this yourself, or you can continue with a simple, step by step solution below:

- First. move A to A' by using a Translation with vector AA', B is also moved to B1
- Second, move B1 to B2 by using a resize with origin in A', scale A'B2/A'B1
- Finally, use a Rotation with origin A', angle (A'B2, A'B') to make vector A'B2 to same direction with A'B', B2 is moved to B'

Next step, we go to implement all of these. To do this, we need to understand how touch data is transferred from your fingers and iPhone screen into your code.


Tips for handling UITouch
1. What is UITouch object?
This except from Apple iOS Event Guideline is extremely useful:
In iPhone OS, a UITouch object represents a touch, and a UIEvent object represents an event. An event object contains all touch objects for the current multi-touch sequence and can provide touch objects specific to a view or window (see Figure 3-2). A touch object is persistent for a given finger during a sequence, and UIKit mutates it as it tracks the finger throughout it. The touch attributes that change are the phase of the touch, its location in a view, its previous location, and its timestamp. Event-handling code evaluates these attributes to determine how to respond to the event. (more discussion)
2. How UITouch objects sent through touchesBegan, touchesMoved, touchesEnded, touchesCanceled?

Sequence of passing objects during a series of events
UITouchtouchBegantouchMovedtouchEndedtouchCanceled
Put 1 finger on screenA
Move 1A
Put another fingerB
Move 2 fingersA,B
Move 2nd fingerB
Put 1st finger out of screenA

1. Number of touches depends on event, so it is not represent how many touches are on screen
2. An UITouch object represent for a finger on screen, its data is mutable and will changed went the finger moved. You can not retain UITouch objects, thus cannot use an NSArray or NSDictionary to store touches data.
3. Number of touches go throughout touchesBegan is equal to number of touches go through touchesEnded + touchesCanceled

Pan/zoom implementation tips
- To handle how many touches are on screen, we need to remember the UITouch objects' pointers, we can use a C array of UITouch to store, and use a pointer comparison if we want to check for a pointer

Now you have all information needed for an implementation. In each UITouch delegates of your UIView, do these task:
Touches Began
  • Remember UITouch objects by saving it into array
Touches Moved
  • Check touches array
  • If there has 1 finger on screen, preform a pan
  • If there have more than or equal to 2 fingers on screen, use our transformations to make a zoom
A = [touch1 previousLocationInView:self];
A' = [touch1 locationInView:self];
B = [touch2 previousLocationInView:self];
B' = [touch2 locationInView:self];

+ Translation with vector AA': AA' = (x_A' - x_A, y_A' - y_A)
Calculate B1

+ Resize at origin A', scale k = A'B'/AB, use the function we build in previous post
- (void)zoomAtPoint:(CGPoint)point scale:(CGFloat)scale;
Calculate B2

+ Rotation:
At this point we want to make rotation to bring B2 to B'. In real, we can not rotate the maps, so we will do a simple trick to bring B2 nearer to B': we perform a pan with vector B2B'/2

Touches Ended/Touches Canceled
Remove touches out of touches array

That's it! Feel free for clarification and discussion :)