I'm displaying a series of images in a UIScrollView. I pretty much want to replicate the Photos application.
My current architecture is:
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With