Parallax album covers with UICollectionView

Parallax is all the rage and with each new version of iOS we seem to get a whole lot more of it. In the current Music app, in iTunes Radio, there is a particular parallax effect that I really like. The album cover flow. This seemed like a fun challenge, so today I’m going to show you can create this effect using UICollectionView. Here is the result.

Before I dive into any code I want to explain my approach a little bit first. We’re going to create a UICollectionViewCell and place a UIImageView centered inside. This image will be fixed and have an equal amount of padding on each side. Then we’ll create several more image views, each one slightly inset from the previous and place them behind the fixed image view. These images will be fluid and move left and right based on where the cell is currently scrolled, occupying the padded space.

Here you can see an outline of the cell I’ve described. The blue outline is our fixed image and the green outline is the bounds of our cell. Our fluid images are outlined in gray.

Next let’s determine how these fluid images will move. To do this we need to know where each cell is relative to collection view’s bounds. Let’s create a common scale to represent this information. Let’s say:

-1 will mean the cell is scrolled leftmost in the view.
0 will mean the cell is perfectly centered in the view.
1 will mean the cell is scrolled rightmost in the view.

This is called normalization and we can do it with a simple linear equation. Here it is in code:

- (CGFloat)parallaxPositionForCell:(UICollectionViewCell *)cell 
{ 
    CGRect frame = [cell frame];
    CGPoint point = [[cell superview] convertPoint:frame.origin toView:collectionView];
 
    CGFloat minX = CGRectGetMinX([collectionView bounds]) - frame.size.width;
    CGFloat maxX = CGRectGetMaxX([collectionView bounds]);
 
    CGFloat minPos = -1.0f;
    CGFloat maxPos = 1.0f;
 
    return (maxPos - minPos) / (maxX - minX) * (point.x - minX) + minPos;
}

Now we need a way to deliver this information to each cell as the collection view scrolls.

- (void)scrollViewDidScroll:(UIScrollView *)scrollView 
{
    for (id cell in [collectionView visibleCells]) {
        CGFloat position = [self parallaxPositionForCell:cell];
        // We will implement this next.
        [cell setParallaxPosition:position];
    }
}

Next we need to implement setParallaxPosition: in our custom cell class. This method will be responsible for moving our fluid images around based on the position value. Since we’re working with a common scale here we should define what our values mean for the cell.

-1 will mean the images are completely offset to the right.

0 will mean the images are perfectly centered.

1 will mean the images are completely offset to the left.

Now, in our custom cell class we’ll add this implementation:

- (void)setParallaxPosition:(CGFloat)position 
{ 
    CGRect bounds = [self bounds];
 
    // Compute the padding on either side of the image
    CGFloat padding = 0.5f * (bounds.size.width - bounds.size.height);
 
    CGFloat minOffsetX  = -padding;
    CGFloat maxOffsetX  = padding;
 
    CGFloat minPosition = 1.0;
    CGFloat maxPosition = -1.0;
 
    // Compute the total offset using a linear equation
    CGFloat offsetX = (maxOffsetX - minOffsetX) / (maxPosition - minPosition) * (position - minPosition) + minOffsetX;
 
    // Divide the offset by the number of moving images
    offsetX /= ([imageViews count] - 1);
 
    // Apply the offsetX to each image relative to the first one
    CGRect fixedRect = [[imageViews objectAtIndex:0] frame];
    for (NSInteger i = 1; i < [imageViews count]; i++) {
 
        UIImageView *imageView = [imageViews objectAtIndex:i];
        CGRect imageRect = [imageView frame];
        CGFloat imageWidth = imageRect.size.width;
        imageRect.origin.x = CGRectGetMidX(fixedRect) - 0.5 * imageWidth + (offsetX * i);
        [imageView setFrame:imageRect];
    }
}

What we are doing here is very similar to what we did previously. We’re taking our position in the -1 to 1 range and converting it to an offset x amount. Then we iterate through the fluid image views and apply that offset to each frame relative to the fixed image frame.

You can view the full source on Github.