Pinch to Reveal Animation Like in Boeing Milestones

Boeing made a nice app for iPad

I was shown this app and asked if it’s hard to do this pinch to reveal animation. It’s actually quite easy to do with CoreAnimation.

Analyzing

Let’s take a look at how Boeing app works when you pinch on screen:

  • User pinches screen on the area he’s interested in
  • The screen is divided into 2 parts and each part boundary is following user finger
  • Specific content is visible under the moving parts

What I don’t like about this app is the lag when user starts pinching, my guess is that the app is taking screenshot and creating the separate graphics for both parts of the screen ( That was idea I have heard when first taking look at the app ). I believe that’s unnecessary and it’s quite slow. We can use simple layer masking instead…

Implementation

Let’s create 2 ImageViews with our front content and use pinch gesture recognizer for interaction:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)viewDidLoad
{
  [super viewDidLoad];

  [self.view addSubview:[[UIImageView alloc] initWithImage:[UIImage imageNamed:@"Background2.jpeg"]]];
  UIImage *frontImage = [UIImage imageNamed:@"Background.jpeg"];

  leftView = [[UIImageView alloc] initWithImage:frontImage];
  [self.view addSubview:leftView];

  rightView = [[UIImageView alloc] initWithImage:frontImage];
  [self.view addSubview:rightView];

  UIPinchGestureRecognizer *pinchGestureRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinchGesture:)];
  [self.view addGestureRecognizer:pinchGestureRecognizer];
}

Next we need to mask our left and right image views properly depending on division point on screen:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)divideBackgroundAtPoint:(CGPoint)point
{
  divisionX = point.x;

//1
  CALayer *leftMask = [CALayer layer];
  leftMask.backgroundColor = [UIColor blackColor].CGColor;
  leftMask.frame = CGRectMake(0, 0, divisionX, leftView.bounds.size.height);
  leftView.layer.mask = leftMask;

//2
  CALayer *rightMask = [CALayer layer];
  rightMask.backgroundColor = [UIColor blackColor].CGColor;
  rightMask.frame = CGRectMake(divisionX, 0, rightView.bounds.size.width - divisionX, rightView.bounds.size.height);
  rightView.layer.mask = rightMask;
}

Creating mask is as simple as adding new layer with black color and setting proper frame on it. Then just assigning it as layer.mask on selected view.

  1. Left part of the view will be masked from the start of image to the division point.
  2. Right part of the view will be masked from the division point to the end of the image.

We need to adjust this division each time user starts pinch gesture, so let’s take a look at handlePinchGesture function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
- (void)handlePinchGesture:(UIPinchGestureRecognizer *)pinchGestureRecognizer
{
//1
  if (pinchGestureRecognizer.state == UIGestureRecognizerStateBegan) {
    [self divideBackgroundAtPoint:[pinchGestureRecognizer locationInView:pinchGestureRecognizer.view]];
  }

//2
  if ([pinchGestureRecognizer numberOfTouches] == 2) {
    float leftPoint = [pinchGestureRecognizer locationOfTouch:0 inView:pinchGestureRecognizer.view].x;
    float rightPoint = [pinchGestureRecognizer locationOfTouch:1 inView:pinchGestureRecognizer.view].x;
    if (leftPoint > rightPoint) {
      float tmp = rightPoint;
      rightPoint = leftPoint;
      leftPoint = tmp;
    }

    [UIView animateWithDuration:0.3f delay:0 options:UIViewAnimationCurveEaseInOut animations:^() {
      leftView.frame = CGRectMake(MIN(leftPoint - divisionX, 0), leftView.frame.origin.y, leftView.bounds.size.width, leftView.bounds.size.height);
      rightView.frame = CGRectMake(MAX(rightPoint - divisionX, 0), rightView.frame.origin.y, rightView.bounds.size.width, rightView.bounds.size.height);
    }                completion:nil];
  }

//3
  if (pinchGestureRecognizer.state == UIGestureRecognizerStateEnded) {
    const CGFloat velocity = pinchGestureRecognizer.velocity;
    if (velocity < 0) {
      [self animateToClosePosition];
    } else {
      [self animateToOpenPosition];
    }
  }
}

  1. If user just started pinching, we adjust our division masks.
  2. If pinch gesture uses 2 fingers, we animate our views positions to match both finger positions, we limit the movement so that user is not allowed to move views away from screen boundary.
  3. If user finishes pinching gesture, we either animate to fully open position or we animate to closed one.

Result:

Conclusion

Grab copy from GitHub.

It was quite simple to reproduce this pinch to reveal effect. About 40 min from seeing the original one to implementing my own implementation. For fun try adding shadow layers underneath the images, this should allow you to get nice darkening effect when left and right part are close to each other.

I’m merowing_ on twitter if you feel like talking about it.