// // ZoomingScrollView.m // MWPhotoBrowser // // Created by Michael Waterfall on 14/10/2010. // Copyright 2010 d3i. All rights reserved. // #import "DACircularProgressView.h" #import "MWCommon.h" #import "MWZoomingScrollView.h" #import "MWPhotoBrowser.h" #import "MWPhoto.h" #import "MWPhotoBrowserPrivate.h" #import "UIImage+MWPhotoBrowser.h" // Private methods and properties @interface MWZoomingScrollView () { MWPhotoBrowser __weak *_photoBrowser; MWTapDetectingView *_tapView; // for background taps MWTapDetectingImageView *_photoImageView; DACircularProgressView *_loadingIndicator; UIImageView *_loadingError; } @end @implementation MWZoomingScrollView - (id)initWithPhotoBrowser:(MWPhotoBrowser *)browser { if ((self = [super init])) { // Setup _index = NSUIntegerMax; _photoBrowser = browser; // Tap view for background _tapView = [[MWTapDetectingView alloc] initWithFrame:self.bounds]; _tapView.tapDelegate = self; _tapView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; _tapView.backgroundColor = [UIColor blackColor]; [self addSubview:_tapView]; // Image view _photoImageView = [[MWTapDetectingImageView alloc] initWithFrame:CGRectZero]; _photoImageView.tapDelegate = self; _photoImageView.contentMode = UIViewContentModeCenter; _photoImageView.backgroundColor = [UIColor blackColor]; [self addSubview:_photoImageView]; // Loading indicator _loadingIndicator = [[DACircularProgressView alloc] initWithFrame:CGRectMake(140.0f, 30.0f, 40.0f, 40.0f)]; _loadingIndicator.userInteractionEnabled = NO; _loadingIndicator.thicknessRatio = 0.1; _loadingIndicator.roundedCorners = NO; _loadingIndicator.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleRightMargin; [self addSubview:_loadingIndicator]; // Listen progress notifications [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(setProgressFromNotification:) name:MWPHOTO_PROGRESS_NOTIFICATION object:nil]; // Setup self.backgroundColor = [UIColor blackColor]; self.delegate = self; self.showsHorizontalScrollIndicator = NO; self.showsVerticalScrollIndicator = NO; self.decelerationRate = UIScrollViewDecelerationRateFast; self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; } return self; } - (void)dealloc { if ([_photo respondsToSelector:@selector(cancelAnyLoading)]) { [_photo cancelAnyLoading]; } [[NSNotificationCenter defaultCenter] removeObserver:self]; } - (void)prepareForReuse { [self hideImageFailure]; self.photo = nil; self.captionView = nil; self.selectedButton = nil; self.playButton = nil; _photoImageView.hidden = NO; _photoImageView.image = nil; _index = NSUIntegerMax; } - (BOOL)displayingVideo { return [_photo respondsToSelector:@selector(isVideo)] && _photo.isVideo; } - (void)setImageHidden:(BOOL)hidden { _photoImageView.hidden = hidden; } #pragma mark - Image -(void)setPhoto:(id)photo thumbPhoto:(id)thumbPhoto { // Cancel any loading on old photo if (_photo && photo == nil) { if ([_photo respondsToSelector:@selector(cancelAnyLoading)]) { [_photo cancelAnyLoading]; } } _photo = photo; UIImage *img = [_photoBrowser imageForPhoto:_photo]; if (img) { [self displayImage]; } else { [thumbPhoto performLoadUnderlyingImageAndNotify]; [self displayThumbnanl:thumbPhoto.underlyingImage]; [self showLoadingIndicator]; } } - (void)setPhoto:(id)photo { // Cancel any loading on old photo if (_photo && photo == nil) { if ([_photo respondsToSelector:@selector(cancelAnyLoading)]) { [_photo cancelAnyLoading]; } } _photo = photo; UIImage *img = [_photoBrowser imageForPhoto:_photo]; if (img) { [self displayImage]; } else { // Will be loading so show loading [self showLoadingIndicator]; } } - (void)displayThumbnanl:(UIImage *)thumbnail { if(thumbnail) { self.maximumZoomScale = 1; self.minimumZoomScale = 1; self.zoomScale = 1; self.contentSize = CGSizeMake(0, 0); // Set image _photoImageView.image = thumbnail; _photoImageView.hidden = NO; // Setup photo frame CGRect photoImageViewFrame; photoImageViewFrame.origin = CGPointZero; photoImageViewFrame.size = thumbnail.size; _photoImageView.frame = photoImageViewFrame; self.contentSize = photoImageViewFrame.size; [self setMaxMinZoomScalesForCurrentBounds]; } } // Get and display image - (void)displayImage { if (_photo) { // Reset self.maximumZoomScale = 1; self.minimumZoomScale = 1; self.zoomScale = 1; self.contentSize = CGSizeMake(0, 0); // Get image from browser as it handles ordering of fetching UIImage *img = [_photoBrowser imageForPhoto:_photo]; if (img) { // Hide indicator [self hideLoadingIndicator]; // Set image _photoImageView.image = img; _photoImageView.hidden = NO; // Setup photo frame CGRect photoImageViewFrame; photoImageViewFrame.origin = CGPointZero; photoImageViewFrame.size = img.size; _photoImageView.frame = photoImageViewFrame; self.contentSize = photoImageViewFrame.size; // Set zoom to minimum zoom [self setMaxMinZoomScalesForCurrentBounds]; } else { // Show image failure [self displayImageFailure]; } [self setNeedsLayout]; } } // Image failed so just show black! - (void)displayImageFailure { [self hideLoadingIndicator]; // _photoImageView.image = nil; // Show if image is not empty if (![_photo respondsToSelector:@selector(emptyImage)] || !_photo.emptyImage) { if (!_loadingError) { _loadingError = [UIImageView new]; _loadingError.image = [UIImage imageForResourcePath:@"MWPhotoBrowser.bundle/ImageError" ofType:@"png" inBundle:[NSBundle bundleForClass:[self class]]]; _loadingError.userInteractionEnabled = NO; _loadingError.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleRightMargin; [_loadingError sizeToFit]; [self addSubview:_loadingError]; } _loadingError.frame = CGRectMake(floorf((self.bounds.size.width - _loadingError.frame.size.width) / 2.), floorf((self.bounds.size.height - _loadingError.frame.size.height) / 2), _loadingError.frame.size.width, _loadingError.frame.size.height); } } - (void)hideImageFailure { if (_loadingError) { [_loadingError removeFromSuperview]; _loadingError = nil; } } #pragma mark - Loading Progress - (void)setProgressFromNotification:(NSNotification *)notification { dispatch_async(dispatch_get_main_queue(), ^{ NSDictionary *dict = [notification object]; id photoWithProgress = [dict objectForKey:@"photo"]; if (photoWithProgress == self.photo) { float progress = [[dict valueForKey:@"progress"] floatValue]; _loadingIndicator.progress = MAX(MIN(1, progress), 0); } }); } - (void)hideLoadingIndicator { _loadingIndicator.hidden = YES; } - (void)showLoadingIndicator { self.zoomScale = 0; self.minimumZoomScale = 0; self.maximumZoomScale = 0; _loadingIndicator.progress = 0; _loadingIndicator.hidden = NO; [self hideImageFailure]; } #pragma mark - Setup - (CGFloat)initialZoomScaleWithMinScale { CGFloat zoomScale = self.minimumZoomScale; if (_photoImageView && _photoBrowser.zoomPhotosToFill) { // Zoom image to fill if the aspect ratios are fairly similar CGSize boundsSize = self.bounds.size; CGSize imageSize = _photoImageView.image.size; CGFloat boundsAR = boundsSize.width / boundsSize.height; CGFloat imageAR = imageSize.width / imageSize.height; CGFloat xScale = boundsSize.width / imageSize.width; // the scale needed to perfectly fit the image width-wise CGFloat yScale = boundsSize.height / imageSize.height; // the scale needed to perfectly fit the image height-wise // Zooms standard portrait images on a 3.5in screen but not on a 4in screen. if (ABS(boundsAR - imageAR) < 0.17) { zoomScale = MAX(xScale, yScale); // Ensure we don't zoom in or out too far, just in case zoomScale = MIN(MAX(self.minimumZoomScale, zoomScale), self.maximumZoomScale); } } return zoomScale; } - (void)setMaxMinZoomScalesForCurrentBounds { // Reset self.maximumZoomScale = 1; self.minimumZoomScale = 1; self.zoomScale = 1; // Bail if no image if (_photoImageView.image == nil) return; // Reset position _photoImageView.frame = CGRectMake(0, 0, _photoImageView.frame.size.width, _photoImageView.frame.size.height); // Sizes CGSize boundsSize = self.bounds.size; CGSize imageSize = _photoImageView.image.size; // Calculate Min CGFloat xScale = boundsSize.width / imageSize.width; // the scale needed to perfectly fit the image width-wise CGFloat yScale = boundsSize.height / imageSize.height; // the scale needed to perfectly fit the image height-wise CGFloat minScale = MIN(xScale, yScale); // use minimum of these to allow the image to become fully visible // Calculate Max CGFloat maxScale = 3; if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { // Let them go a bit bigger on a bigger screen! maxScale = 4; } // Image is smaller than screen so no zooming! if (xScale >= 1 && yScale >= 1) { minScale = 1.0; } // Set min/max zoom self.maximumZoomScale = maxScale; self.minimumZoomScale = minScale; // Initial zoom self.zoomScale = [self initialZoomScaleWithMinScale]; // If we're zooming to fill then centralise // if (self.zoomScale != minScale) { // // // Centralise // self.contentOffset = CGPointMake((imageSize.width * self.zoomScale - boundsSize.width) / 2.0, // (imageSize.height * self.zoomScale - boundsSize.height) / 2.0); // // } // // Disable scrolling initially until the first pinch to fix issues with swiping on an initally zoomed in photo self.scrollEnabled = NO; // If it's a video then disable zooming if ([self displayingVideo]) { self.maximumZoomScale = self.zoomScale; self.minimumZoomScale = self.zoomScale; } // Layout [self setNeedsLayout]; } #pragma mark - Layout - (void)layoutSubviews { // Update tap view frame _tapView.frame = self.bounds; // Position indicators (centre does not seem to work!) if (!_loadingIndicator.hidden) _loadingIndicator.frame = CGRectMake(floorf((self.bounds.size.width - _loadingIndicator.frame.size.width) / 2.), floorf((self.bounds.size.height - _loadingIndicator.frame.size.height) / 2), _loadingIndicator.frame.size.width, _loadingIndicator.frame.size.height); if (_loadingError) _loadingError.frame = CGRectMake(floorf((self.bounds.size.width - _loadingError.frame.size.width) / 2.), floorf((self.bounds.size.height - _loadingError.frame.size.height) / 2), _loadingError.frame.size.width, _loadingError.frame.size.height); // Super [super layoutSubviews]; // Center the image as it becomes smaller than the size of the screen CGSize boundsSize = self.bounds.size; CGRect frameToCenter = _photoImageView.frame; // Horizontally if (frameToCenter.size.width < boundsSize.width) { frameToCenter.origin.x = floorf((boundsSize.width - frameToCenter.size.width) / 2.0); } else { frameToCenter.origin.x = 0; } // Vertically if (frameToCenter.size.height < boundsSize.height) { frameToCenter.origin.y = floorf((boundsSize.height - frameToCenter.size.height) / 2.0); } else { frameToCenter.origin.y = 0; } // Center if (!CGRectEqualToRect(_photoImageView.frame, frameToCenter)) _photoImageView.frame = frameToCenter; } #pragma mark - UIScrollViewDelegate - (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView { return _photoImageView; } - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { [_photoBrowser cancelControlHiding]; } - (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view { self.scrollEnabled = YES; // reset [_photoBrowser cancelControlHiding]; } - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { [_photoBrowser hideControlsAfterDelay]; } - (void)scrollViewDidZoom:(UIScrollView *)scrollView { [self setNeedsLayout]; [self layoutIfNeeded]; } #pragma mark - Tap Detection - (void)handleSingleTap:(CGPoint)touchPoint { [_photoBrowser performSelector:@selector(toggleControls) withObject:nil afterDelay:0.2]; } - (void)handleDoubleTap:(CGPoint)touchPoint { // Dont double tap to zoom if showing a video if ([self displayingVideo]) { return; } // Cancel any single tap handling [NSObject cancelPreviousPerformRequestsWithTarget:_photoBrowser]; // Zoom if (self.zoomScale != self.minimumZoomScale && self.zoomScale != [self initialZoomScaleWithMinScale]) { // Zoom out [self setZoomScale:self.minimumZoomScale animated:YES]; } else { // Zoom in to twice the size CGFloat newZoomScale = ((self.maximumZoomScale + self.minimumZoomScale) / 2); CGFloat xsize = self.bounds.size.width / newZoomScale; CGFloat ysize = self.bounds.size.height / newZoomScale; [self zoomToRect:CGRectMake(touchPoint.x - xsize/2, touchPoint.y - ysize/2, xsize, ysize) animated:YES]; } // Delay controls [_photoBrowser hideControlsAfterDelay]; } // Image View - (void)imageView:(UIImageView *)imageView singleTapDetected:(UITouch *)touch { [self handleSingleTap:[touch locationInView:imageView]]; } - (void)imageView:(UIImageView *)imageView doubleTapDetected:(UITouch *)touch { [self handleDoubleTap:[touch locationInView:imageView]]; } // Background View - (void)view:(UIView *)view singleTapDetected:(UITouch *)touch { // Translate touch location to image view location CGFloat touchX = [touch locationInView:view].x; CGFloat touchY = [touch locationInView:view].y; touchX *= 1/self.zoomScale; touchY *= 1/self.zoomScale; touchX += self.contentOffset.x; touchY += self.contentOffset.y; [self handleSingleTap:CGPointMake(touchX, touchY)]; } - (void)view:(UIView *)view doubleTapDetected:(UITouch *)touch { // Translate touch location to image view location CGFloat touchX = [touch locationInView:view].x; CGFloat touchY = [touch locationInView:view].y; touchX *= 1/self.zoomScale; touchY *= 1/self.zoomScale; touchX += self.contentOffset.x; touchY += self.contentOffset.y; [self handleDoubleTap:CGPointMake(touchX, touchY)]; } @end