We wrote Titanium in order to respond to the following user story:
As an EasyShift user, I want all images inside of a job / survey to be tappable, zoomable, etc., so that I can get a better look at them.
Strictly speaking, we could have dealt with this problem by using a good old UIScrollView and a standard modal transition. However, we felt like this deserved a little bit more love.
Apple provides an easy way to customise transitions between view controllers: the
UIViewControllerAnimatedTransitioning protocol. It lets you manually specify all the animations for presenting and dismissing view controllers.
Scale and translate
In this case, I wanted to recreate an animation like the one in the Photos app, where a thumbnail preview enlarges to fill the entire screen. In order to achieve this, the logic was to instantiate the full-screen image view, apply a scale + translation transform to make it the size and position of the thumbnail, then revert the transform to
CGAffineTransformIdentity while animating. Easy enough.
But we’re not done yet. Just like in the Photos app, the aspect ratio of the thumbnail is independent from that of the image itself. What’s more, in the Photos app the thumbnails are always square, whereas our thumbnails can have any arbitrary aspect ratio. In a plain
UIImageView (or any other
UIView subclass, for that matter), this is easily achieved like so:
However, we need an animated transition between the cropped image of the thumbnail and the full view. The solution here is to use the
mask property of
CALayer like so:
1 2 3
We also wanted to enable users of Titanium to use thumbnails with rounded corners. That meant we had to use the
cornerRadius property on our full screen view. However, we couldn’t just read the value from the thumbnail view, apply it to our full screen view and call it a day. Because our view was going to get scaled, we had to multiply the value by the inverse of the scale factor before applying it.
The major drawback of using CALayer properties is that, unlike
CGAffineTransforms, they cannot be animated using
UIView animation blocks. Instead, I had to create a
CABasicAnimation for each property I wanted to animated (
cornerRadius). Here’s a quick example:
1 2 3 4 5
Full screen image view
The easiest, most straightforward way of providing scrolling and zooming capabilities to a view with arbitrarily-sized content is to use a
UIScrollView. In practice however, it proved too challenging to integrate into the custom animations, and an alternative solution was found.
The full screen view is composed of a black background view and an image view. Interaction is achieved using
UIGestureRecognizers that detect four different types of gestures:
* double tap
The general structure of the code was derived from the Touches GestureRecognizers sample project (available here).
Pan & pinch
These two gesture recognizers work in tandem to provide direct manipulation of the image view. The
UIPanGestureRecognizer affects the
center property of the image view, while the
UIPinchGestureRecognizer applies a
CGAffineTransformScale to it.
Tap & double tap
In addition to the pinching and panning, two instances of
UITapGestureRecognizer handle single- and double-taps. A single tap will revert to the original zoom level and dismiss the view, and a double tap will zoom to the maximum zoom level.
In order for these to work alongside each other, the
gestureRecognizer:shouldRequireFailureOfGestureRecognizer: delegate method is implemented to return
YES if the two gesture recognizers in question are the single-tap and the double-tap, respectively. One drawback of this solution is that it introduces a slight delay in the detection of a single-tap, while the system gives the user a chance to perform a double-tap.