TOCropView.m 49 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210
  1. //
  2. // TOCropView.m
  3. //
  4. // Copyright 2015 Timothy Oliver. All rights reserved.
  5. //
  6. // Permission is hereby granted, free of charge, to any person obtaining a copy
  7. // of this software and associated documentation files (the "Software"), to
  8. // deal in the Software without restriction, including without limitation the
  9. // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
  10. // sell copies of the Software, and to permit persons to whom the Software is
  11. // furnished to do so, subject to the following conditions:
  12. //
  13. // The above copyright notice and this permission notice shall be included in
  14. // all copies or substantial portions of the Software.
  15. //
  16. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
  17. // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
  20. // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
  21. // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  22. #import "TOCropView.h"
  23. #import "TOCropOverlayView.h"
  24. #import "TOCropScrollView.h"
  25. #define TOCROPVIEW_BACKGROUND_COLOR [UIColor colorWithWhite:0.12f alpha:1.0f]
  26. static const CGFloat kTOCropViewPadding = 14.0f;
  27. static const NSTimeInterval kTOCropTimerDuration = 0.8f;
  28. static const CGFloat kTOCropViewMinimumBoxSize = 42.0f;
  29. /* When the user taps down to resize the box, this state is used
  30. to determine where they tapped and how to manipulate the box */
  31. typedef NS_ENUM(NSInteger, TOCropViewOverlayEdge) {
  32. TOCropViewOverlayEdgeNone,
  33. TOCropViewOverlayEdgeTopLeft,
  34. TOCropViewOverlayEdgeTop,
  35. TOCropViewOverlayEdgeTopRight,
  36. TOCropViewOverlayEdgeRight,
  37. TOCropViewOverlayEdgeBottomRight,
  38. TOCropViewOverlayEdgeBottom,
  39. TOCropViewOverlayEdgeBottomLeft,
  40. TOCropViewOverlayEdgeLeft
  41. };
  42. @interface TOCropView () <UIScrollViewDelegate, UIGestureRecognizerDelegate>
  43. @property (nonatomic, strong, readwrite) UIImage *image;
  44. /* Views */
  45. @property (nonatomic, strong) UIImageView *backgroundImageView; /* The main image view, placed within the scroll view */
  46. @property (nonatomic, strong) UIView *backgroundContainerView; /* A view which contains the background image view, to separate its transforms from the scroll view. */
  47. @property (nonatomic, strong) UIImageView *foregroundImageView; /* A copy of the background image view, placed over the dimming views */
  48. @property (nonatomic, strong) UIView *foregroundContainerView; /* A container view that clips the foreground image view to the crop box frame */
  49. @property (nonatomic, strong) TOCropScrollView *scrollView; /* The scroll view in charge of panning/zooming the image. */
  50. @property (nonatomic, strong) UIView *overlayView; /* A semi-transparent grey view, overlaid on top of the background image */
  51. @property (nonatomic, strong) UIView *translucencyView; /* A blur view that is made visible when the user isn't interacting with the crop view */
  52. @property (nonatomic, strong) TOCropOverlayView *gridOverlayView; /* A grid view overlaid on top of the foreground image view's container. */
  53. @property (nonatomic, strong) UIPanGestureRecognizer *gridPanGestureRecognizer; /* The gesture recognizer in charge of controlling the resizing of the crop view */
  54. /* Crop box handling */
  55. @property (nonatomic, assign) TOCropViewOverlayEdge tappedEdge; /* The edge region that the user tapped on, to resize the cropping region */
  56. @property (nonatomic, assign) CGRect cropOriginFrame; /* When resizing, this is the original frame of the crop box. */
  57. @property (nonatomic, assign) CGPoint panOriginPoint; /* The initial touch point of the pan gesture recognizer */
  58. @property (nonatomic, assign, readwrite) CGRect cropBoxFrame; /* The frame, in relation to to this view where the grid, and crop container view are aligned */
  59. @property (nonatomic, strong) NSTimer *resetTimer; /* The timer used to reset the view after the user stops interacting with it */
  60. @property (nonatomic, assign) BOOL editing; /* Used to denote the active state of the user manipulating the content */
  61. @property (nonatomic, assign, readwrite) NSInteger angle;
  62. /* Pre-screen-rotation state information */
  63. @property (nonatomic, assign) CGPoint rotationContentOffset;
  64. @property (nonatomic, assign) CGSize rotationContentSize;
  65. @property (nonatomic, readonly) CGRect contentBounds; /* Give the current screen real-estate, the frame that the scroll view is allowed to use */
  66. @property (nonatomic, readonly) CGSize imageSize; /* Given the current rotation of the image, the size of the image */
  67. /* 90-degree rotation state data */
  68. @property (nonatomic, assign) CGSize cropBoxLastEditedSize; /* When performing 90-degree rotations, remember what our last manual size was to use that as a base */
  69. @property (nonatomic, assign) NSInteger cropBoxLastEditedAngle; /* Remember which angle we were at when we saved the editing size */
  70. @property (nonatomic, assign) BOOL rotateAnimationInProgress; /* Disallow any input while the rotation animation is playing */
  71. /* Reset state data */
  72. @property (nonatomic, assign) CGSize originalCropBoxSize; /* Save the original crop box size so we can tell when the content has been edited */
  73. @property (nonatomic, assign, readwrite) BOOL canReset;
  74. - (void)setup;
  75. /* Image layout */
  76. - (void)layoutInitialImage;
  77. - (void)matchForegroundToBackground;
  78. /* Crop box handling */
  79. - (TOCropViewOverlayEdge)cropEdgeForPoint:(CGPoint)point;
  80. - (void)updateCropBoxFrameWithGesturePoint:(CGPoint)point;
  81. /* Editing state */
  82. - (void)setEditing:(BOOL)editing animated:(BOOL)animated;
  83. - (void)moveCroppedContentToCenterAnimated:(BOOL)animated;
  84. - (void)startEditing;
  85. /* Timer handling */
  86. - (void)startResetTimer;
  87. - (void)timerTriggered;
  88. - (void)cancelResetTimer;
  89. /* Gesture Recognizers */
  90. - (void)gridPanGestureRecognized:(UIPanGestureRecognizer *)recognizer;
  91. - (void)longPressGestureRecognized:(UILongPressGestureRecognizer *)recognizer;
  92. /* Reset state */
  93. - (void)checkForCanReset;
  94. @end
  95. @implementation TOCropView
  96. - (instancetype)initWithImage:(UIImage *)image
  97. {
  98. if (self = [super init]) {
  99. _image = image;
  100. [self setup];
  101. }
  102. return self;
  103. }
  104. - (void)setup
  105. {
  106. __weak typeof(self) weakSelf = self;
  107. //View properties
  108. self.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
  109. self.backgroundColor = TOCROPVIEW_BACKGROUND_COLOR;
  110. self.cropBoxFrame = CGRectZero;
  111. //Scroll View properties
  112. self.scrollView = [[TOCropScrollView alloc] initWithFrame:self.bounds];
  113. self.scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
  114. self.scrollView.alwaysBounceHorizontal = YES;
  115. self.scrollView.alwaysBounceVertical = YES;
  116. self.scrollView.showsHorizontalScrollIndicator = NO;
  117. self.scrollView.showsVerticalScrollIndicator = NO;
  118. self.scrollView.delegate = self;
  119. [self addSubview:self.scrollView];
  120. self.scrollView.touchesBegan = ^{ [weakSelf startEditing]; };
  121. self.scrollView.touchesEnded = ^{ [weakSelf startResetTimer]; };
  122. //Background Image View
  123. self.backgroundImageView = [[UIImageView alloc] initWithImage:self.image];
  124. //self.backgroundImageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
  125. //Background container view
  126. self.backgroundContainerView = [[UIView alloc] initWithFrame:self.backgroundImageView.frame];
  127. [self.backgroundContainerView addSubview:self.backgroundImageView];
  128. [self.scrollView addSubview:self.backgroundContainerView];
  129. //Grey transparent overlay view
  130. self.overlayView = [[UIView alloc] initWithFrame:self.bounds];
  131. self.overlayView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
  132. self.overlayView.backgroundColor = [self.backgroundColor colorWithAlphaComponent:0.35f];
  133. self.overlayView.hidden = NO;
  134. self.overlayView.userInteractionEnabled = NO;
  135. [self addSubview:self.overlayView];
  136. //Translucency View
  137. if (NSClassFromString(@"UIVisualEffectView")) {
  138. self.translucencyView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleDark]];
  139. self.translucencyView.frame = self.bounds;
  140. }
  141. else {
  142. UIToolbar *toolbar = [[UIToolbar alloc] init];
  143. toolbar.barStyle = UIBarStyleBlack;
  144. self.translucencyView = toolbar;
  145. self.translucencyView.frame = CGRectInset(self.bounds, -1.0f, -1.0f);
  146. }
  147. self.translucencyView.hidden = NO;
  148. self.translucencyView.userInteractionEnabled = NO;
  149. self.translucencyView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
  150. [self addSubview:self.translucencyView];
  151. self.foregroundContainerView = [[UIView alloc] initWithFrame:(CGRect){0,0,200,200}];
  152. self.foregroundContainerView.clipsToBounds = YES;
  153. self.foregroundContainerView.userInteractionEnabled = NO;
  154. [self addSubview:self.foregroundContainerView];
  155. self.gridOverlayView = [[TOCropOverlayView alloc] initWithFrame:self.foregroundContainerView.frame];
  156. self.gridOverlayView.userInteractionEnabled = NO;
  157. self.gridOverlayView.gridHidden = YES;
  158. [self addSubview:self.gridOverlayView];
  159. self.foregroundImageView = [[UIImageView alloc] initWithImage:self.image];
  160. [self.foregroundContainerView addSubview:self.foregroundImageView];
  161. self.gridPanGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(gridPanGestureRecognized:)];
  162. self.gridPanGestureRecognizer.delegate = self;
  163. [self.scrollView.panGestureRecognizer requireGestureRecognizerToFail:self.gridPanGestureRecognizer];
  164. [self addGestureRecognizer:self.gridPanGestureRecognizer];
  165. self.editing = NO;
  166. }
  167. #pragma mark - View Layout -
  168. - (void)didMoveToSuperview
  169. {
  170. [super didMoveToSuperview];
  171. [self layoutInitialImage];
  172. }
  173. - (void)layoutInitialImage
  174. {
  175. CGSize imageSize = self.imageSize;
  176. self.scrollView.contentSize = imageSize;
  177. CGRect bounds = self.contentBounds;
  178. //work out the max and min scale of the image
  179. CGFloat scale = MIN(CGRectGetWidth(bounds)/imageSize.width, CGRectGetHeight(bounds)/imageSize.height);
  180. CGSize scaledSize = (CGSize){floorf(imageSize.width * scale), floorf(imageSize.height * scale)};
  181. self.scrollView.minimumZoomScale = scale;
  182. self.scrollView.maximumZoomScale = 15.0f;
  183. //set the fully zoomed out state initially
  184. self.scrollView.zoomScale = self.scrollView.minimumZoomScale;
  185. self.scrollView.contentSize = scaledSize;
  186. //Relayout the image in the scroll view
  187. CGRect frame = CGRectZero;
  188. frame.size = scaledSize;
  189. frame.origin.x = bounds.origin.x + floorf((CGRectGetWidth(bounds) - frame.size.width) * 0.5f);
  190. frame.origin.y = bounds.origin.y + floorf((CGRectGetHeight(bounds) - frame.size.height) * 0.5f);
  191. self.cropBoxFrame = frame;
  192. //save the current state for use with 90-degree rotations
  193. self.cropBoxLastEditedSize = self.cropBoxFrame.size;
  194. self.cropBoxLastEditedAngle = 0;
  195. //save the size for checking if we're in a resettable state
  196. self.originalCropBoxSize = self.cropBoxFrame.size;
  197. [self matchForegroundToBackground];
  198. }
  199. - (void)prepareforRotation
  200. {
  201. self.rotationContentOffset = self.scrollView.contentOffset;
  202. self.rotationContentSize = self.scrollView.contentSize;
  203. }
  204. - (void)performRelayoutForRotation
  205. {
  206. CGRect cropFrame = self.cropBoxFrame;
  207. CGRect contentFrame = self.contentBounds;
  208. //Work out the portion of the image we were focused on
  209. CGPoint cropMidPoint = (CGPoint){CGRectGetMidX(cropFrame), CGRectGetMidY(cropFrame)};
  210. CGPoint cropTargetPoint = (CGPoint){cropMidPoint.x + self.rotationContentOffset.x, cropMidPoint.y + self.rotationContentOffset.y};
  211. CGFloat scale = MIN(contentFrame.size.width / cropFrame.size.width, contentFrame.size.height / cropFrame.size.height);
  212. self.scrollView.minimumZoomScale *= scale;
  213. self.scrollView.zoomScale *= scale;
  214. //Work out the centered, upscaled version of the crop rectangle
  215. cropFrame.size.width = floorf(cropFrame.size.width * scale);
  216. cropFrame.size.height = floorf(cropFrame.size.height * scale);
  217. cropFrame.origin.x = floorf(contentFrame.origin.x + ((contentFrame.size.width - cropFrame.size.width) * 0.5f));
  218. cropFrame.origin.y = floorf(contentFrame.origin.y + ((contentFrame.size.height - cropFrame.size.height) * 0.5f));
  219. self.cropBoxFrame = cropFrame;
  220. self.cropBoxLastEditedSize = self.cropBoxFrame.size;
  221. //work out how to line up out point of interest into the middle of the crop box
  222. cropTargetPoint.x *= scale;
  223. cropTargetPoint.y *= scale;
  224. CGPoint midPoint = {floorf(CGRectGetMidX(cropFrame)), floorf(CGRectGetMidY(cropFrame))};
  225. CGPoint offset = CGPointZero;
  226. offset.x = floorf(-midPoint.x + cropTargetPoint.x);
  227. offset.y = floorf(-midPoint.y + cropTargetPoint.y);
  228. offset.x = MAX(-self.scrollView.contentInset.left, offset.x);
  229. offset.y = MAX(-self.scrollView.contentInset.top, offset.y);
  230. self.scrollView.contentOffset = offset;
  231. [self matchForegroundToBackground];
  232. }
  233. - (void)matchForegroundToBackground
  234. {
  235. //We can't simply match the frames since if the images are rotated, the frame property becomes unusable
  236. self.foregroundImageView.frame = [self.backgroundContainerView.superview convertRect:self.backgroundContainerView.frame toView:self.foregroundContainerView];
  237. }
  238. - (void)updateCropBoxFrameWithGesturePoint:(CGPoint)point
  239. {
  240. CGRect frame = self.cropBoxFrame;
  241. CGRect originFrame = self.cropOriginFrame;
  242. CGRect contentFrame = self.contentBounds;
  243. point.x = MAX(contentFrame.origin.x, point.x);
  244. point.y = MAX(contentFrame.origin.y, point.y);
  245. //The delta between where we first tapped, and where our finger is now
  246. CGFloat xDelta = ceilf(point.x - self.panOriginPoint.x);
  247. CGFloat yDelta = ceilf(point.y - self.panOriginPoint.y);
  248. //Current aspect ratio of the crop box in case we need to clamp it
  249. CGFloat aspectRatio = (originFrame.size.width / originFrame.size.height);
  250. BOOL aspectHorizontal = NO, aspectVertical = NO;
  251. switch (self.tappedEdge) {
  252. case TOCropViewOverlayEdgeLeft:
  253. if (self.aspectLockEnabled) {
  254. aspectHorizontal = YES;
  255. xDelta = MAX(xDelta, 0);
  256. CGPoint scaleOrigin = (CGPoint){CGRectGetMaxX(originFrame), CGRectGetMidY(originFrame)};
  257. frame.size.height = frame.size.width / aspectRatio;
  258. frame.origin.y = scaleOrigin.y - (frame.size.height * 0.5f);
  259. }
  260. frame.origin.x = originFrame.origin.x + xDelta;
  261. frame.size.width = originFrame.size.width - xDelta;
  262. break;
  263. case TOCropViewOverlayEdgeRight:
  264. if (self.aspectLockEnabled) {
  265. aspectHorizontal = YES;
  266. CGPoint scaleOrigin = (CGPoint){CGRectGetMinX(originFrame), CGRectGetMidY(originFrame)};
  267. frame.size.height = frame.size.width / aspectRatio;
  268. frame.origin.y = scaleOrigin.y - (frame.size.height * 0.5f);
  269. frame.size.width = originFrame.size.width + xDelta;
  270. frame.size.width = MIN(frame.size.width, contentFrame.size.height * aspectRatio);
  271. }
  272. else {
  273. frame.size.width = originFrame.size.width + xDelta;
  274. }
  275. break;
  276. case TOCropViewOverlayEdgeBottom:
  277. if (self.aspectLockEnabled) {
  278. aspectVertical = YES;
  279. CGPoint scaleOrigin = (CGPoint){CGRectGetMidX(originFrame), CGRectGetMinY(originFrame)};
  280. frame.size.width = frame.size.height * aspectRatio;
  281. frame.origin.x = scaleOrigin.x - (frame.size.width * 0.5f);
  282. frame.size.height = originFrame.size.height + yDelta;
  283. frame.size.height = MIN(frame.size.height, contentFrame.size.width / aspectRatio);
  284. }
  285. else {
  286. frame.size.height = originFrame.size.height + yDelta;
  287. }
  288. break;
  289. case TOCropViewOverlayEdgeTop:
  290. if (self.aspectLockEnabled) {
  291. aspectVertical = YES;
  292. yDelta = MAX(0,yDelta);
  293. CGPoint scaleOrigin = (CGPoint){CGRectGetMidX(originFrame), CGRectGetMaxY(originFrame)};
  294. frame.size.width = frame.size.height * aspectRatio;
  295. frame.origin.x = scaleOrigin.x - (frame.size.width * 0.5f);
  296. frame.origin.y = originFrame.origin.y + yDelta;
  297. frame.size.height = originFrame.size.height - yDelta;
  298. }
  299. else {
  300. frame.origin.y = originFrame.origin.y + yDelta;
  301. frame.size.height = originFrame.size.height - yDelta;
  302. }
  303. break;
  304. case TOCropViewOverlayEdgeTopLeft:
  305. if (self.aspectLockEnabled) {
  306. xDelta = MAX(xDelta, 0);
  307. yDelta = MAX(yDelta, 0);
  308. CGPoint distance;
  309. distance.x = 1.0f - (xDelta / CGRectGetWidth(originFrame));
  310. distance.y = 1.0f - (yDelta / CGRectGetHeight(originFrame));
  311. CGFloat scale = (distance.x + distance.y) * 0.5f;
  312. frame.size.width = ceilf(CGRectGetWidth(originFrame) * scale);
  313. frame.size.height = ceilf(CGRectGetHeight(originFrame) * scale);
  314. frame.origin.x = originFrame.origin.x + (CGRectGetWidth(originFrame) - frame.size.width);
  315. frame.origin.y = originFrame.origin.y + (CGRectGetHeight(originFrame) - frame.size.height);
  316. aspectVertical = YES;
  317. aspectHorizontal = YES;
  318. }
  319. else {
  320. frame.origin.x = originFrame.origin.x + xDelta;
  321. frame.size.width = originFrame.size.width - xDelta;
  322. frame.origin.y = originFrame.origin.y + yDelta;
  323. frame.size.height = originFrame.size.height - yDelta;
  324. }
  325. break;
  326. case TOCropViewOverlayEdgeTopRight:
  327. if (self.aspectLockEnabled) {
  328. xDelta = MAX(xDelta, 0);
  329. yDelta = MAX(yDelta, 0);
  330. CGPoint distance;
  331. distance.x = 1.0f - ((-xDelta) / CGRectGetWidth(originFrame));
  332. distance.y = 1.0f - ((yDelta) / CGRectGetHeight(originFrame));
  333. CGFloat scale = (distance.x + distance.y) * 0.5f;
  334. scale = MIN(1.0f, scale);
  335. frame.size.width = ceilf(CGRectGetWidth(originFrame) * scale);
  336. frame.size.height = ceilf(CGRectGetHeight(originFrame) * scale);
  337. frame.origin.y = CGRectGetMaxY(originFrame) - frame.size.height;
  338. aspectVertical = YES;
  339. aspectHorizontal = YES;
  340. }
  341. else {
  342. frame.size.width = originFrame.size.width + xDelta;
  343. frame.origin.y = originFrame.origin.y + yDelta;
  344. frame.size.height = originFrame.size.height - yDelta;
  345. }
  346. break;
  347. case TOCropViewOverlayEdgeBottomLeft:
  348. if (self.aspectLockEnabled) {
  349. CGPoint distance;
  350. distance.x = 1.0f - (xDelta / CGRectGetWidth(originFrame));
  351. distance.y = 1.0f - (-yDelta / CGRectGetHeight(originFrame));
  352. CGFloat scale = (distance.x + distance.y) * 0.5f;
  353. frame.size.width = ceilf(CGRectGetWidth(originFrame) * scale);
  354. frame.size.height = ceilf(CGRectGetHeight(originFrame) * scale);
  355. frame.origin.x = CGRectGetMaxX(originFrame) - frame.size.width;
  356. aspectVertical = YES;
  357. aspectHorizontal = YES;
  358. }
  359. else {
  360. frame.size.height = originFrame.size.height + yDelta;
  361. frame.origin.x = originFrame.origin.x + xDelta;
  362. frame.size.width = originFrame.size.width - xDelta;
  363. }
  364. break;
  365. case TOCropViewOverlayEdgeBottomRight:
  366. if (self.aspectLockEnabled) {
  367. CGPoint distance;
  368. distance.x = 1.0f - ((-1 * xDelta) / CGRectGetWidth(originFrame));
  369. distance.y = 1.0f - ((-1 * yDelta) / CGRectGetHeight(originFrame));
  370. CGFloat scale = (distance.x + distance.y) * 0.5f;
  371. frame.size.width = ceilf(CGRectGetWidth(originFrame) * scale);
  372. frame.size.height = ceilf(CGRectGetHeight(originFrame) * scale);
  373. aspectVertical = YES;
  374. aspectHorizontal = YES;
  375. }
  376. else {
  377. frame.size.height = originFrame.size.height + yDelta;
  378. frame.size.width = originFrame.size.width + xDelta;
  379. }
  380. break;
  381. case TOCropViewOverlayEdgeNone: break;
  382. }
  383. //Work out the limits the box may be scaled before it starts to overlap itself
  384. CGSize minSize = CGSizeZero;
  385. minSize.width = kTOCropViewMinimumBoxSize;
  386. minSize.height = kTOCropViewMinimumBoxSize;
  387. CGSize maxSize = CGSizeZero;
  388. maxSize.width = CGRectGetWidth(contentFrame);
  389. maxSize.height = CGRectGetHeight(contentFrame);
  390. //clamp the box to ensure it doesn't go beyond the bounds we've set
  391. if (self.aspectLockEnabled && aspectHorizontal) {
  392. maxSize.height = contentFrame.size.width / aspectRatio;
  393. minSize.width = kTOCropViewMinimumBoxSize * aspectRatio;
  394. }
  395. if (self.aspectLockEnabled && aspectVertical) {
  396. maxSize.width = contentFrame.size.height * aspectRatio;
  397. minSize.height = kTOCropViewMinimumBoxSize / aspectRatio;
  398. }
  399. //Clamp the minimum size
  400. frame.size.width = MAX(frame.size.width, minSize.width);
  401. frame.size.height = MAX(frame.size.height, minSize.height);
  402. //Clamp the maximum size
  403. frame.size.width = MIN(frame.size.width, maxSize.width);
  404. frame.size.height = MIN(frame.size.height, maxSize.height);
  405. frame.origin.x = MAX(frame.origin.x, CGRectGetMinX(contentFrame));
  406. frame.origin.x = MIN(frame.origin.x, CGRectGetMaxX(contentFrame) - minSize.width);
  407. frame.origin.y = MAX(frame.origin.y, CGRectGetMinY(contentFrame));
  408. frame.origin.y = MIN(frame.origin.y, CGRectGetMaxY(contentFrame) - minSize.height);
  409. self.cropBoxFrame = frame;
  410. [self checkForCanReset];
  411. }
  412. - (void)resetLayoutToDefaultAnimated:(BOOL)animated
  413. {
  414. if (animated == NO || self.angle < 0) {
  415. self.angle = 0;
  416. self.foregroundImageView.transform = CGAffineTransformIdentity;
  417. self.backgroundImageView.transform = CGAffineTransformIdentity;
  418. self.scrollView.zoomScale = 1.0f;
  419. self.backgroundContainerView.frame = (CGRect){CGPointZero, self.backgroundImageView.frame.size};
  420. self.backgroundImageView.frame = self.backgroundContainerView.frame;
  421. self.foregroundImageView.frame = self.backgroundContainerView.frame;
  422. [self layoutInitialImage];
  423. [self checkForCanReset];
  424. return;
  425. }
  426. [UIView animateWithDuration:0.5f delay:0.0f usingSpringWithDamping:1.0f initialSpringVelocity:0.7f options:0 animations:^{
  427. [self layoutInitialImage];
  428. [self checkForCanReset];
  429. } completion:nil];
  430. }
  431. #pragma mark - Gesture Recognizer -
  432. - (void)gridPanGestureRecognized:(UIPanGestureRecognizer *)recognizer
  433. {
  434. CGPoint point = [recognizer locationInView:self];
  435. if (recognizer.state == UIGestureRecognizerStateBegan) {
  436. [self startEditing];
  437. self.panOriginPoint = point;
  438. self.cropOriginFrame = self.cropBoxFrame;
  439. self.tappedEdge = [self cropEdgeForPoint:self.panOriginPoint];
  440. }
  441. if (recognizer.state == UIGestureRecognizerStateEnded)
  442. [self startResetTimer];
  443. [self updateCropBoxFrameWithGesturePoint:point];
  444. }
  445. - (void)longPressGestureRecognized:(UILongPressGestureRecognizer *)recognizer
  446. {
  447. if (recognizer.state == UIGestureRecognizerStateBegan)
  448. [self.gridOverlayView setGridHidden:NO animated:YES];
  449. if (recognizer.state == UIGestureRecognizerStateEnded)
  450. [self.gridOverlayView setGridHidden:YES animated:YES];
  451. }
  452. - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
  453. {
  454. if (gestureRecognizer != self.gridPanGestureRecognizer)
  455. return YES;
  456. CGPoint tapPoint = [gestureRecognizer locationInView:self];
  457. CGRect frame = self.gridOverlayView.frame;
  458. CGRect innerFrame = CGRectInset(frame, 22.0f, 22.0f);
  459. CGRect outerFrame = CGRectInset(frame, -22.0f, -22.0f);
  460. if (CGRectContainsPoint(innerFrame, tapPoint) || !CGRectContainsPoint(outerFrame, tapPoint))
  461. return NO;
  462. return YES;
  463. }
  464. #pragma mark - Timer -
  465. - (void)startResetTimer
  466. {
  467. if (self.resetTimer)
  468. return;
  469. self.resetTimer = [NSTimer scheduledTimerWithTimeInterval:kTOCropTimerDuration target:self selector:@selector(timerTriggered) userInfo:nil repeats:NO];
  470. }
  471. - (void)timerTriggered
  472. {
  473. [self setEditing:NO animated:YES];
  474. [self.resetTimer invalidate];
  475. self.resetTimer = nil;
  476. }
  477. - (void)cancelResetTimer
  478. {
  479. [self.resetTimer invalidate];
  480. self.resetTimer = nil;
  481. }
  482. - (TOCropViewOverlayEdge)cropEdgeForPoint:(CGPoint)point
  483. {
  484. CGRect frame = self.cropBoxFrame;
  485. //account for padding around the box
  486. frame = CGRectInset(frame, -22.0f, -22.0f);
  487. //Make sure the corners take priority
  488. CGRect topLeftRect = (CGRect){frame.origin, {44,44}};
  489. if (CGRectContainsPoint(topLeftRect, point))
  490. return TOCropViewOverlayEdgeTopLeft;
  491. CGRect topRightRect = topLeftRect;
  492. topRightRect.origin.x = CGRectGetMaxX(frame) - 44.0f;
  493. if (CGRectContainsPoint(topRightRect, point))
  494. return TOCropViewOverlayEdgeTopRight;
  495. CGRect bottomLeftRect = topLeftRect;
  496. bottomLeftRect.origin.y = CGRectGetMaxY(frame) - 44.0f;
  497. if (CGRectContainsPoint(bottomLeftRect, point))
  498. return TOCropViewOverlayEdgeBottomLeft;
  499. CGRect bottomRightRect = topRightRect;
  500. bottomRightRect.origin.y = bottomLeftRect.origin.y;
  501. if (CGRectContainsPoint(bottomRightRect, point))
  502. return TOCropViewOverlayEdgeBottomRight;
  503. //Check for edges
  504. CGRect topRect = (CGRect){frame.origin, {CGRectGetWidth(frame), 44.0f}};
  505. if (CGRectContainsPoint(topRect, point))
  506. return TOCropViewOverlayEdgeTop;
  507. CGRect bottomRect = topRect;
  508. bottomRect.origin.y = CGRectGetMaxY(frame) - 44.0f;
  509. if (CGRectContainsPoint(bottomRect, point))
  510. return TOCropViewOverlayEdgeBottom;
  511. CGRect leftRect = (CGRect){frame.origin, {44.0f, CGRectGetHeight(frame)}};
  512. if (CGRectContainsPoint(leftRect, point))
  513. return TOCropViewOverlayEdgeLeft;
  514. CGRect rightRect = leftRect;
  515. rightRect.origin.x = CGRectGetMaxX(frame) - 44.0f;
  516. if (CGRectContainsPoint(rightRect, point))
  517. return TOCropViewOverlayEdgeRight;
  518. return TOCropViewOverlayEdgeNone;
  519. }
  520. #pragma mark - Scroll View Delegate -
  521. - (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView { return self.backgroundContainerView; }
  522. - (void)scrollViewDidScroll:(UIScrollView *)scrollView { [self matchForegroundToBackground]; }
  523. - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { [self startEditing]; }
  524. - (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view { [self startEditing]; }
  525. - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { [self startResetTimer]; }
  526. - (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale { [self startResetTimer]; }
  527. - (void)scrollViewDidZoom:(UIScrollView *)scrollView
  528. {
  529. [self checkForCanReset];
  530. [self matchForegroundToBackground];
  531. }
  532. - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
  533. {
  534. if (!decelerate)
  535. [self startResetTimer];
  536. }
  537. #pragma mark - Accessors -
  538. - (void)setCropBoxFrame:(CGRect)cropBoxFrame
  539. {
  540. if (CGRectEqualToRect(cropBoxFrame, _cropBoxFrame))
  541. return;
  542. //Upon init, sometimes the box size is still 0, which can result in CALayer issues
  543. if (cropBoxFrame.size.width < FLT_EPSILON || cropBoxFrame.size.height < FLT_EPSILON)
  544. return;
  545. //clamp the cropping region to the inset boundaries of the screen
  546. CGRect contentFrame = self.contentBounds;
  547. CGFloat xOrigin = ceilf(contentFrame.origin.x);
  548. CGFloat xDelta = cropBoxFrame.origin.x - xOrigin;
  549. cropBoxFrame.origin.x = floorf(MAX(cropBoxFrame.origin.x, xOrigin));
  550. if (xDelta < -FLT_EPSILON) //If we clamp the x value, ensure we compensate for the subsequent delta generated in the width (Or else, the box will keep growing)
  551. cropBoxFrame.size.width += xDelta;
  552. CGFloat yOrigin = ceilf(contentFrame.origin.y);
  553. CGFloat yDelta = cropBoxFrame.origin.y - yOrigin;
  554. cropBoxFrame.origin.y = floorf(MAX(cropBoxFrame.origin.y, yOrigin));
  555. if (yDelta < -FLT_EPSILON)
  556. cropBoxFrame.size.height += yDelta;
  557. //given the clamped X/Y values, make sure we can't extend the crop box beyond the edge of the screen in the current state
  558. CGFloat maxWidth = (contentFrame.size.width + contentFrame.origin.x) - cropBoxFrame.origin.x;
  559. cropBoxFrame.size.width = floorf(MIN(cropBoxFrame.size.width, maxWidth));
  560. CGFloat maxHeight = (contentFrame.size.height + contentFrame.origin.y) - cropBoxFrame.origin.y;
  561. cropBoxFrame.size.height = floorf(MIN(cropBoxFrame.size.height, maxHeight));
  562. //Make sure we can't make the crop box too small
  563. cropBoxFrame.size.width = MAX(cropBoxFrame.size.width, kTOCropViewMinimumBoxSize);
  564. cropBoxFrame.size.height = MAX(cropBoxFrame.size.height, kTOCropViewMinimumBoxSize);
  565. _cropBoxFrame = cropBoxFrame;
  566. self.foregroundContainerView.frame = _cropBoxFrame; //set the clipping view to match the new rect
  567. self.gridOverlayView.frame = _cropBoxFrame; //set the new overlay view to match the same region
  568. //reset the scroll view insets to match the region of the new crop rect
  569. self.scrollView.contentInset = (UIEdgeInsets){CGRectGetMinY(_cropBoxFrame),
  570. CGRectGetMinX(_cropBoxFrame),
  571. CGRectGetMaxY(self.bounds) - CGRectGetMaxY(_cropBoxFrame),
  572. CGRectGetMaxX(self.bounds) - CGRectGetMaxX(_cropBoxFrame)};
  573. //if necessary, work out the new minimum size of the scroll view so it fills the crop box
  574. CGSize imageSize = self.backgroundContainerView.bounds.size;
  575. CGFloat scale = MAX(cropBoxFrame.size.height/imageSize.height, cropBoxFrame.size.width/imageSize.width);
  576. self.scrollView.minimumZoomScale = scale;
  577. //make sure content isn't smaller than the crop box
  578. CGSize size = self.scrollView.contentSize;
  579. size.width = floorf(size.width);
  580. size.height = floorf(size.height);
  581. //self.backgroundContainerView.frame = (CGRect){CGPointZero, size};
  582. self.scrollView.contentSize = size;\
  583. //IMPORTANT: Force the scroll view to update its content after changing the zoom scale
  584. self.scrollView.zoomScale = self.scrollView.zoomScale;
  585. [self matchForegroundToBackground]; //re-align the background content to match
  586. }
  587. - (void)setEditing:(BOOL)editing
  588. {
  589. [self setEditing:editing animated:NO];
  590. }
  591. - (void)setSimpleMode:(BOOL)simpleMode
  592. {
  593. [self setSimpleMode:simpleMode animated:NO];
  594. }
  595. - (BOOL)cropBoxAspectRatioIsPortrait
  596. {
  597. CGRect cropFrame = self.cropBoxFrame;
  598. return CGRectGetWidth(cropFrame) < CGRectGetHeight(cropFrame);
  599. }
  600. - (CGRect)croppedImageFrame
  601. {
  602. CGSize imageSize = self.imageSize;
  603. CGSize contentSize = self.scrollView.contentSize;
  604. CGRect cropBoxFrame = self.cropBoxFrame;
  605. CGPoint contentOffset = self.scrollView.contentOffset;
  606. UIEdgeInsets edgeInsets = self.scrollView.contentInset;
  607. CGRect frame = CGRectZero;
  608. frame.origin.x = floorf((contentOffset.x + edgeInsets.left) * (imageSize.width / contentSize.width));
  609. frame.origin.x = MAX(0, frame.origin.x);
  610. frame.origin.y = floorf((contentOffset.y + edgeInsets.top) * (imageSize.height / contentSize.height));
  611. frame.origin.y = MAX(0, frame.origin.y);
  612. frame.size.width = ceilf(cropBoxFrame.size.width * (imageSize.width / contentSize.width));
  613. frame.size.width = MIN(imageSize.width, frame.size.width);
  614. frame.size.height = ceilf(cropBoxFrame.size.height * (imageSize.height / contentSize.height));
  615. frame.size.height = MIN(imageSize.height, frame.size.height);
  616. return frame;
  617. }
  618. - (void)setCroppingViewsHidden:(BOOL)hidden
  619. {
  620. [self setCroppingViewsHidden:hidden animated:NO];
  621. }
  622. - (void)setCroppingViewsHidden:(BOOL)hidden animated:(BOOL)animated
  623. {
  624. if (_croppingViewsHidden == hidden)
  625. return;
  626. _croppingViewsHidden = hidden;
  627. CGFloat alpha = hidden ? 0.0f : 1.0f;
  628. if (animated == NO) {
  629. self.backgroundImageView.alpha = alpha;
  630. self.translucencyView.alpha = alpha;
  631. self.foregroundContainerView.alpha = alpha;
  632. self.gridOverlayView.alpha = alpha;
  633. return;
  634. }
  635. self.foregroundContainerView.alpha = alpha;
  636. self.backgroundImageView.alpha = alpha;
  637. [UIView animateWithDuration:0.5f animations:^{
  638. self.translucencyView.alpha = alpha;
  639. self.gridOverlayView.alpha = alpha;
  640. }];
  641. }
  642. - (void)setGridOverlayHidden:(BOOL)gridOverlayHidden
  643. {
  644. [self setGridOverlayHidden:_gridOverlayHidden animated:NO];
  645. }
  646. - (void)setGridOverlayHidden:(BOOL)gridOverlayHidden animated:(BOOL)animated
  647. {
  648. _gridOverlayHidden = gridOverlayHidden;
  649. self.gridOverlayView.alpha = gridOverlayHidden ? 1.0f : 0.0f;
  650. [UIView animateWithDuration:0.4f animations:^{
  651. self.gridOverlayView.alpha = gridOverlayHidden ? 0.0f : 1.0f;
  652. }];
  653. }
  654. - (CGRect)imageViewFrame
  655. {
  656. CGRect frame = CGRectZero;
  657. frame.origin.x = -self.scrollView.contentOffset.x;
  658. frame.origin.y = -self.scrollView.contentOffset.y;
  659. frame.size = self.scrollView.contentSize;
  660. return frame;
  661. }
  662. #pragma mark - Editing Mode -
  663. - (void)startEditing
  664. {
  665. [self cancelResetTimer];
  666. [self setEditing:YES animated:YES];
  667. }
  668. - (void)setEditing:(BOOL)editing animated:(BOOL)animated
  669. {
  670. if (editing == _editing)
  671. return;
  672. _editing = editing;
  673. [self.gridOverlayView setGridHidden:!editing animated:animated];
  674. if (editing == NO) {
  675. [self moveCroppedContentToCenterAnimated:animated];
  676. self.cropBoxLastEditedSize = self.cropBoxFrame.size;
  677. self.cropBoxLastEditedAngle = self.angle;
  678. }
  679. if (animated == NO) {
  680. self.translucencyView.alpha = editing ? 0.0f : 1.0f;
  681. return;
  682. }
  683. [UIView animateWithDuration:editing?0.2f:0.35f animations:^{
  684. self.translucencyView.alpha = editing ? 0.0f : 1.0f;
  685. }];
  686. }
  687. - (void)moveCroppedContentToCenterAnimated:(BOOL)animated
  688. {
  689. if (self.simpleMode)
  690. return;
  691. CGRect contentRect = self.contentBounds;
  692. CGRect cropFrame = self.cropBoxFrame;
  693. CGPoint focusPoint = (CGPoint){CGRectGetMidX(cropFrame), CGRectGetMidY(cropFrame)};
  694. CGPoint midPoint = (CGPoint){CGRectGetMidX(contentRect), CGRectGetMidY(contentRect)};
  695. //The scale we need to scale up the crop box to fit full screen
  696. CGFloat scale = MIN(CGRectGetWidth(contentRect)/CGRectGetWidth(cropFrame), CGRectGetHeight(contentRect)/CGRectGetHeight(cropFrame));
  697. cropFrame.size.width = floorf(cropFrame.size.width * scale);
  698. cropFrame.size.height = floorf(cropFrame.size.height * scale);
  699. cropFrame.origin.x = contentRect.origin.x + floorf((contentRect.size.width - cropFrame.size.width) * 0.5f);
  700. cropFrame.origin.y = contentRect.origin.y + floorf((contentRect.size.height - cropFrame.size.height) * 0.5f);
  701. //Work out the point on the scroll content that the focusPoint is aiming at
  702. CGPoint contentTargetPoint = CGPointZero;
  703. contentTargetPoint.x = ((focusPoint.x + self.scrollView.contentOffset.x) * scale);
  704. contentTargetPoint.y = ((focusPoint.y + self.scrollView.contentOffset.y) * scale);
  705. //Work out where the crop box is focusing, so we can re-align to center that point
  706. __block CGPoint offset = CGPointZero;
  707. offset.x = -midPoint.x + contentTargetPoint.x;
  708. offset.y = -midPoint.y + contentTargetPoint.y;
  709. //clamp the content so it doesn't create any seams around the grid
  710. offset.x = MAX(-cropFrame.origin.x, offset.x);
  711. offset.y = MAX(-cropFrame.origin.y, offset.y);
  712. __weak typeof(self) weakSelf = self;
  713. void (^translateBlock)() = ^{
  714. typeof(self) strongSelf = weakSelf;
  715. strongSelf.scrollView.zoomScale *= scale;
  716. offset.x = MIN(-CGRectGetMaxX(cropFrame)+strongSelf.scrollView.contentSize.width, offset.x);
  717. offset.y = MIN(-CGRectGetMaxY(cropFrame)+strongSelf.scrollView.contentSize.height, offset.y);
  718. strongSelf.scrollView.contentOffset = offset;
  719. strongSelf.cropBoxFrame = cropFrame;
  720. };
  721. if (!animated) {
  722. translateBlock();
  723. return;
  724. }
  725. [UIView animateWithDuration:0.5f delay:0.0f usingSpringWithDamping:1.0f initialSpringVelocity:1.0f options:0 animations:translateBlock completion:nil];
  726. }
  727. - (void)setSimpleMode:(BOOL)simpleMode animated:(BOOL)animated
  728. {
  729. if (simpleMode == _simpleMode)
  730. return;
  731. _simpleMode = simpleMode;
  732. self.editing = NO;
  733. if (animated == NO) {
  734. self.translucencyView.alpha = simpleMode ? 0.0f : 1.0f;
  735. return;
  736. }
  737. [UIView animateWithDuration:0.35f animations:^{
  738. self.translucencyView.alpha = simpleMode ? 0.0f : 1.0f;
  739. }];
  740. }
  741. - (void)setAspectLockEnabledWithAspectRatio:(CGSize)aspectRatio animated:(BOOL)animated
  742. {
  743. if (aspectRatio.width < FLT_EPSILON && aspectRatio.height < FLT_EPSILON)
  744. aspectRatio = (CGSize){self.imageSize.width, self.imageSize.height};
  745. CGRect boundsFrame = self.contentBounds;
  746. CGRect cropBoxFrame = self.cropBoxFrame;
  747. CGPoint offset = self.scrollView.contentOffset;
  748. BOOL cropBoxIsPortrait = NO;
  749. if ((NSInteger)aspectRatio.width == 1 && (NSInteger)aspectRatio.height == 1)
  750. cropBoxIsPortrait = self.image.size.width > self.image.size.height;
  751. else
  752. cropBoxIsPortrait = aspectRatio.width < aspectRatio.height;
  753. BOOL zoomOut = NO;
  754. if (cropBoxIsPortrait) {
  755. CGFloat newWidth = cropBoxFrame.size.height * (aspectRatio.width/aspectRatio.height);
  756. CGFloat delta = cropBoxFrame.size.width - newWidth;
  757. cropBoxFrame.size.width = newWidth;
  758. offset.x += (delta * 0.5f);
  759. CGFloat boundsWidth = CGRectGetWidth(boundsFrame);
  760. if (newWidth > boundsWidth) {
  761. CGFloat scale = boundsWidth / newWidth;
  762. cropBoxFrame.size.height *= scale;
  763. cropBoxFrame.size.width = boundsWidth;
  764. zoomOut = YES;
  765. }
  766. }
  767. else {
  768. CGFloat newHeight = cropBoxFrame.size.width * (aspectRatio.height/aspectRatio.width);
  769. CGFloat delta = cropBoxFrame.size.height - newHeight;
  770. cropBoxFrame.size.height = newHeight;
  771. offset.y += (delta * 0.5f);
  772. CGFloat boundsHeight = CGRectGetHeight(boundsFrame);
  773. if (newHeight > boundsHeight) {
  774. CGFloat scale = boundsHeight / newHeight;
  775. cropBoxFrame.size.width *= scale;
  776. cropBoxFrame.size.height = boundsHeight;
  777. zoomOut = YES;
  778. }
  779. }
  780. self.aspectLockEnabled = YES;
  781. if (animated == NO) {
  782. self.scrollView.contentOffset = offset;
  783. self.cropBoxFrame = cropBoxFrame;
  784. if (zoomOut)
  785. self.scrollView.zoomScale = self.scrollView.minimumZoomScale;
  786. [self moveCroppedContentToCenterAnimated:NO];
  787. [self checkForCanReset];
  788. return;
  789. }
  790. [UIView animateWithDuration:0.5f delay:0.0 usingSpringWithDamping:1.0f initialSpringVelocity:0.7f options:0 animations:^{
  791. self.scrollView.contentOffset = offset;
  792. self.cropBoxFrame = cropBoxFrame;
  793. [self checkForCanReset];
  794. if (zoomOut)
  795. self.scrollView.zoomScale = self.scrollView.minimumZoomScale;
  796. [self moveCroppedContentToCenterAnimated:NO];
  797. } completion:nil];
  798. }
  799. - (void)rotateImageNinetyDegreesAnimated:(BOOL)animated
  800. {
  801. //Only allow one rotation animation at a time
  802. if (self.rotateAnimationInProgress)
  803. return;
  804. //Cancel any pending resizing timers
  805. if (self.resetTimer) {
  806. [self cancelResetTimer];
  807. [self.gridOverlayView setGridHidden:YES];
  808. [self moveCroppedContentToCenterAnimated:NO];
  809. self.cropBoxLastEditedAngle = self.angle;
  810. self.cropBoxLastEditedSize = self.cropBoxFrame.size;
  811. }
  812. //Work out the new angle, and wrap around once we exceed 360s
  813. NSInteger newAngle = self.angle;
  814. newAngle -= 90;
  815. if (newAngle <= -360)
  816. newAngle = 0;
  817. self.angle = newAngle;
  818. //Convert the new angle to radians
  819. CGFloat angleInRadians = 0.0f;
  820. switch (newAngle) {
  821. case -90:
  822. angleInRadians = M_PI_2;
  823. break;
  824. case -180:
  825. angleInRadians = M_PI;
  826. break;
  827. case -270:
  828. angleInRadians = (M_PI + M_PI_2);
  829. break;
  830. default:
  831. angleInRadians = (M_PI * 2);
  832. break;
  833. }
  834. // Set up the transformation matrix for the rotation
  835. CGAffineTransform rotation = CGAffineTransformRotate(CGAffineTransformIdentity, -angleInRadians);
  836. //Work out how much we'll need to scale everything to fit to the new rotation
  837. CGRect contentBounds = self.contentBounds;
  838. CGRect cropBoxFrame = self.cropBoxFrame;
  839. CGFloat scale = MIN(contentBounds.size.width / cropBoxFrame.size.height, contentBounds.size.height / cropBoxFrame.size.width);
  840. //Work out which section of the image we're currently focusing at
  841. CGPoint cropMidPoint = (CGPoint){CGRectGetMidX(cropBoxFrame), CGRectGetMidY(cropBoxFrame)};
  842. CGPoint cropTargetPoint = (CGPoint){cropMidPoint.x + self.scrollView.contentOffset.x, cropMidPoint.y + self.scrollView.contentOffset.y};
  843. //Work out the dimensions of the crop box when rotated
  844. CGRect newCropFrame = CGRectZero;
  845. if (self.angle == self.cropBoxLastEditedAngle || self.angle == ((self.cropBoxLastEditedAngle - 180) % 360)) {
  846. newCropFrame.size = self.cropBoxLastEditedSize;
  847. }
  848. else {
  849. newCropFrame.size = (CGSize){floorf(self.cropBoxLastEditedSize.height * scale), floorf(self.cropBoxLastEditedSize.width * scale)};
  850. //update last edited size
  851. self.cropBoxLastEditedSize = cropBoxFrame.size;
  852. }
  853. newCropFrame.origin.x = floorf((CGRectGetWidth(self.bounds) - newCropFrame.size.width) * 0.5f);
  854. newCropFrame.origin.y = floorf((CGRectGetHeight(self.bounds) - newCropFrame.size.height) * 0.5f);
  855. //If we're animated, generate a snapshot view that we'll animate in place of the real view
  856. UIView *snapshotView = nil;
  857. if (animated) {
  858. snapshotView = [self.foregroundContainerView snapshotViewAfterScreenUpdates:NO];
  859. self.rotateAnimationInProgress = YES;
  860. }
  861. //Re-adjust the scrolling dimensions of the scroll view to match the new size
  862. self.scrollView.minimumZoomScale *= scale;
  863. self.scrollView.zoomScale *= scale;
  864. //Rotate the background image view, inside its container view
  865. self.backgroundImageView.transform = rotation;
  866. //Flip the width/height of the container view so it matches the rotated image view's size
  867. CGSize containerSize = self.backgroundContainerView.frame.size;
  868. self.backgroundContainerView.frame = (CGRect){CGPointZero, {containerSize.height, containerSize.width}};
  869. self.backgroundImageView.frame = (CGRect){CGPointZero, self.backgroundImageView.frame.size};
  870. //Rotate the foreground image view to match
  871. self.foregroundContainerView.transform = CGAffineTransformIdentity;
  872. self.foregroundImageView.transform = rotation;
  873. //Flip the content size of the scroll view to match the rotated bounds
  874. self.scrollView.contentSize = self.backgroundContainerView.frame.size;
  875. //assign the new crop box frame and re-adjust the content to fill it
  876. self.cropBoxFrame = newCropFrame;
  877. [self moveCroppedContentToCenterAnimated:NO];
  878. newCropFrame = self.cropBoxFrame;
  879. //work out how to line up out point of interest into the middle of the crop box
  880. cropTargetPoint.x *= scale;
  881. cropTargetPoint.y *= scale;
  882. //swap the target dimensions to match a -90 degree rotation
  883. CGFloat swap = cropTargetPoint.x;
  884. cropTargetPoint.x = cropTargetPoint.y;
  885. cropTargetPoint.y = self.scrollView.contentSize.height - swap;
  886. //reapply the translated scroll offset to the scroll view
  887. CGPoint midPoint = {CGRectGetMidX(newCropFrame), CGRectGetMidY(newCropFrame)};
  888. CGPoint offset = CGPointZero;
  889. offset.x = floorf(-midPoint.x + cropTargetPoint.x);
  890. offset.y = floorf(-midPoint.y + cropTargetPoint.y);
  891. offset.x = MAX(-self.scrollView.contentInset.left, offset.x);
  892. offset.y = MAX(-self.scrollView.contentInset.top, offset.y);
  893. //if the scroll view's new scale is 1 and the new offset is equal to the old, will not trigger the delegate 'scrollViewDidScroll:'
  894. //so we should call the method manually to update the foregroundImageView's frame
  895. if (offset.x == self.scrollView.contentOffset.x && offset.y == self.scrollView.contentOffset.y && scale == 1) {
  896. [self matchForegroundToBackground];
  897. }
  898. self.scrollView.contentOffset = offset;
  899. //If we're animated, play an animation of the snapshot view rotating,
  900. //then fade it out over the live content
  901. if (animated) {
  902. snapshotView.center = self.scrollView.center;
  903. [self addSubview:snapshotView];
  904. self.backgroundContainerView.hidden = YES;
  905. self.foregroundContainerView.hidden = YES;
  906. self.translucencyView.hidden = YES;
  907. self.gridOverlayView.hidden = YES;
  908. [UIView animateWithDuration:0.45f delay:0.0f usingSpringWithDamping:1.0f initialSpringVelocity:0.8f options:0 animations:^{
  909. CGAffineTransform transform = CGAffineTransformRotate(CGAffineTransformIdentity, -M_PI_2);
  910. transform = CGAffineTransformScale(transform, scale, scale);
  911. snapshotView.transform = transform;
  912. } completion:^(BOOL complete) {
  913. self.backgroundContainerView.hidden = NO;
  914. self.foregroundContainerView.hidden = NO;
  915. self.translucencyView.hidden = NO;
  916. self.gridOverlayView.hidden = NO;
  917. self.backgroundContainerView.alpha = 0.0f;
  918. self.gridOverlayView.alpha = 0.0f;
  919. self.translucencyView.alpha = 1.0f;
  920. [UIView animateWithDuration:0.45f animations:^{
  921. snapshotView.alpha = 0.0f;
  922. self.backgroundContainerView.alpha = 1.0f;
  923. self.gridOverlayView.alpha = 1.0f;
  924. } completion:^(BOOL complete) {
  925. self.rotateAnimationInProgress = NO;
  926. [snapshotView removeFromSuperview];
  927. }];
  928. }];
  929. }
  930. [self checkForCanReset];
  931. }
  932. #pragma mark - Resettable State -
  933. - (void)checkForCanReset
  934. {
  935. BOOL canReset = NO;
  936. if (self.angle != 0) { //Image has been rotated
  937. canReset = YES;
  938. }
  939. else if (self.scrollView.zoomScale > self.scrollView.minimumZoomScale + FLT_EPSILON) { //image has been zoomed in
  940. canReset = YES;
  941. }
  942. else if ((NSInteger)floorf(self.cropBoxFrame.size.width) != (NSInteger)floorf(self.originalCropBoxSize.width) || (NSInteger)floorf(self.cropBoxFrame.size.height) != (NSInteger)floorf(self.originalCropBoxSize.height)) { //crop box has been changed
  943. canReset = YES;
  944. }
  945. if (canReset && self.canReset == NO) {
  946. self.canReset = YES;
  947. if ([self.delegate respondsToSelector:@selector(cropViewDidBecomeResettable:)])
  948. [self.delegate cropViewDidBecomeResettable:self];
  949. }
  950. else if (!canReset && self.canReset) {
  951. self.canReset = NO;
  952. if ([self.delegate respondsToSelector:@selector(cropViewDidBecomeNonResettable:)])
  953. [self.delegate cropViewDidBecomeNonResettable:self];
  954. }
  955. }
  956. #pragma mark - Convienience Methods -
  957. - (CGRect)contentBounds
  958. {
  959. CGRect contentRect = CGRectZero;
  960. contentRect.origin.x = kTOCropViewPadding + self.cropRegionInsets.left;
  961. contentRect.origin.y = kTOCropViewPadding + self.cropRegionInsets.top;
  962. contentRect.size.width = CGRectGetWidth(self.bounds) - ((kTOCropViewPadding * 2) + self.cropRegionInsets.left + self.cropRegionInsets.right);
  963. contentRect.size.height = CGRectGetHeight(self.bounds) - ((kTOCropViewPadding * 2) + self.cropRegionInsets.top + self.cropRegionInsets.bottom);
  964. return contentRect;
  965. }
  966. - (CGSize)imageSize
  967. {
  968. if (self.angle == -90 || self.angle == -270)
  969. return (CGSize){self.image.size.height, self.image.size.width};
  970. return (CGSize){self.image.size.width, self.image.size.height};
  971. }
  972. @end