RSKImageScrollView.m 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. // This file is based on third party code, see below for the original author
  2. // and original license.
  3. // Modifications are (c) by Threema GmbH and licensed under the AGPLv3.
  4. /*
  5. File: RSKImageScrollView.m
  6. Abstract: Centers image within the scroll view and configures image sizing and display.
  7. Version: 1.3 modified by Ruslan Skorb on 8/24/14.
  8. Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple
  9. Inc. ("Apple") in consideration of your agreement to the following
  10. terms, and your use, installation, modification or redistribution of
  11. this Apple software constitutes acceptance of these terms. If you do
  12. not agree with these terms, please do not use, install, modify or
  13. redistribute this Apple software.
  14. In consideration of your agreement to abide by the following terms, and
  15. subject to these terms, Apple grants you a personal, non-exclusive
  16. license, under Apple's copyrights in this original Apple software (the
  17. "Apple Software"), to use, reproduce, modify and redistribute the Apple
  18. Software, with or without modifications, in source and/or binary forms;
  19. provided that if you redistribute the Apple Software in its entirety and
  20. without modifications, you must retain this notice and the following
  21. text and disclaimers in all such redistributions of the Apple Software.
  22. Neither the name, trademarks, service marks or logos of Apple Inc. may
  23. be used to endorse or promote products derived from the Apple Software
  24. without specific prior written permission from Apple. Except as
  25. expressly stated in this notice, no other rights or licenses, express or
  26. implied, are granted by Apple herein, including but not limited to any
  27. patent rights that may be infringed by your derivative works or by other
  28. works in which the Apple Software may be incorporated.
  29. The Apple Software is provided by Apple on an "AS IS" basis. APPLE
  30. MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
  31. THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS
  32. FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
  33. OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
  34. IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
  35. OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  36. SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  37. INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
  38. MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
  39. AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
  40. STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
  41. POSSIBILITY OF SUCH DAMAGE.
  42. Copyright (C) 2012 Apple Inc. All Rights Reserved.
  43. */
  44. #import <Foundation/Foundation.h>
  45. #import "RSKImageScrollView.h"
  46. #pragma mark -
  47. @interface RSKImageScrollView () <UIScrollViewDelegate>
  48. {
  49. CGSize _imageSize;
  50. CGPoint _pointToCenterAfterResize;
  51. CGFloat _scaleToRestoreAfterResize;
  52. }
  53. @end
  54. @implementation RSKImageScrollView
  55. - (id)initWithFrame:(CGRect)frame
  56. {
  57. self = [super initWithFrame:frame];
  58. if (self)
  59. {
  60. _aspectFill = NO;
  61. self.showsVerticalScrollIndicator = NO;
  62. self.showsHorizontalScrollIndicator = NO;
  63. self.bouncesZoom = YES;
  64. self.scrollsToTop = NO;
  65. self.decelerationRate = UIScrollViewDecelerationRateFast;
  66. self.delegate = self;
  67. /***** BEGIN THREEMA MODIFICATION: accessibilityIgnoresInvertColors *********/
  68. if (@available(iOS 11.0, *)) {
  69. self.accessibilityIgnoresInvertColors = true;
  70. }
  71. /***** END THREEMA MODIFICATION: accessibilityIgnoresInvertColors *********/
  72. }
  73. return self;
  74. }
  75. - (void)didAddSubview:(UIView *)subview
  76. {
  77. [super didAddSubview:subview];
  78. [self centerZoomView];
  79. }
  80. - (void)setAspectFill:(BOOL)aspectFill
  81. {
  82. if (_aspectFill != aspectFill) {
  83. _aspectFill = aspectFill;
  84. if (_zoomView) {
  85. [self setMaxMinZoomScalesForCurrentBounds];
  86. if (self.zoomScale < self.minimumZoomScale) {
  87. self.zoomScale = self.minimumZoomScale;
  88. }
  89. }
  90. }
  91. }
  92. - (void)setFrame:(CGRect)frame
  93. {
  94. BOOL sizeChanging = !CGSizeEqualToSize(frame.size, self.frame.size);
  95. if (sizeChanging) {
  96. [self prepareToResize];
  97. }
  98. [super setFrame:frame];
  99. if (sizeChanging) {
  100. [self recoverFromResizing];
  101. }
  102. [self centerZoomView];
  103. }
  104. #pragma mark - UIScrollViewDelegate
  105. - (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
  106. {
  107. return _zoomView;
  108. }
  109. - (void)scrollViewDidZoom:(__unused UIScrollView *)scrollView
  110. {
  111. [self centerZoomView];
  112. }
  113. #pragma mark - Center zoomView within scrollView
  114. - (void)centerZoomView
  115. {
  116. // center zoomView as it becomes smaller than the size of the screen
  117. // we need to use contentInset instead of contentOffset for better positioning when zoomView fills the screen
  118. if (self.aspectFill) {
  119. CGFloat top = 0;
  120. CGFloat left = 0;
  121. // center vertically
  122. if (self.contentSize.height < CGRectGetHeight(self.bounds)) {
  123. top = (CGRectGetHeight(self.bounds) - self.contentSize.height) * 0.5f;
  124. }
  125. // center horizontally
  126. if (self.contentSize.width < CGRectGetWidth(self.bounds)) {
  127. left = (CGRectGetWidth(self.bounds) - self.contentSize.width) * 0.5f;
  128. }
  129. self.contentInset = UIEdgeInsetsMake(top, left, top, left);
  130. } else {
  131. CGRect frameToCenter = self.zoomView.frame;
  132. // center horizontally
  133. if (CGRectGetWidth(frameToCenter) < CGRectGetWidth(self.bounds)) {
  134. frameToCenter.origin.x = (CGRectGetWidth(self.bounds) - CGRectGetWidth(frameToCenter)) * 0.5f;
  135. } else {
  136. frameToCenter.origin.x = 0;
  137. }
  138. // center vertically
  139. if (CGRectGetHeight(frameToCenter) < CGRectGetHeight(self.bounds)) {
  140. frameToCenter.origin.y = (CGRectGetHeight(self.bounds) - CGRectGetHeight(frameToCenter)) * 0.5f;
  141. } else {
  142. frameToCenter.origin.y = 0;
  143. }
  144. self.zoomView.frame = frameToCenter;
  145. }
  146. }
  147. #pragma mark - Configure scrollView to display new image
  148. - (void)displayImage:(UIImage *)image
  149. {
  150. // clear view for the previous image
  151. [_zoomView removeFromSuperview];
  152. _zoomView = nil;
  153. // reset our zoomScale to 1.0 before doing any further calculations
  154. self.zoomScale = 1.0;
  155. // make views to display the new image
  156. _zoomView = [[UIImageView alloc] initWithImage:image];
  157. [self addSubview:_zoomView];
  158. [self configureForImageSize:image.size];
  159. }
  160. - (void)configureForImageSize:(CGSize)imageSize
  161. {
  162. _imageSize = imageSize;
  163. self.contentSize = imageSize;
  164. [self setMaxMinZoomScalesForCurrentBounds];
  165. [self setInitialZoomScale];
  166. [self setInitialContentOffset];
  167. self.contentInset = UIEdgeInsetsZero;
  168. }
  169. - (void)setMaxMinZoomScalesForCurrentBounds
  170. {
  171. CGSize boundsSize = self.bounds.size;
  172. // calculate min/max zoomscale
  173. CGFloat xScale = boundsSize.width / _imageSize.width; // the scale needed to perfectly fit the image width-wise
  174. CGFloat yScale = boundsSize.height / _imageSize.height; // the scale needed to perfectly fit the image height-wise
  175. CGFloat minScale;
  176. if (!self.aspectFill) {
  177. minScale = MIN(xScale, yScale); // use minimum of these to allow the image to become fully visible
  178. } else {
  179. minScale = MAX(xScale, yScale); // use maximum of these to allow the image to fill the screen
  180. }
  181. CGFloat maxScale = MAX(xScale, yScale);
  182. // Image must fit/fill the screen, even if its size is smaller.
  183. CGFloat xImageScale = maxScale*_imageSize.width / boundsSize.width;
  184. CGFloat yImageScale = maxScale*_imageSize.height / boundsSize.height;
  185. CGFloat maxImageScale = MAX(xImageScale, yImageScale);
  186. maxImageScale = MAX(minScale, maxImageScale);
  187. maxScale = MAX(maxScale, maxImageScale);
  188. // don't let minScale exceed maxScale. (If the image is smaller than the screen, we don't want to force it to be zoomed.)
  189. if (minScale > maxScale) {
  190. minScale = maxScale;
  191. }
  192. self.maximumZoomScale = maxScale;
  193. self.minimumZoomScale = minScale;
  194. }
  195. - (void)setInitialZoomScale
  196. {
  197. CGSize boundsSize = self.bounds.size;
  198. CGFloat xScale = boundsSize.width / _imageSize.width; // the scale needed to perfectly fit the image width-wise
  199. CGFloat yScale = boundsSize.height / _imageSize.height; // the scale needed to perfectly fit the image height-wise
  200. CGFloat scale = MAX(xScale, yScale);
  201. self.zoomScale = scale;
  202. }
  203. - (void)setInitialContentOffset
  204. {
  205. CGSize boundsSize = self.bounds.size;
  206. CGRect frameToCenter = self.zoomView.frame;
  207. CGPoint contentOffset;
  208. if (CGRectGetWidth(frameToCenter) > boundsSize.width) {
  209. contentOffset.x = (CGRectGetWidth(frameToCenter) - boundsSize.width) * 0.5f;
  210. } else {
  211. contentOffset.x = 0;
  212. }
  213. if (CGRectGetHeight(frameToCenter) > boundsSize.height) {
  214. contentOffset.y = (CGRectGetHeight(frameToCenter) - boundsSize.height) * 0.5f;
  215. } else {
  216. contentOffset.y = 0;
  217. }
  218. [self setContentOffset:contentOffset];
  219. }
  220. #pragma mark -
  221. #pragma mark Methods called during rotation to preserve the zoomScale and the visible portion of the image
  222. #pragma mark - Rotation support
  223. - (void)prepareToResize
  224. {
  225. if (_zoomView == nil) {
  226. return;
  227. }
  228. CGPoint boundsCenter = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
  229. _pointToCenterAfterResize = [self convertPoint:boundsCenter toView:self.zoomView];
  230. _scaleToRestoreAfterResize = self.zoomScale;
  231. // If we're at the minimum zoom scale, preserve that by returning 0, which will be converted to the minimum
  232. // allowable scale when the scale is restored.
  233. if (_scaleToRestoreAfterResize <= self.minimumZoomScale + FLT_EPSILON)
  234. _scaleToRestoreAfterResize = 0;
  235. }
  236. - (void)recoverFromResizing
  237. {
  238. if (_zoomView == nil) {
  239. return;
  240. }
  241. [self setMaxMinZoomScalesForCurrentBounds];
  242. // Step 1: restore zoom scale, first making sure it is within the allowable range.
  243. CGFloat maxZoomScale = MAX(self.minimumZoomScale, _scaleToRestoreAfterResize);
  244. self.zoomScale = MIN(self.maximumZoomScale, maxZoomScale);
  245. // Step 2: restore center point, first making sure it is within the allowable range.
  246. // 2a: convert our desired center point back to our own coordinate space
  247. CGPoint boundsCenter = [self convertPoint:_pointToCenterAfterResize fromView:self.zoomView];
  248. // 2b: calculate the content offset that would yield that center point
  249. CGPoint offset = CGPointMake(boundsCenter.x - self.bounds.size.width / 2.0,
  250. boundsCenter.y - self.bounds.size.height / 2.0);
  251. // 2c: restore offset, adjusted to be within the allowable range
  252. CGPoint maxOffset = [self maximumContentOffset];
  253. CGPoint minOffset = [self minimumContentOffset];
  254. CGFloat realMaxOffset = MIN(maxOffset.x, offset.x);
  255. offset.x = MAX(minOffset.x, realMaxOffset);
  256. realMaxOffset = MIN(maxOffset.y, offset.y);
  257. offset.y = MAX(minOffset.y, realMaxOffset);
  258. self.contentOffset = offset;
  259. }
  260. - (CGPoint)maximumContentOffset
  261. {
  262. CGSize contentSize = self.contentSize;
  263. CGSize boundsSize = self.bounds.size;
  264. return CGPointMake(contentSize.width - boundsSize.width, contentSize.height - boundsSize.height);
  265. }
  266. - (CGPoint)minimumContentOffset
  267. {
  268. return CGPointZero;
  269. }
  270. @end