MWZoomingScrollView.m 12 KB

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