Under the Bridge

Snippet: Pinch And Stretch

Here’s some sample code provided by Erica Sadun via Apple on the iphone-sdk list that was worked up to do the job of updating gesture-driven transformations on the fly. If you’ve struggled with doing this kind of thing so as to make the consequences work out intuitively — and if you haven’t, it’s not quite as straightforward as you think! — there’s probably some good tips for you to be found picking through this. Enjoy!

#import <UIKit/UIKit.h>
#include “time.h”

// Courtesy of Apple Sample Code
@interface UITouch (TouchSorting)
- (NSComparisonResult)compareAddress:(id)obj;
@end

@implementation UITouch (TouchSorting)
- (NSComparisonResult)compareAddress:(id)obj {
    if ((void *)self < (void *)obj) return NSOrderedAscending;
    else if ((void *)self == (void *)obj) return NSOrderedSame;
    else return NSOrderedDescending;
}
@end

// Apple Sample Code again
@interface DragView : UIImageView {
    CGAffineTransform originalTransform;
    CFMutableDictionaryRef touchBeginPoints;
}
- (CGAffineTransform)incrementalTransformWithTouches:(NSSet *)touches;
- (void)updateOriginalTransformForTouches:(NSSet *)touches;
- (void)cacheBeginPointForTouches:(NSSet *)touches;
- (void)removeTouchesFromCache:(NSSet *)touches;
@end

@implementation DragView
- (CGAffineTransform)incrementalTransformWithTouches:(NSSet *)touches {
    NSArray *sortedTouches = [[touches allObjects] sortedArrayUsingSelector:@selector(compareAddress:)];
    NSInteger numTouches = [sortedTouches count];

    // If there are no touches, simply return identify transform.
    if (numTouches == 0) return CGAffineTransformIdentity;

    // Single touch
    if (numTouches == 1) {
        UITouch *touch = [sortedTouches objectAtIndex:0];
        CGPoint beginPoint = *(CGPoint*)CFDictionaryGetValue(touchBeginPoints, touch);
        CGPoint currentPoint = [touch locationInView:self.superview];
        return CGAffineTransformMakeTranslation(currentPoint.x – beginPoint.x, currentPoint.y – beginPoint.y);
    }

    // If two or more touches, go with the first two (sorted by address)
    UITouch *touch1 = [sortedTouches objectAtIndex:0];
    UITouch *touch2 = [sortedTouches objectAtIndex:1];

    CGPoint beginPoint1 = *(CGPoint*)CFDictionaryGetValue(touchBeginPoints, touch1);
    CGPoint currentPoint1 = [touch1 locationInView:self.superview];
    CGPoint beginPoint2 = *(CGPoint*)CFDictionaryGetValue(touchBeginPoints, touch2);
    CGPoint currentPoint2 = [touch2 locationInView:self.superview];
 
    double layerX = self.center.x;
    double layerY = self.center.y;

    double x1 = beginPoint1.x – layerX;
    double y1 = beginPoint1.y – layerY;
    double x2 = beginPoint2.x – layerX;
    double y2 = beginPoint2.y – layerY;
    double x3 = currentPoint1.x – layerX;
    double y3 = currentPoint1.y – layerY;
    double x4 = currentPoint2.x – layerX;
    double y4 = currentPoint2.y – layerY;

    // Solve the system:
    //   [a b t1, -b a t2, 0 0 1] * [x1, y1, 1] = [x3, y3, 1]
    //   [a b t1, -b a t2, 0 0 1] * [x2, y2, 1] = [x4, y4, 1]

    double D = (y1-y2)*(y1-y2) + (x1-x2)*(x1-x2);
    if (D < 0.1) {
        return CGAffineTransformMakeTranslation(x3-x1, y3-y1);
    }

    double a = (y1-y2)*(y3-y4) + (x1-x2)*(x3-x4);
    double b = (y1-y2)*(x3-x4) – (x1-x2)*(y3-y4);
    double tx = (y1*x2 – x1*y2)*(y4-y3) – (x1*x2 + y1*y2)*(x3+x4) + x3*(y2*y2 + x2*x2) + x4*(y1*y1 + x1*x1);
    double ty = (x1*x2 + y1*y2)*(-y4-y3) + (y1*x2 - x1*y2)*(x3-x4) + y3*(y2*y2 + x2*x2) + y4*(y1*y1 + x1*x1);

    return CGAffineTransformMake(a/D, -b/D, b/D, a/D, tx/D, ty/D);
}

- (void)updateOriginalTransformForTouches:(NSSet *)touches {
    if ([touches count] > 0) {
        CGAffineTransform incrementalTransform = [self incrementalTransformWithTouches:touches];
        self.transform = CGAffineTransformConcat(originalTransform, incrementalTransform);
        originalTransform = self.transform;
    }
}

- (void)cacheBeginPointForTouches:(NSSet *)touches {
   for (UITouch *touch in touches) {
      CGPoint *point = (CGPoint*)CFDictionaryGetValue(touchBeginPoints, touch);
      if (point == NULL) {
         point = (CGPoint *)malloc(sizeof(CGPoint));
         CFDictionarySetValue(touchBeginPoints, touch, point);
      }
   *point = [touch locationInView:self.superview];
   }
}

- (void)removeTouchesFromCache:(NSSet *)touches {
    for (UITouch *touch in touches) {
        CGPoint *point = (CGPoint *)CFDictionaryGetValue(touchBeginPoints, touch);
        if (point != NULL) {
            free((void *)CFDictionaryGetValue(touchBeginPoints, touch));
            CFDictionaryRemoveValue(touchBeginPoints, touch);
        }
    }
}

- (id)initWithFrame:(CGRect)frame {
    if ((self = [super initWithFrame:frame])) {
      originalTransform = CGAffineTransformIdentity;

      touchBeginPoints = CFDictionaryCreateMutable(NULL, 0, NULL, NULL);

      self.userInteractionEnabled = YES;
      self.multipleTouchEnabled = YES;
      self.exclusiveTouch = NO;
   }
   return self;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [[self superview] bringSubviewToFront:self]; // pop to front please!
    NSMutableSet *currentTouches = [[[event touchesForView:self] mutableCopy] autorelease];
    [currentTouches minusSet:touches];
    if ([currentTouches count] > 0) {
        [self updateOriginalTransformForTouches:currentTouches];
        [self cacheBeginPointForTouches:currentTouches];
    }
    [self cacheBeginPointForTouches:touches];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    CGAffineTransform incrementalTransform = [self incrementalTransformWithTouches:[event touchesForView:self]];
    self.transform = CGAffineTransformConcat(originalTransform, incrementalTransform);
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    [self updateOriginalTransformForTouches:[event touchesForView:self]];
    [self removeTouchesFromCache:touches];

    for (UITouch *touch in touches) {
        if (touch.tapCount >= 2) {
            [self.superview bringSubviewToFront:self];
        }
    }

    NSMutableSet *remainingTouches = [[[event touchesForView:self] mutableCopy] autorelease];
    [remainingTouches minusSet:touches];
    [self cacheBeginPointForTouches:remainingTouches];
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    [self touchesEnded:touches withEvent:event];
}

- (void)dealloc {
   if (touchBeginPoints != NULL) CFRelease(touchBeginPoints);
   [super dealloc];
}
@end

@interface HelloController : UIViewController
{
   UIView *contentView;
}
@end

@implementation HelloController
CGPoint randomPoint() {return CGPointMake(random() % 256, random() % 396);}
- (void)loadView
{
   // Create the main view
   CGRect apprect = [[UIScreen mainScreen] applicationFrame];
   contentView = [[UIView alloc] initWithFrame:apprect];
   contentView.backgroundColor = [UIColor blackColor];
   self.view = contentView;
   [contentView release];

   // Add the flowers to random points on the screen
   for (int i = 0; i < 3; i++)
   {
      CGRect dragRect = CGRectMake(0.0f, 0.0f, 100.0f, 100.0f);

      dragRect.origin = randomPoint();

      DragView *dragger = [[DragView alloc] initWithFrame:dragRect];

      NSString *whichFlower = [[NSArray 

         arrayWithObjects:@"blueFlower.png", @"pinkFlower.png", 

         @"orangeFlower.png", nil] objectAtIndex:i];

      [dragger setImage:[UIImage imageNamed:whichFlower]];

      [dragger setUserInteractionEnabled:YES];

      [contentView addSubview:dragger];

      [dragger release];

   }
}

-(BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
   return NO;
}

-(void) dealloc
{
   [contentView release];
   [super dealloc];
}
@end

@interface SampleAppDelegate : NSObject <UIApplicationDelegate> {
   HelloController *hello;
}
@end

@implementation SampleAppDelegate
- (void)applicationDidFinishLaunching:(UIApplication *)application {
   srandom(time(0));
   UIWindow *window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
   hello = [[HelloController alloc] init];
   [window addSubview:hello.view];
   [window makeKeyAndVisible];
}

- (void)dealloc{
   [hello release];
   [super dealloc];
}
@end

int main(int argc, char *argv[])
{
   NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
   int retVal = UIApplicationMain(argc, argv, nil, @”SampleAppDelegate”);
   [pool release];
   return retVal;
}

 

h/t: iphonesdk!

0