Earlier this week Ole Begemann wrote a really great tutorial on how UIScrollViews work, and to explain it effectively, he even created a really simple scroll view, written from scratch.

The setup was quite simple: Use a UIPanGestureRecognizer, and change the boundsorigin in response to translation of the pan gesture.

Extending Ole’s custom scroll view to incorporate UIScrollView’s inertial scrolling seemed like a natural extension, and with Facebook’s recent release of Pop, I thought it writing a decelerating custom scroll view would be an interesting weekend project.

In case you’re unaware, Pop is Facebook’s recently open-sourced animation framework that powers the animations in Paper for iOS. The Pop API looks mostly like CoreAnimation, so it isn’t that difficult to learn.

Two of the best things about Pop:

  • It gives you spring and decay animations out of the box
  • You can animate any property on any NSObject using Pop, not just the ones that are declared animatable in UIKit

Given that Pop has built-in support for decaying animations, implementing UIScrollView’s deceleration isn’t very tough.

The main idea is to animate the boundsorigin in a decaying fashion using the velocity off the UIPanGestureRecognizer in its ended state.

So let’s begin. Start off by forking my fork of Ole’s CustomScrollView project, jump to the handlePanGesture: method in CustomScrollView.m. (Why start with my fork and not Ole’s project? He sets the translation to zero each time the gesture fires, which in turn resets the velocity. We, however, want the velocity to be preserved.)

We want to start a decay animation only after the gesture has finished, so we’re interested in the UIGestureRecognizerStateEnded case of the switch statement.

Here’s the basic flow of the code fragment embedded below:

  • We get the velocity of the gesture with the velocityInView: method.
CGPoint velocity = [panGestureRecognizer velocityInView:self];
  • We do not want any movement in x or y direction if there’s not enough horizontal or vertical content respectively. So we add checks for that.
if (self.bounds.size.width >= self.contentSize.width) {
    velocity.x = 0;
}
if (self.bounds.size.height >= self.contentSize.height) {
    velocity.y = 0;
}
  • Also it turns out that the velocity we get from the velocityInView: method is the negative of what we actually want, so fix that.

  • We want to animate the bounds (specifically the bounds’ origin) so create a new POPDecayAnimation with the kPOPViewBounds property.

POPDecayAnimation *decayAnimation = [POPDecayAnimation animationWithPropertyNamed:kPOPViewBounds];
  • Pop expects the velocity to be of the same coordinate space as the property you’re trying to animate. The value will be an NSNumber for alpha (CGFloat), CGPoint wrapped in NSValue for center, CGRect wrapped in NSValue for bounds and so on. As you might have guessed, the change in the property value during the animation is a factor of the corresponding velocity component.
  • In our case, animating bounds means our velocity will be a CGRect, and its origin.x and origin.y values correspond to the change in the boundsorigin. Similarly, the size.width and size.height velocity components correspond to the change in the boundssize.
  • We don’t want to change the size of the bounds, so lets keep that velocity component zero always. Feed in the origin.x and origin.y values of the velocity with the pan gesture’s velocity. Wrap the whole thing into an NSValue and assign it to decayAnimation.velocity
decayAnimation.velocity = [NSValue valueWithCGRect:CGRectMake(velocity.x, velocity.y, 0, 0)];
  • Finally, add the decayAnimation to the view using the pop_addAnimation: method with any arbitrary key you want.
[self pop_addAnimation:decayAnimation forKey:@"decelerate"];

Here’s the combined code:

With this you should have the basic deceleration working. You’ll notice that if your velocity is high, the view will scroll beyond its contentSize, but this could easily be solved by overriding setBounds: and adding checks to see that the bounds origin do not go beyond the thresholds. I’m surprised that Pop doesn’t add these checks itself.




A really interesting aspect of Pop is the ability to animate any property at all, not just ones declared as animatable by UIKit. Since we’re just animating the bounds’ origin, I thought this was a good chance to try out Pop`s custom property animation.

Pop lets you do this is by giving you constant callbacks with the progress of the animation, so that you can update your view accordingly. It takes care of the hard part of handling timing, converting the timing into the value of your property, decaying or oscillating the animation etc.

So here’s how you’ll do the same thing we did earlier, but now with your own custom property. Specifically, we’ll animate bounds.origin.x and bounds.origin.y. You’ll see that the structure of the code largely remains the same, except for the initialisation of this giant POPAnimatableProperty.

  • The property name, as I understand, can be any arbitrary string. In this case, I’ve taken it to be @"com.rounak.bounds.origin"
  • There’s also an initializer block with POPMutableAnimatableProperty as the argument. (I think this initialisation pattern is called the builder pattern.)
  • POPMutableAnimatableProperty has two important properties, readBlock and writeBlock. In readBlock you’ll have to feed data to Pop, and writeBlock you’ll have to retrieve that data, and update your view. The readBlock is called once whereas the writeBlock is called on each frame update of the display (via CADisplayLink).
  • Pop internally converts everything into vectors, and hence it asks and gives you data in the form of a linear array of CGFloats.
  • In readBlock you simply assign bounds.origin.x and bounds.origin.y to values[0] and values[1] respectively.

prop.readBlock = ^(id obj, CGFloat values[]) {
    values[0] = [obj bounds].origin.x;
    values[1] = [obj bounds].origin.y;
};
  • And in writeBlock you read values[0] (bounds.origin.x) and values[1] (bounds.origin.y) and update your view’s bounds.

prop.writeBlock = ^(id obj, const CGFloat values[]) {
    CGRect tempBounds = [obj bounds];
    tempBounds.origin.x = values[0];
    tempBounds.origin.y = values[1];
    [obj setBounds:tempBounds];
};
  • The magic happens in the writeBlock which is called each time with values that follow a decay (or an oscillating) curve.
  • You’ll notice that since we’re operating in just two dimensions here, our velocity is CGPoint, and not CGRect.

Here’s the combined code:

The entire project with the deceleration can be found on GitHub.

Liked the post? Follow me on Twitter @r0unak and try Design Shots, our Dribbble app.