4

In the latest Expedia app for iOS, they have a very interesting effect that I am trying to wrap my head around. The have two columns of infinitely scrolling subviews which I know can be accomplished with 2 scrollviews. The interesting part is that the overall scrollview appears to have a linen background that stays static and can be seen in the gap between each of the subview cells. The really cool part is that the subviews have a different background that stays in place. In the screenshot below it is city skyline image. When the subviews scroll, the city image can only be seen behind the subview cells. It appears to be some sort of masking trick but I can't quite figure out how the effect is done. How can I achieve the same result?

Essentially, how can you show a static background behind subviews that act as little windows and not show the linen. The linen should only be shown around the cells.

You can download the app, hit airplane mode and try it for yourself.

Here is a screenshot:enter image description here

Here is another to show that the cells scrolled but the city stays the same:

enter image description here

SonnyBurnette
  • 680
  • 3
  • 11
  • 25

2 Answers2

1

I'd like to found an elegant solution, for now I would do it by tracking the visible subviews offset and configuring their appearance.

Please check the result at sample project.

For the future reference I'll attach the code below:

ViewController.m

//
//  OSViewController.m
//  ScrollMasks
//
//  Created by #%$^Q& on 11/30/12.
//  Copyright (c) 2012 Demo. All rights reserved.
//

#import "OSViewController.h"

@interface OSViewController ()

// subviews
@property (strong) IBOutlet UIScrollView * scrollView;

// all the subviews
@property (strong) NSArray * maskedSubviews;
// subviews visible at scrollview, we'll update only them
@property (strong) NSArray * visibleMaskedSubviews;

// updates the views from visibleMaskedSubviews
-(void) updateVisibleSubviews;
// updates the visibleMaskedSubviews array with the given scrollView offset
-(void) updateVisibleSubviewsArrayForOffset:(CGPoint) offset;
@end

@implementation OSViewController

-(void) unused {}

#pragma mark - view

-(void) viewWillAppear:(BOOL)animated {
    [self updateVisibleSubviews];
    [super viewWillAppear:animated];
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    /*
     See -updateVisibleSubviews comment for the class comments
     */
    UIView * newMaskedView = nil;
    NSMutableArray * newMaskedSubviews = [NSMutableArray array];

    const CGSize scrollViewSize = self.scrollView.bounds.size;
    const int totalSubviews = 10;
    const float offset = 20.;
    const float height = 100.;

    UIImage * maskImage = [UIImage imageNamed:@"PeeringFrog.jpg"];

    /*

     // Uncomment to compare

     UIImageView * iv = [[UIImageView alloc] initWithFrame:self.scrollView.bounds];
     iv.image = maskImage;
     [self.view insertSubview:iv atIndex:0];
     */

    // add scrollview subviews
    for (int i = 0; i < totalSubviews; i++) {

        CGRect newViewFrame = CGRectMake(offset, offset*(i+1) + height*i, scrollViewSize.width - offset*2, height);
        newMaskedView = [UIView new];
        newMaskedView.frame = newViewFrame;
        newMaskedView.clipsToBounds = YES;
        newMaskedView.backgroundColor = [UIColor redColor];
        newMaskedView.autoresizingMask = UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleWidth;

        UIImageView * maskImageView = [UIImageView new];
        maskImageView.frame = CGRectMake(0, 0, self.scrollView.bounds.size.width, self.scrollView.bounds.size.height);
        maskImageView.image = maskImage;
        [newMaskedView addSubview:maskImageView];

        [self.scrollView addSubview:newMaskedView];
        [newMaskedSubviews addObject:newMaskedView];
    }

    self.scrollView.contentSize = CGSizeMake(scrollViewSize.width, (height+offset)*totalSubviews + offset*2);

    self.maskedSubviews = [NSArray arrayWithArray:newMaskedSubviews];
    [self updateVisibleSubviewsArrayForOffset:self.scrollView.contentOffset];
}

-(void) updateVisibleSubviews {
    [self updateVisibleSubviewsArrayForOffset:self.scrollView.contentOffset];

    for (UIView * view in self.visibleMaskedSubviews) {

        /*
        TODO:
         view must be UIView subclass with the imageView initializer and imageView frame update method
        */

        CGPoint viewOffset = [self.view convertPoint:CGPointZero fromView:view];
        UIImageView * subview = [[view subviews] objectAtIndex:0];
        CGRect subviewFrame = subview.frame;
        subviewFrame = CGRectMake(-viewOffset.x, -viewOffset.y, subviewFrame.size.width, subviewFrame.size.height);
        subview.frame = subviewFrame;
    }
}


#pragma mark - scrollview delegate & utilities
-(void) scrollViewDidScroll:(UIScrollView *)scrollView {
    [self updateVisibleSubviews];
}

-(void) updateVisibleSubviewsArrayForOffset:(CGPoint) offset {

    NSMutableArray * newVisibleMaskedSubviews = [NSMutableArray array];
    for (UIView * view in self.maskedSubviews) {
        CGRect intersectionRect = CGRectIntersection(view.frame, self.scrollView.bounds);
        if (NO == CGRectIsNull(intersectionRect)) {
            [newVisibleMaskedSubviews addObject:view];
        }
    }

    self.visibleMaskedSubviews = [NSArray arrayWithArray:newVisibleMaskedSubviews];
}

#pragma mark - memory
- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

ViewController.h

//
//  OSViewController.h
//  ScrollMasks
//
//  Created by #%$^Q& on 11/30/12.
//  Copyright (c) 2012 Demo. All rights reserved.
//

/*
 PeeringFrog image is taken (and resized) from Apple sample project "PhotoScroller"
 */

#import <UIKit/UIKit.h>

@interface OSViewController : UIViewController <UIScrollViewDelegate>

@end
A-Live
  • 8,904
  • 2
  • 39
  • 74
  • I'm in the process of looking through the code but this sample project appears to replicate the exact effect I was looking for. – SonnyBurnette Nov 30 '12 at 17:52
  • @SonnyBurnette it does, I'd recommend not to accept the answer yet as we could miss some beautiful solution based on layers. My solution is based on the scrollView delegate which is not as nice and requires much logic. – A-Live Nov 30 '12 at 17:59
  • Good point. I'll lift the acceptance and revisit it after a bit. – SonnyBurnette Nov 30 '12 at 18:04
0

I did something similar a few years ago. At first I tried using the CGImageMaskCreate stuff, but found it far easier just to create an image that had transparent "cutouts" and then used animation effects to scroll the picture(s) under it.

For your case, I'd find an image of linen the size of the screen. Then I'd use a image editor (I use GIMP) to draw some number of boxes on the linen using a flat color. Then I'd map that box color to transparent to make the cutouts. There's other ways to do this, but that's the way I do it.

In your app, add two or more image views to the main view. Don't worry about the placement because that will be determined at run time. You'll want to set these image views to contain the images you want to have "scroll" under. Then add your linen-with-cutouts UIImageView so it's on top and it's occupying the entire screen size. Make sure that the top UIImageView's background is set to transparent.

When the app starts, layout your "underneath" imageviews, top to bottom, and then start a [UIView beginAnimation] that scrolls your underneath images views up by modifying the "y" position. This animation should have a done callback that gets called when the top image view is completely off the screen. Then, in the done callback, layout the current state and start the animation again. Here's the guts of the code I used (but note, my scrolling was right to left, not bottom to top and my images were all the same size.)

    - (void)doAnimationSet
    {

        [iv1 setFrame:CGRectMake(0, 0, imageWidth, imageHeight)];
        [iv2 setFrame:CGRectMake(imageWidth, 0, imageWidth, imageHeight)];
        [iv3 setFrame:CGRectMake(imageWidth*2, 0, imageWidth, imageHeight)];

        [self loadNextImageSet];

        [UIView beginAnimations:nil context:nil];
        [UIView setAnimationDuration:10];
        [UIView setAnimationCurve:UIViewAnimationCurveLinear];

        [iv1 setFrame:CGRectMake(-imageWidth, 0, imageWidth, imageHeight)];
        [iv2 setFrame:CGRectMake(0, 0, imageWidth, imageHeight)];
        [iv3 setFrame:CGRectMake(imageWidth, 0, imageWidth, imageHeight)];

        [UIView setAnimationDelegate:self];
        [UIView setAnimationDidStopSelector:@selector(doneAnimation:finished:context:)];

        [UIView commitAnimations];

    }

    - (void)doneAnimation:(NSString *)aid finished:(BOOL)fin context:(void *)context
    {
        [self doAnimationSet];
    }

This should give you the effect that you are looking for. Good luck :)

Mike M
  • 4,358
  • 1
  • 28
  • 48
  • Is that for infinite horizontal scrolling done by a gesture ? – A-Live Nov 30 '12 at 17:47
  • @A-Live, no it's not driven by a gesture. – Mike M Nov 30 '12 at 17:52
  • I can't imagine this snippet to work with a scrollView delegate, could you please explain it better ? – A-Live Nov 30 '12 at 17:56
  • @A-Live - I misunderstood what sonny was asking for. My example simply does an animation-based scroll of images underneath a mask. However, after I installed the expedia app I'd take a different approach. I'd put the big image on bottom and then a scrollview on top with views that can be transparent or not. – Mike M Nov 30 '12 at 21:56