iKy1e | iOS Developer

Tutorial: Cool UIScrollView Effects

While I was working on the “Cardflow” interface mode for CardSwitcher I had to work out get custom page widths and custom effects as you scroll through the cards, this is actually quite simple but I haven’t seen many posts on customising a UIScrollView. Most of the tips are spread across lots of different stackoverflow questions.

  • Problems with custom scrollview’s

To start with (actually while I was working on the first version of CardSwitcher) I looked into iCarousel, which has quite a lot of cool effects and a tableView style data source and delegate protocols. However as the first version of CardSwitcher was actually just a standard WebOS, or Safari tab’s style, linear layout that highlighted a problem with iCarousel (which joehewitt also noticed with iScroll), caused by the fact it doesn’t customize a UIScrollView but instead uses UIGestureRecognizers to do the scrolling itself (like UIScrollView itself).

However because of this the scrolling just feel wrong, the physics are off by a little bit. The bounce as you scroll of the edge, flicking and then how many pages it scrolls past before stopping and the paging effect all just feels off slightly.

When using iCarousel for the purpose it was designed for, the coverflow or other custom modes, and you might not notice. However as CardSwitcher would have used the linear mode only, at least for the initial release, that would annoy me. For instance Multifl0w's 'cards' style has a custom scrolling too, it feels very smooth but isn’t quite right still.

So after explaining why I didn’t like using a custom methods I’ll now point out why people feel a need to develop these custom methods rather then using a UIScrollView.





The most obvious problem is “paging”. To activate paging on a UIScrollView you just set it’s pagingEnabled property to YES. However it defaults to each page being the same width as the scrollview itself, meaning if you want to achieve a preview effect like in Safari (screenshot below) you can’t do it by simply specifing a custom page width.

Also the custom scrollview’s all have built in ways to create custom effects whereas UIScrollView provides no built in way to achieve cool effects, so you’ll have to work it out yourself.

Despite the need to subclass and customize the UIScrollView yourself it’s is a very refined and very well built UIKit class and over all I think it’s best to just deal with the one or 2 limitations then throw it all away and start again from scratch or use a custom scrollview library. The difficulty of matching the refinement of a UIScrollView is highlighted well by the numerous JavaScript libraries which try to mimic it for web apps and the native attempts at making custom scrollviews (none of which get the physics quite right).









Customizing UIScrollView & Adding Effects

So now to the fun bit (the code). Customizing the page width of a UIScrollView is actually quite simple, set the width to whatever you want and then set the ‘clipsToBounds’ property to NO, very simple. Except it’s not quite that simple as there’s a little problem with that (how big a problem depends on how small you want to the page widths to be). The problem is that touches will only be received inside the view’s bounds. So if your use for this is a scrolling menu (like a UITableView) that’s page width is the width of one cell only the center one will receive touches. To fix that you need to subclass UIScrollView and override the -(BOOL)pointInside: withEvent: method.

@interface CustomScrollView : UIScrollView
@property (nonatomic, assign) UIEdgeInsets responseInsets;
@end

@implementation CustomScrollView
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    CGPoint parentLocation = [self convertPoint:point toView:self.superview];
    CGRect responseRect = self.frame;
    responseRect.origin.x -= self.responseInsets.left;
    responseRect.origin.y -= self.responseInsets.top;
    responseRect.size.width += (self.responseInsets.left + self.responseInsets.right);
    responseRect.size.height += (self.responseInsets.top + self.responseInsets.bottom);
    return CGRectContainsPoint(responseRect, parentLocation);
}
@end

Though for CardSwitcher I use a less flexible implementation because it does what I want.

@implementation CustomScrollView
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    CGPoint parentLocation = [self convertPoint:point toView:self.superview];
    return [self.superview pointInside:parentLocation withEvent:event];
}
@end

This now means that no matter how small the scrollview (in Cardflow mode it’s only 30px wide) you’ll still be able to scroll by dragging anywhere on it’s parent view.





Cool Effects

Now we’re going to go through and create a few cool scrolling effects.

Now I actually have each card keep track of it’s offset and set it’s own transform but you can easily do this in a for loop in the scrollViewDidScroll delegate method if you want. Note though that you’ll need a way to track the index of each card (hopefully besides just using it’s position in the UIView’s subviews).

// Method that get's called every time the scrollView scrolls.
-(void)setTransform{
/// setting up zoom effect!!!!!!
    UIScrollView *scrollView = //get the scrollView (most likely either self.scrollView or self.superView);
    // Set the layers transform back to normal so it's frame wouldn't be effected while we do the calculations
    self.layer.transform = CATransform3DIdentity;

    // Easier reference to these
    CGFloat scrollViewWidth = scrollView.bounds.size.width;
    CGFloat offset = scrollView.contentOffset.x;
    // Do some initial calculations to see how far off it is from being the center card
    CGFloat pageIndex = ([/* an array (hopefully not self.superviews.subviews)*/ indexOfObject:self]);
    CGFloat currentPage = (offset/scrollViewWidth);
    CGFloat pageDifference = (pageIndex-currentPage);
    // And the default values
    CGFloat scale = 1.0f;
    CGFloat alpha = 1.0f;


    /*** NOW FOR THE EFFECTS ***/
    // Scale it based on how far it is from being centered
    scale += (pageDifference*0.2);

    // If it's meant to have faded into the screen fade it out
    if (pageDifference > 0.0f) {
            alpha = 1 - pageDifference;
    }

    // Don't let it get below nothing (like reversed is -1)
    if (scale < 0.0f) {
        scale = 0.0f;
    }

    // If you can't see it disable userInteraction so as to stop it preventing touches on the one bellow.
    if (alpha <= 0.0f) {
        alpha = 0.0f;
        self.userInteractionEnabled = NO;
    }
    else{
        self.userInteractionEnabled = YES;
    }

    /*** Set our effects ***/
    self.alpha = alpha;
    // We could do just self.transform = but it comes by default with an animation.
    [CATransaction begin];
    [CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
    self.layer.transform = CATransform3DMakeScale(scale, scale, 1.0f);
    [CATransaction commit];
}

This will create the same effect as the ‘Cardflow’ mode in CardSwitcher. However the important bit’s are those initial variables and then the end bit where you set it’s transform. In between you can fiddle with those values to create the cool effects.

If you want some examples of cool scrollView effects have a look at the iCarousel Demo project, which includes these effects: Linear, Rotary, Inverted Rotary, Cylinder, Inverted Cylinder, CoverFlow, CoverFlow2 and a Custom one similar to this one.

If you have any questions or comments please post them below.


  1. catie-graham reblogged this from iky1e
  2. dulcinea-mathews reblogged this from iky1e
  3. martinbowling reblogged this from iky1e
  4. iky1e posted this
To Tumblr, Love PixelUnion