MWZoomingScrollView.m 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. //
  2. // ZoomingScrollView.m
  3. // MWPhotoBrowser
  4. //
  5. // Created by Michael Waterfall on 14/10/2010.
  6. // Copyright 2010 d3i. All rights reserved.
  7. //
  8. #import "MWCommon.h"
  9. #import "MWZoomingScrollView.h"
  10. #import "MWPhotoBrowser.h"
  11. #import "MWPhoto.h"
  12. #import "DACircularProgressView.h"
  13. #import "MWPhotoBrowserPrivate.h"
  14. // Private methods and properties
  15. @interface MWZoomingScrollView () {
  16. MWPhotoBrowser __weak *_photoBrowser;
  17. MWTapDetectingView *_tapView; // for background taps
  18. MWTapDetectingImageView *_photoImageView;
  19. DACircularProgressView *_loadingIndicator;
  20. UIImageView *_loadingError;
  21. }
  22. @end
  23. @implementation MWZoomingScrollView
  24. - (id)initWithPhotoBrowser:(MWPhotoBrowser *)browser {
  25. if ((self = [super init])) {
  26. // Setup
  27. _index = NSUIntegerMax;
  28. _photoBrowser = browser;
  29. // Tap view for background
  30. _tapView = [[MWTapDetectingView alloc] initWithFrame:self.bounds];
  31. _tapView.tapDelegate = self;
  32. _tapView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
  33. _tapView.backgroundColor = [UIColor blackColor];
  34. [self addSubview:_tapView];
  35. // Image view
  36. _photoImageView = [[MWTapDetectingImageView alloc] initWithFrame:CGRectZero];
  37. _photoImageView.tapDelegate = self;
  38. _photoImageView.contentMode = UIViewContentModeCenter;
  39. _photoImageView.backgroundColor = [UIColor blackColor];
  40. [self addSubview:_photoImageView];
  41. // Loading indicator
  42. _loadingIndicator = [[DACircularProgressView alloc] initWithFrame:CGRectMake(140.0f, 30.0f, 40.0f, 40.0f)];
  43. _loadingIndicator.userInteractionEnabled = NO;
  44. if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"7")) {
  45. _loadingIndicator.thicknessRatio = 0.1;
  46. _loadingIndicator.roundedCorners = NO;
  47. } else {
  48. _loadingIndicator.thicknessRatio = 0.2;
  49. _loadingIndicator.roundedCorners = YES;
  50. }
  51. _loadingIndicator.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin |
  52. UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleRightMargin;
  53. [self addSubview:_loadingIndicator];
  54. // Listen progress notifications
  55. [[NSNotificationCenter defaultCenter] addObserver:self
  56. selector:@selector(setProgressFromNotification:)
  57. name:MWPHOTO_PROGRESS_NOTIFICATION
  58. object:nil];
  59. // Setup
  60. self.backgroundColor = [UIColor blackColor];
  61. self.delegate = self;
  62. self.showsHorizontalScrollIndicator = NO;
  63. self.showsVerticalScrollIndicator = NO;
  64. self.decelerationRate = UIScrollViewDecelerationRateFast;
  65. self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
  66. }
  67. return self;
  68. }
  69. - (void)dealloc {
  70. [[NSNotificationCenter defaultCenter] removeObserver:self];
  71. }
  72. - (void)prepareForReuse {
  73. [self hideImageFailure];
  74. self.photo = nil;
  75. self.captionView = nil;
  76. self.selectedButton = nil;
  77. _photoImageView.image = nil;
  78. _index = NSUIntegerMax;
  79. }
  80. #pragma mark - Image
  81. - (void)setPhoto:(id<MWPhoto>)photo {
  82. // Cancel any loading on old photo
  83. if (_photo && photo == nil) {
  84. if ([_photo respondsToSelector:@selector(cancelAnyLoading)]) {
  85. [_photo cancelAnyLoading];
  86. }
  87. }
  88. _photo = photo;
  89. UIImage *img = [_photoBrowser imageForPhoto:_photo];
  90. if (img) {
  91. [self displayImage];
  92. } else {
  93. // Will be loading so show loading
  94. [self showLoadingIndicator];
  95. }
  96. }
  97. // Get and display image
  98. - (void)displayImage {
  99. if (_photo && _photoImageView.image == nil) {
  100. // Reset
  101. self.maximumZoomScale = 1;
  102. self.minimumZoomScale = 1;
  103. self.zoomScale = 1;
  104. self.contentSize = CGSizeMake(0, 0);
  105. // Get image from browser as it handles ordering of fetching
  106. UIImage *img = [_photoBrowser imageForPhoto:_photo];
  107. if (img) {
  108. // Hide indicator
  109. [self hideLoadingIndicator];
  110. // Set image
  111. _photoImageView.image = img;
  112. _photoImageView.hidden = NO;
  113. // Setup photo frame
  114. CGRect photoImageViewFrame;
  115. photoImageViewFrame.origin = CGPointZero;
  116. photoImageViewFrame.size = img.size;
  117. _photoImageView.frame = photoImageViewFrame;
  118. self.contentSize = photoImageViewFrame.size;
  119. // Set zoom to minimum zoom
  120. [self setMaxMinZoomScalesForCurrentBounds];
  121. } else {
  122. // Failed no image
  123. [self displayImageFailure];
  124. }
  125. [self setNeedsLayout];
  126. }
  127. }
  128. // Image failed so just show black!
  129. - (void)displayImageFailure {
  130. [self hideLoadingIndicator];
  131. _photoImageView.image = nil;
  132. if (!_loadingError) {
  133. _loadingError = [UIImageView new];
  134. _loadingError.image = [UIImage imageNamed:@"MWPhotoBrowser.bundle/images/ImageError.png"];
  135. _loadingError.userInteractionEnabled = NO;
  136. _loadingError.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin |
  137. UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleRightMargin;
  138. [_loadingError sizeToFit];
  139. [self addSubview:_loadingError];
  140. }
  141. _loadingError.frame = CGRectMake(floorf((self.bounds.size.width - _loadingError.frame.size.width) / 2.),
  142. floorf((self.bounds.size.height - _loadingError.frame.size.height) / 2),
  143. _loadingError.frame.size.width,
  144. _loadingError.frame.size.height);
  145. }
  146. - (void)hideImageFailure {
  147. if (_loadingError) {
  148. [_loadingError removeFromSuperview];
  149. _loadingError = nil;
  150. }
  151. }
  152. #pragma mark - Loading Progress
  153. - (void)setProgressFromNotification:(NSNotification *)notification {
  154. NSDictionary *dict = [notification object];
  155. id <MWPhoto> photoWithProgress = [dict objectForKey:@"photo"];
  156. if (photoWithProgress == self.photo) {
  157. float progress = [[dict valueForKey:@"progress"] floatValue];
  158. _loadingIndicator.progress = MAX(MIN(1, progress), 0);
  159. }
  160. }
  161. - (void)hideLoadingIndicator {
  162. _loadingIndicator.hidden = YES;
  163. }
  164. - (void)showLoadingIndicator {
  165. self.zoomScale = 0;
  166. self.minimumZoomScale = 0;
  167. self.maximumZoomScale = 0;
  168. _loadingIndicator.progress = 0;
  169. _loadingIndicator.hidden = NO;
  170. [self hideImageFailure];
  171. }
  172. #pragma mark - Setup
  173. - (CGFloat)initialZoomScaleWithMinScale {
  174. CGFloat zoomScale = self.minimumZoomScale;
  175. if (_photoImageView && _photoBrowser.zoomPhotosToFill) {
  176. // Zoom image to fill if the aspect ratios are fairly similar
  177. CGSize boundsSize = self.bounds.size;
  178. CGSize imageSize = _photoImageView.image.size;
  179. CGFloat boundsAR = boundsSize.width / boundsSize.height;
  180. CGFloat imageAR = imageSize.width / imageSize.height;
  181. CGFloat xScale = boundsSize.width / imageSize.width; // the scale needed to perfectly fit the image width-wise
  182. CGFloat yScale = boundsSize.height / imageSize.height; // the scale needed to perfectly fit the image height-wise
  183. // Zooms standard portrait images on a 3.5in screen but not on a 4in screen.
  184. if (ABS(boundsAR - imageAR) < 0.17) {
  185. zoomScale = MAX(xScale, yScale);
  186. // Ensure we don't zoom in or out too far, just in case
  187. zoomScale = MIN(MAX(self.minimumZoomScale, zoomScale), self.maximumZoomScale);
  188. }
  189. }
  190. return zoomScale;
  191. }
  192. - (void)setMaxMinZoomScalesForCurrentBounds {
  193. // Reset
  194. self.maximumZoomScale = 1;
  195. self.minimumZoomScale = 1;
  196. self.zoomScale = 1;
  197. // Bail if no image
  198. if (_photoImageView.image == nil) return;
  199. // Reset position
  200. _photoImageView.frame = CGRectMake(0, 0, _photoImageView.frame.size.width, _photoImageView.frame.size.height);
  201. // Sizes
  202. CGSize boundsSize = self.bounds.size;
  203. CGSize imageSize = _photoImageView.image.size;
  204. // Calculate Min
  205. CGFloat xScale = boundsSize.width / imageSize.width; // the scale needed to perfectly fit the image width-wise
  206. CGFloat yScale = boundsSize.height / imageSize.height; // the scale needed to perfectly fit the image height-wise
  207. CGFloat minScale = MIN(xScale, yScale); // use minimum of these to allow the image to become fully visible
  208. // Calculate Max
  209. CGFloat maxScale = 3;
  210. if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
  211. // Let them go a bit bigger on a bigger screen!
  212. maxScale = 4;
  213. }
  214. // Image is smaller than screen so no zooming!
  215. if (xScale >= 1 && yScale >= 1) {
  216. minScale = 1.0;
  217. }
  218. // Set min/max zoom
  219. self.maximumZoomScale = maxScale;
  220. self.minimumZoomScale = minScale;
  221. // Initial zoom
  222. self.zoomScale = [self initialZoomScaleWithMinScale];
  223. // If we're zooming to fill then centralise
  224. if (self.zoomScale != minScale) {
  225. // Centralise
  226. self.contentOffset = CGPointMake((imageSize.width * self.zoomScale - boundsSize.width) / 2.0,
  227. (imageSize.height * self.zoomScale - boundsSize.height) / 2.0);
  228. // Disable scrolling initially until the first pinch to fix issues with swiping on an initally zoomed in photo
  229. self.scrollEnabled = NO;
  230. }
  231. // Layout
  232. [self setNeedsLayout];
  233. }
  234. #pragma mark - Layout
  235. - (void)layoutSubviews {
  236. // Update tap view frame
  237. _tapView.frame = self.bounds;
  238. // Position indicators (centre does not seem to work!)
  239. if (!_loadingIndicator.hidden)
  240. _loadingIndicator.frame = CGRectMake(floorf((self.bounds.size.width - _loadingIndicator.frame.size.width) / 2.),
  241. floorf((self.bounds.size.height - _loadingIndicator.frame.size.height) / 2),
  242. _loadingIndicator.frame.size.width,
  243. _loadingIndicator.frame.size.height);
  244. if (_loadingError)
  245. _loadingError.frame = CGRectMake(floorf((self.bounds.size.width - _loadingError.frame.size.width) / 2.),
  246. floorf((self.bounds.size.height - _loadingError.frame.size.height) / 2),
  247. _loadingError.frame.size.width,
  248. _loadingError.frame.size.height);
  249. // Super
  250. [super layoutSubviews];
  251. // Center the image as it becomes smaller than the size of the screen
  252. CGSize boundsSize = self.bounds.size;
  253. CGRect frameToCenter = _photoImageView.frame;
  254. // Horizontally
  255. if (frameToCenter.size.width < boundsSize.width) {
  256. frameToCenter.origin.x = floorf((boundsSize.width - frameToCenter.size.width) / 2.0);
  257. } else {
  258. frameToCenter.origin.x = 0;
  259. }
  260. // Vertically
  261. if (frameToCenter.size.height < boundsSize.height) {
  262. frameToCenter.origin.y = floorf((boundsSize.height - frameToCenter.size.height) / 2.0);
  263. } else {
  264. frameToCenter.origin.y = 0;
  265. }
  266. // Center
  267. if (!CGRectEqualToRect(_photoImageView.frame, frameToCenter))
  268. _photoImageView.frame = frameToCenter;
  269. }
  270. #pragma mark - UIScrollViewDelegate
  271. - (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
  272. return _photoImageView;
  273. }
  274. - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
  275. [_photoBrowser cancelControlHiding];
  276. }
  277. - (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view {
  278. self.scrollEnabled = YES; // reset
  279. [_photoBrowser cancelControlHiding];
  280. }
  281. - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
  282. [_photoBrowser hideControlsAfterDelay];
  283. }
  284. - (void)scrollViewDidZoom:(UIScrollView *)scrollView {
  285. [self setNeedsLayout];
  286. [self layoutIfNeeded];
  287. }
  288. #pragma mark - Tap Detection
  289. - (void)handleSingleTap:(CGPoint)touchPoint {
  290. [_photoBrowser performSelector:@selector(toggleControls) withObject:nil afterDelay:0.2];
  291. }
  292. - (void)handleDoubleTap:(CGPoint)touchPoint {
  293. // Cancel any single tap handling
  294. [NSObject cancelPreviousPerformRequestsWithTarget:_photoBrowser];
  295. // Zoom
  296. if (self.zoomScale != self.minimumZoomScale && self.zoomScale != [self initialZoomScaleWithMinScale]) {
  297. // Zoom out
  298. [self setZoomScale:self.minimumZoomScale animated:YES];
  299. } else {
  300. // Zoom in to twice the size
  301. CGFloat newZoomScale = ((self.maximumZoomScale + self.minimumZoomScale) / 2);
  302. CGFloat xsize = self.bounds.size.width / newZoomScale;
  303. CGFloat ysize = self.bounds.size.height / newZoomScale;
  304. [self zoomToRect:CGRectMake(touchPoint.x - xsize/2, touchPoint.y - ysize/2, xsize, ysize) animated:YES];
  305. }
  306. // Delay controls
  307. [_photoBrowser hideControlsAfterDelay];
  308. }
  309. // Image View
  310. - (void)imageView:(UIImageView *)imageView singleTapDetected:(UITouch *)touch {
  311. [self handleSingleTap:[touch locationInView:imageView]];
  312. }
  313. - (void)imageView:(UIImageView *)imageView doubleTapDetected:(UITouch *)touch {
  314. [self handleDoubleTap:[touch locationInView:imageView]];
  315. }
  316. // Background View
  317. - (void)view:(UIView *)view singleTapDetected:(UITouch *)touch {
  318. // Translate touch location to image view location
  319. CGFloat touchX = [touch locationInView:view].x;
  320. CGFloat touchY = [touch locationInView:view].y;
  321. touchX *= 1/self.zoomScale;
  322. touchY *= 1/self.zoomScale;
  323. touchX += self.contentOffset.x;
  324. touchY += self.contentOffset.y;
  325. [self handleSingleTap:CGPointMake(touchX, touchY)];
  326. }
  327. - (void)view:(UIView *)view doubleTapDetected:(UITouch *)touch {
  328. // Translate touch location to image view location
  329. CGFloat touchX = [touch locationInView:view].x;
  330. CGFloat touchY = [touch locationInView:view].y;
  331. touchX *= 1/self.zoomScale;
  332. touchY *= 1/self.zoomScale;
  333. touchX += self.contentOffset.x;
  334. touchY += self.contentOffset.y;
  335. [self handleDoubleTap:CGPointMake(touchX, touchY)];
  336. }
  337. @end