Blog Archives

Elastic dragging with a CALayer

You’ll notice when you pull beyond the bounds of a UITableView that the table view appears spring loaded before jumping back when you lift your finger off the device. I posted a project to GitHub that illustrates something similar using a CALayer and the UITouch event handlers on UIView.

There’s a few strange things I did using a category on UIWindow to handle the touch events, but you could implement the same functionality on any UIView. The important things are the touch callbacks which are posted below.

static BOOL tapOnImageLayer;
static CGPoint initialPosition;
static float velocity = 900; // px / sec

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    if (tapOnImageLayer)
    {
        CALayer *imageLayer = [(elasticAppDelegate_iPhone *)[UIApplication sharedApplication].delegate imageLayer];
        
        CGFloat distance = CGPointDistance(initialPosition, imageLayer.position);
        
        NSTimeInterval time = distance / velocity;
        NSLog(@"%f %f", distance, time);
        [CATransaction begin];
        [CATransaction setDisableActions:NO];
        [CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
        [CATransaction setAnimationDuration:time];
        
        imageLayer.position = initialPosition;
        
        [CATransaction commit];
        
        tapOnImageLayer = NO;
    }
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *t = [touches anyObject];
    CALayer *layer = [self.layer hitTest:[t locationInView:self]];
    if (layer == [(elasticAppDelegate_iPhone *)[UIApplication sharedApplication].delegate imageLayer])
    {
        tapOnImageLayer = YES;
        initialPosition = layer.position;

    }
    else
    {
        tapOnImageLayer = NO;
    }
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    if (tapOnImageLayer)
    {
        CALayer *imageLayer = [(elasticAppDelegate_iPhone *)[UIApplication sharedApplication].delegate imageLayer];
        
        CGSize delta = CGSizeMake([touch locationInView:self.window].x - initialPosition.x, 
                                  [touch locationInView:self.window].y - initialPosition.y);
        
        [CATransaction begin];
        [CATransaction setDisableActions:YES];
        
        imageLayer.position = CGPointOffset(initialPosition, CGSizeMultiplyScalar(delta, 0.7));
        
        [CATransaction commit];
    }
}

The important parts are setting the desired spring back velocity in line 3, and scaling the drag position in line 56. There are a couple of convenience functions for working with the CGFoundation types:

CGPoint CGPointOffset (CGPoint point1, CGSize delta)
{
    CGPoint p;
    p.x = point1.x + delta.width;
    p.y = point1.y + delta.height;
    
    return p;
}

CGFloat CGPointDistance (CGPoint p1, CGPoint p2)
{
    CGFloat dx = p1.x - p2.x;
    CGFloat dy = p1.y - p2.y;
    
    return sqrtf(dx * dx + dy * dy);
}

CGSize CGSizeMultiplyScalar (CGSize p, CGFloat scalar)
{
    return CGSizeMake(p.width * scalar, p.height * scalar);
}

I’ll post this and the other code I’ve done on Github shortly (I’m pretty new to it and still trying to figure it out!)