Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Handling touch events within a child UIScrollView

Tags:

iphone

I'm displaying a series of images in a UIScrollView. I pretty much want to replicate the Photos application.

My current architecture is:

  • A parent UIScrollView with content size that is wide enough for x number of pages + some extra space for margins in between the images.
  • Each image is contained in a UIImageView.
  • Each UIImageView is contained within its own UIScrollview which are then the subviews of the parent UIScrollView.

    So I basically have a row of UIScrollViews within a parent UIScrollView.

    The parent UIScrollView has paging enabled so I can scroll from page to page without any problems.

    The problem is how to seamlessly pan around a zoomed-in image. I have overridden the viewForZoomingInScrollView method to return the appropriate UIImageView when the user pinches in/out. I have overriden the scrollViewDidEndZooming method to set the parent view's canCancelContentTouches property to NO if the zoom scale is greater than 1.

    So users are able to pan around an image. However, they must hold their finger down for a moment to get past the small delay the parent scroll view has before sending touch events down to the subviews. Also, once the user is panning in one image, the next/prev images do not enter the viewable area when the user has reached the border of the current image.

    Any ideas?

    Thanks.

  • like image 964
    Ryan Brubaker Avatar asked Oct 27 '08 19:10

    Ryan Brubaker


    1 Answers

    Yay! I tried to approach the problem with just one UIScrollView, and I think I found a solution.

    Before the user starts zooming (in viewForZoomingInScrollView:), I switch the scroll view into zooming mode (remove all additional pages, reset content size and offset). When the user zooms out to scale 1.00 (in scrollViewDidEndZooming:withView:atScale:), I switch back to paging view (add all pages back, adjust content size and offset).

    Here's the code of a simple view controller that does just that. This sample switches between, zooms and pans three large UIImageViews.

    Note that a single view controller with a handful of functions is all it takes, no need to subclass UIScrollView or something.

    typedef enum {
        ScrollViewModeNotInitialized,           // view has just been loaded
        ScrollViewModePaging,                   // fully zoomed out, swiping enabled
        ScrollViewModeZooming,                  // zoomed in, panning enabled
        ScrollViewModeAnimatingFullZoomOut,     // fully zoomed out, animations not yet finished
        ScrollViewModeInTransition,             // during the call to setPagingMode to ignore scrollViewDidScroll events
    } ScrollViewMode;
    
    @interface ScrollingMadnessViewController : UIViewController <UIScrollViewDelegate> {
        UIScrollView *scrollView;
        NSArray *pageViews;
        NSUInteger currentPage;
        ScrollViewMode scrollViewMode;
    }
    
    @end
    
    @implementation ScrollingMadnessViewController
    
    - (void)setPagingMode {
        NSLog(@"setPagingMode");
        if (scrollViewMode != ScrollViewModeAnimatingFullZoomOut && scrollViewMode != ScrollViewModeNotInitialized)
            return; // setPagingMode is called after a delay, so something might have changed since it was scheduled
        scrollViewMode = ScrollViewModeInTransition; // to ignore scrollViewDidScroll when setting contentOffset
    
        // reposition pages side by side, add them back to the view
        CGSize pageSize = scrollView.frame.size;
    
        NSUInteger page = 0;
        for (UIView *view in pageViews) {
            if (!view.superview)
                [scrollView addSubview:view];
            view.frame = CGRectMake(pageSize.width * page++, 0, pageSize.width, pageSize.height);
        }
    
        scrollView.pagingEnabled = YES;
        scrollView.showsVerticalScrollIndicator = scrollView.showsHorizontalScrollIndicator = NO;
        scrollView.contentSize = CGSizeMake(pageSize.width * [pageViews count], pageSize.height);
        scrollView.contentOffset = CGPointMake(pageSize.width * currentPage, 0);
    
        scrollViewMode = ScrollViewModePaging;
    }
    
    - (void)setZoomingMode {
        NSLog(@"setZoomingMode");
        scrollViewMode = ScrollViewModeInTransition; // to ignore scrollViewDidScroll when setting contentOffset
    
        CGSize pageSize = scrollView.frame.size;
    
        // hide all pages besides the current one
        NSUInteger page = 0;
        for (UIView *view in pageViews)
            if (currentPage != page++)
                [view removeFromSuperview];
    
        // move the current page to (0, 0), as if no other pages ever existed
        [[pageViews objectAtIndex:currentPage] setFrame:CGRectMake(0, 0, pageSize.width, pageSize.height)];
    
        scrollView.pagingEnabled = NO;
        scrollView.showsVerticalScrollIndicator = scrollView.showsHorizontalScrollIndicator = YES;
        scrollView.contentSize = pageSize;
        scrollView.contentOffset = CGPointZero;
    
        scrollViewMode = ScrollViewModeZooming;
    }
    
    - (void)loadView {
        CGRect frame = [UIScreen mainScreen].applicationFrame;
        scrollView = [[UIScrollView alloc] initWithFrame:frame];
        scrollView.delegate = self;
        scrollView.maximumZoomScale = 2.0f;
        scrollView.minimumZoomScale = 1.0f;
    
        UIImageView *imageView1 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"red.png"]];
        UIImageView *imageView2 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"green.png"]];
        UIImageView *imageView3 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"yellow-blue.png"]];
    
        // in a real app, you most likely want to have an array of view controllers, not views;
        // also should be instantiating those views and view controllers lazily
        pageViews = [[NSArray alloc] initWithObjects:imageView1, imageView2, imageView3, nil];
    
        self.view = scrollView;
    }
    
    - (void)setCurrentPage:(NSUInteger)page {
        if (page == currentPage)
            return;
        currentPage = page;
        // in a real app, this would be a good place to instantiate more view controllers -- see SDK examples
    }
    
    - (void)viewDidLoad {
        scrollViewMode = ScrollViewModeNotInitialized;
        [self setPagingMode];
    }
    
    - (void)viewDidUnload {
        [pageViews release]; // need to release all page views here; our array is created in loadView, so just releasing it
        pageViews = nil;
    }
    
    - (void)scrollViewDidScroll:(UIScrollView *)aScrollView {
        [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(setPagingMode) object:nil];
        CGPoint offset = scrollView.contentOffset;
        NSLog(@"scrollViewDidScroll: (%f, %f)", offset.x, offset.y);
        if (scrollViewMode == ScrollViewModeAnimatingFullZoomOut && ABS(offset.x) < 1e-5 && ABS(offset.y) < 1e-5)
            // bouncing is still possible (and actually happened for me), so wait a bit more to be sure
            [self performSelector:@selector(setPagingMode) withObject:nil afterDelay:0.1];
        else if (scrollViewMode == ScrollViewModePaging)
            [self setCurrentPage:roundf(scrollView.contentOffset.x / scrollView.frame.size.width)];
    }
    
    - (UIView *)viewForZoomingInScrollView:(UIScrollView *)aScrollView {
        if (scrollViewMode != ScrollViewModeZooming)
            [self setZoomingMode];
        return [pageViews objectAtIndex:currentPage];
    }
    
    - (void)scrollViewDidEndZooming:(UIScrollView *)aScrollView withView:(UIView *)view atScale:(float)scale {
        NSLog(@"scrollViewDidEndZooming: scale = %f", scale);
        if (fabsf(scale - 1.0) < 1e-5) {
            if (scrollView.zoomBouncing)
                NSLog(@"scrollViewDidEndZooming, but zoomBouncing is still true!");
    
            // cannot call setPagingMode now because scrollView will bounce after a call to this method, resetting contentOffset to (0, 0)
            scrollViewMode = ScrollViewModeAnimatingFullZoomOut;
            // however sometimes bouncing will not take place
            [self performSelector:@selector(setPagingMode) withObject:nil afterDelay:0.2];
        }
    }
    
    @end
    

    Runnable sample project is available at http://github.com/andreyvit/ScrollingMadness/ (if you don't use Git, just click Download button there). A README is available there, explaining why the code was written the way it is.

    (The sample project also illustrates how to zoom a scroll view programmatically, and has ZoomScrollView class that encapsulates a solution to that. It is a neat class, but is not required for this trick. If you want an example that does not use ZoomScrollView, go back a few commits in commit history.)

    P.S. For the sake of completeness, there's TTScrollView — UIScrollView reimplemented from scratch. It's part of the great and famous Three20 library. I don't like how it feels to the user, but it does make implementing paging/scrolling/zooming dead simple.

    P.P.S. The real Photo app by Apple has pre-SDK code and uses pre-SDK classes. One can spot two classes derived from pre-SDK variant of UIScrollView inside PhotoLibrary framework, however it is not immediately clear what they do (and they do quite a lot). I can easily believe this effect used to be harder to achieve in pre-SDK times.

    like image 165
    Andrey Tarantsov Avatar answered Sep 26 '22 01:09

    Andrey Tarantsov