2
0

MWZoomingScrollView.m 16 KB

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