@onekeyfe/react-native-image-crop-picker 3.0.147

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +83 -0
  3. package/ReactNativeImageCropPicker.podspec +40 -0
  4. package/android/CMakeLists.txt +24 -0
  5. package/android/build.gradle +144 -0
  6. package/android/gradle.properties +4 -0
  7. package/android/src/main/AndroidManifest.xml +24 -0
  8. package/android/src/main/cpp/cpp-adapter.cpp +6 -0
  9. package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ImageCropPickerException.kt +45 -0
  10. package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ImageCropPickerImageProcessor.kt +308 -0
  11. package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ImageCropPickerSession.kt +297 -0
  12. package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ReactNativeImageCropPicker.kt +69 -0
  13. package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ReactNativeImageCropPickerPackage.kt +24 -0
  14. package/ios/ImageCropPickerError.swift +57 -0
  15. package/ios/ImageCropPickerImageProcessor.swift +343 -0
  16. package/ios/ImageCropPickerSession.swift +407 -0
  17. package/ios/ReactNativeImageCropPicker.swift +65 -0
  18. package/ios/TOCropViewController/Categories/UIImage+CropRotate.h +39 -0
  19. package/ios/TOCropViewController/Categories/UIImage+CropRotate.m +94 -0
  20. package/ios/TOCropViewController/Constants/TOCropViewConstants.h +63 -0
  21. package/ios/TOCropViewController/LICENSE +21 -0
  22. package/ios/TOCropViewController/Models/TOActivityCroppedImageProvider.h +38 -0
  23. package/ios/TOCropViewController/Models/TOActivityCroppedImageProvider.m +73 -0
  24. package/ios/TOCropViewController/Models/TOCropViewControllerAspectRatioPreset.h +64 -0
  25. package/ios/TOCropViewController/Models/TOCropViewControllerAspectRatioPreset.m +121 -0
  26. package/ios/TOCropViewController/Models/TOCropViewControllerTransitioning.h +49 -0
  27. package/ios/TOCropViewController/Models/TOCropViewControllerTransitioning.m +125 -0
  28. package/ios/TOCropViewController/Models/TOCroppedImageAttributes.h +38 -0
  29. package/ios/TOCropViewController/Models/TOCroppedImageAttributes.m +45 -0
  30. package/ios/TOCropViewController/Resources/Base.lproj/TOCropViewControllerLocalizable.strings +8 -0
  31. package/ios/TOCropViewController/Resources/PrivacyInfo.xcprivacy +14 -0
  32. package/ios/TOCropViewController/Resources/ar.lproj/TOCropViewControllerLocalizable.strings +9 -0
  33. package/ios/TOCropViewController/Resources/ca.lproj/TOCropViewControllerLocalizable.strings +8 -0
  34. package/ios/TOCropViewController/Resources/cs.lproj/TOCropViewControllerLocalizable.strings +8 -0
  35. package/ios/TOCropViewController/Resources/da-DK.lproj/TOCropViewControllerLocalizable.strings +8 -0
  36. package/ios/TOCropViewController/Resources/de.lproj/TOCropViewControllerLocalizable.strings +8 -0
  37. package/ios/TOCropViewController/Resources/en.lproj/TOCropViewControllerLocalizable.strings +8 -0
  38. package/ios/TOCropViewController/Resources/es.lproj/TOCropViewControllerLocalizable.strings +8 -0
  39. package/ios/TOCropViewController/Resources/fa-IR.lproj/TOCropViewControllerLocalizable.strings +8 -0
  40. package/ios/TOCropViewController/Resources/fa.lproj/TOCropViewControllerLocalizable.strings +8 -0
  41. package/ios/TOCropViewController/Resources/fi.lproj/TOCropViewControllerLocalizable.strings +8 -0
  42. package/ios/TOCropViewController/Resources/fr.lproj/TOCropViewControllerLocalizable.strings +8 -0
  43. package/ios/TOCropViewController/Resources/hu.lproj/TOCropViewControllerLocalizable.strings +9 -0
  44. package/ios/TOCropViewController/Resources/id.lproj/TOCropViewControllerLocalizable.strings +8 -0
  45. package/ios/TOCropViewController/Resources/it.lproj/TOCropViewControllerLocalizable.strings +8 -0
  46. package/ios/TOCropViewController/Resources/ja.lproj/TOCropViewControllerLocalizable.strings +8 -0
  47. package/ios/TOCropViewController/Resources/ko.lproj/TOCropViewControllerLocalizable.strings +8 -0
  48. package/ios/TOCropViewController/Resources/ms.lproj/TOCropViewControllerLocalizable.strings +8 -0
  49. package/ios/TOCropViewController/Resources/nl.lproj/TOCropViewControllerLocalizable.strings +8 -0
  50. package/ios/TOCropViewController/Resources/pl.lproj/TOCropViewControllerLocalizable.strings +8 -0
  51. package/ios/TOCropViewController/Resources/pt-BR.lproj/TOCropViewControllerLocalizable.strings +8 -0
  52. package/ios/TOCropViewController/Resources/pt.lproj/TOCropViewControllerLocalizable.strings +8 -0
  53. package/ios/TOCropViewController/Resources/ro.lproj/TOCropViewControllerLocalizable.strings +8 -0
  54. package/ios/TOCropViewController/Resources/ru.lproj/TOCropViewControllerLocalizable.strings +8 -0
  55. package/ios/TOCropViewController/Resources/sk.lproj/TOCropViewControllerLocalizable.strings +8 -0
  56. package/ios/TOCropViewController/Resources/tr.lproj/TOCropViewControllerLocalizable.strings +9 -0
  57. package/ios/TOCropViewController/Resources/uk.lproj/TOCropViewControllerLocalizable.strings +8 -0
  58. package/ios/TOCropViewController/Resources/vi.lproj/TOCropViewControllerLocalizable.strings +9 -0
  59. package/ios/TOCropViewController/Resources/zh-Hans.lproj/TOCropViewControllerLocalizable.strings +9 -0
  60. package/ios/TOCropViewController/Resources/zh-Hant.lproj/TOCropViewControllerLocalizable.strings +9 -0
  61. package/ios/TOCropViewController/TOCropViewController.h +479 -0
  62. package/ios/TOCropViewController/TOCropViewController.m +1355 -0
  63. package/ios/TOCropViewController/Views/TOCropOverlayView.h +43 -0
  64. package/ios/TOCropViewController/Views/TOCropOverlayView.m +240 -0
  65. package/ios/TOCropViewController/Views/TOCropScrollView.h +39 -0
  66. package/ios/TOCropViewController/Views/TOCropScrollView.m +48 -0
  67. package/ios/TOCropViewController/Views/TOCropToolbar.h +104 -0
  68. package/ios/TOCropViewController/Views/TOCropToolbar.m +783 -0
  69. package/ios/TOCropViewController/Views/TOCropView.h +311 -0
  70. package/ios/TOCropViewController/Views/TOCropView.m +1787 -0
  71. package/lib/module/ReactNativeImageCropPicker.nitro.js +4 -0
  72. package/lib/module/ReactNativeImageCropPicker.nitro.js.map +1 -0
  73. package/lib/module/index.js +83 -0
  74. package/lib/module/index.js.map +1 -0
  75. package/lib/module/package.json +1 -0
  76. package/lib/typescript/package.json +1 -0
  77. package/lib/typescript/src/ReactNativeImageCropPicker.nitro.d.ts +54 -0
  78. package/lib/typescript/src/ReactNativeImageCropPicker.nitro.d.ts.map +1 -0
  79. package/lib/typescript/src/index.d.ts +31 -0
  80. package/lib/typescript/src/index.d.ts.map +1 -0
  81. package/nitro.json +23 -0
  82. package/nitrogen/generated/android/c++/JCropRect.hpp +69 -0
  83. package/nitrogen/generated/android/c++/JHybridReactNativeImageCropPickerSpec.cpp +125 -0
  84. package/nitrogen/generated/android/c++/JHybridReactNativeImageCropPickerSpec.hpp +66 -0
  85. package/nitrogen/generated/android/c++/JImageCropPickerOptions.hpp +154 -0
  86. package/nitrogen/generated/android/c++/JPickedImage.hpp +88 -0
  87. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/CropRect.kt +66 -0
  88. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/HybridReactNativeImageCropPickerSpec.kt +69 -0
  89. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/ImageCropPickerOptions.kt +171 -0
  90. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/PickedImage.kt +86 -0
  91. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/reactnativeimagecroppickerOnLoad.kt +35 -0
  92. package/nitrogen/generated/android/reactnativeimagecroppicker+autolinking.cmake +81 -0
  93. package/nitrogen/generated/android/reactnativeimagecroppicker+autolinking.gradle +27 -0
  94. package/nitrogen/generated/android/reactnativeimagecroppickerOnLoad.cpp +54 -0
  95. package/nitrogen/generated/android/reactnativeimagecroppickerOnLoad.hpp +34 -0
  96. package/nitrogen/generated/ios/ReactNativeImageCropPicker+autolinking.rb +62 -0
  97. package/nitrogen/generated/ios/ReactNativeImageCropPicker-Swift-Cxx-Bridge.cpp +57 -0
  98. package/nitrogen/generated/ios/ReactNativeImageCropPicker-Swift-Cxx-Bridge.hpp +221 -0
  99. package/nitrogen/generated/ios/ReactNativeImageCropPicker-Swift-Cxx-Umbrella.hpp +55 -0
  100. package/nitrogen/generated/ios/ReactNativeImageCropPickerAutolinking.mm +33 -0
  101. package/nitrogen/generated/ios/ReactNativeImageCropPickerAutolinking.swift +26 -0
  102. package/nitrogen/generated/ios/c++/HybridReactNativeImageCropPickerSpecSwift.cpp +11 -0
  103. package/nitrogen/generated/ios/c++/HybridReactNativeImageCropPickerSpecSwift.hpp +116 -0
  104. package/nitrogen/generated/ios/swift/CropRect.swift +44 -0
  105. package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
  106. package/nitrogen/generated/ios/swift/Func_void_PickedImage.swift +46 -0
  107. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  108. package/nitrogen/generated/ios/swift/HybridReactNativeImageCropPickerSpec.swift +58 -0
  109. package/nitrogen/generated/ios/swift/HybridReactNativeImageCropPickerSpec_cxx.swift +202 -0
  110. package/nitrogen/generated/ios/swift/ImageCropPickerOptions.swift +474 -0
  111. package/nitrogen/generated/ios/swift/PickedImage.swift +96 -0
  112. package/nitrogen/generated/shared/c++/CropRect.hpp +95 -0
  113. package/nitrogen/generated/shared/c++/HybridReactNativeImageCropPickerSpec.cpp +24 -0
  114. package/nitrogen/generated/shared/c++/HybridReactNativeImageCropPickerSpec.hpp +71 -0
  115. package/nitrogen/generated/shared/c++/ImageCropPickerOptions.hpp +180 -0
  116. package/nitrogen/generated/shared/c++/PickedImage.hpp +114 -0
  117. package/package.json +173 -0
  118. package/src/ReactNativeImageCropPicker.nitro.ts +72 -0
  119. package/src/index.tsx +134 -0
@@ -0,0 +1,1787 @@
1
+ //
2
+ // TOCropView.m
3
+ //
4
+ // Copyright 2015-2026 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
+
23
+ #import "TOCropView.h"
24
+
25
+ #import "TOCropOverlayView.h"
26
+ #import "TOCropScrollView.h"
27
+
28
+ #define TOCROPVIEW_BACKGROUND_COLOR [UIColor colorWithWhite:0.12f alpha:1.0f]
29
+
30
+ static const CGFloat kTOCropViewPadding = 14.0f;
31
+ static const NSTimeInterval kTOCropTimerDuration = 0.8f;
32
+ static const CGFloat kTOCropViewMinimumBoxSize = 42.0f;
33
+ static const CGFloat kTOMaximumZoomScale = 15.0f;
34
+
35
+ /* When the user taps down to resize the box, this state is used
36
+ to determine where they tapped and how to manipulate the box */
37
+ typedef NS_ENUM(NSInteger, TOCropViewOverlayEdge) {
38
+ TOCropViewOverlayEdgeNone,
39
+ TOCropViewOverlayEdgeTopLeft,
40
+ TOCropViewOverlayEdgeTop,
41
+ TOCropViewOverlayEdgeTopRight,
42
+ TOCropViewOverlayEdgeRight,
43
+ TOCropViewOverlayEdgeBottomRight,
44
+ TOCropViewOverlayEdgeBottom,
45
+ TOCropViewOverlayEdgeBottomLeft,
46
+ TOCropViewOverlayEdgeLeft
47
+ };
48
+
49
+ @interface TOCropView () <UIScrollViewDelegate, UIGestureRecognizerDelegate>
50
+
51
+ @property (nonatomic, strong, readwrite) UIImage *image;
52
+ @property (nonatomic, assign, readwrite) TOCropViewCroppingStyle croppingStyle;
53
+
54
+ /* Views */
55
+ @property (nonatomic, strong) UIImageView *backgroundImageView; /* The main image view, placed within the scroll view */
56
+ @property (nonatomic, strong) UIView *backgroundContainerView; /* A view which contains the background image view, to separate its transforms from the scroll view. */
57
+ @property (nonatomic, strong, readwrite) UIView *foregroundContainerView;
58
+ @property (nonatomic, strong) UIImageView *foregroundImageView; /* A copy of the background image view, placed over the dimming views */
59
+ @property (nonatomic, strong) TOCropScrollView *scrollView; /* The scroll view in charge of panning/zooming the image. */
60
+ @property (nonatomic, strong) UIView *overlayView; /* A semi-transparent grey view, overlaid on top of the background image */
61
+ @property (nonatomic, strong) UIView *translucencyView; /* A blur view that is made visible when the user isn't interacting with the crop view */
62
+ @property (nonatomic, strong) id translucencyEffect; /* The dark blur visual effect applied to the visual effect view. */
63
+ @property (nonatomic, strong, readwrite) TOCropOverlayView *gridOverlayView; /* A grid view overlaid on top of the foreground image view's container. */
64
+
65
+ /* Gesture Recognizers */
66
+ @property (nonatomic, strong) UIPanGestureRecognizer *gridPanGestureRecognizer; /* The gesture recognizer in charge of controlling the resizing of the crop view */
67
+
68
+ /* Crop box handling */
69
+ @property (nonatomic, assign) BOOL applyInitialCroppedImageFrame; /* No by default, when setting initialCroppedImageFrame this will be set to YES, and set back to NO after first application - so it's only done once */
70
+ @property (nonatomic, assign) TOCropViewOverlayEdge tappedEdge; /* The edge region that the user tapped on, to resize the cropping region */
71
+ @property (nonatomic, assign) CGRect cropOriginFrame; /* When resizing, this is the original frame of the crop box. */
72
+ @property (nonatomic, assign) CGPoint panOriginPoint; /* The initial touch point of the pan gesture recognizer */
73
+ @property (nonatomic, assign, readwrite) CGRect cropBoxFrame; /* The frame, in relation to to this view where the grid, and crop container view are aligned */
74
+ @property (nonatomic, strong) NSTimer *resetTimer; /* The timer used to reset the view after the user stops interacting with it */
75
+ @property (nonatomic, assign) BOOL editing; /* Used to denote the active state of the user manipulating the content */
76
+ @property (nonatomic, assign) BOOL disableForgroundMatching; /* At times during animation, disable matching the forground image view to the background */
77
+
78
+ /* Pre-screen-rotation state information */
79
+ @property (nonatomic, assign) CGPoint rotationContentOffset;
80
+ @property (nonatomic, assign) CGSize rotationContentSize;
81
+ @property (nonatomic, assign) CGRect rotationBoundFrame;
82
+
83
+ /* View State information */
84
+ @property (nonatomic, readonly) CGRect contentBounds; /* Give the current screen real-estate, the frame that the scroll view is allowed to use */
85
+ @property (nonatomic, readonly) CGSize imageSize; /* Given the current rotation of the image, the size of the image */
86
+ @property (nonatomic, readonly) BOOL hasAspectRatio; /* True if an aspect ratio was explicitly applied to this crop view */
87
+
88
+ /* 90-degree rotation state data */
89
+ @property (nonatomic, assign) CGSize cropBoxLastEditedSize; /* When performing 90-degree rotations, remember what our last manual size was to use that as a base */
90
+ @property (nonatomic, assign) NSInteger cropBoxLastEditedAngle; /* Remember which angle we were at when we saved the editing size */
91
+ @property (nonatomic, assign) CGFloat cropBoxLastEditedZoomScale; /* Remember the zoom size when we last edited */
92
+ @property (nonatomic, assign) CGFloat cropBoxLastEditedMinZoomScale; /* Remember the minimum size when we last edited. */
93
+ @property (nonatomic, assign) CGFloat cropBoxLastEditedMaxZoomScale; /* Remember the zoom ceiling when we last edited. */
94
+ @property (nonatomic, assign) CGFloat baseMaximumZoomScale; /* The absolute zoom ceiling for the current layout; the scroll view's own maximum is always derived from this */
95
+ @property (nonatomic, assign) BOOL rotateAnimationInProgress; /* Disallow any input while the rotation animation is playing */
96
+
97
+ /* Reset state data */
98
+ @property (nonatomic, assign) CGSize originalCropBoxSize; /* Save the original crop box size so we can tell when the content has been edited */
99
+ @property (nonatomic, assign) CGPoint originalContentOffset; /* Save the original content offset so we can tell if it's been scrolled. */
100
+ @property (nonatomic, assign, readwrite) BOOL canBeReset;
101
+
102
+ /* If restoring to a previous crop setting, these properties hang onto the
103
+ values until the view is configured for the first time. */
104
+ @property (nonatomic, assign) NSInteger restoreAngle;
105
+ @property (nonatomic, assign) CGRect restoreImageCropFrame;
106
+
107
+ /* Set to YES once `performInitialLayout` is called. This lets pending properties get queued until the view
108
+ has been properly set up in its parent. */
109
+ @property (nonatomic, assign) BOOL initialSetupPerformed;
110
+
111
+ @end
112
+
113
+ @implementation TOCropView
114
+
115
+ - (instancetype)initWithImage:(UIImage *)image {
116
+ return [self initWithCroppingStyle:TOCropViewCroppingStyleDefault image:image];
117
+ }
118
+
119
+ - (instancetype)initWithCroppingStyle:(TOCropViewCroppingStyle)style image:(UIImage *)image {
120
+ if (self = [super init]) {
121
+ _image = image;
122
+ _croppingStyle = style;
123
+ [self setup];
124
+ }
125
+
126
+ return self;
127
+ }
128
+
129
+ - (void)setup {
130
+ __weak typeof(self) weakSelf = self;
131
+
132
+ BOOL circularMode = (self.croppingStyle == TOCropViewCroppingStyleCircular);
133
+
134
+ // View properties
135
+ self.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
136
+ self.backgroundColor = TOCROPVIEW_BACKGROUND_COLOR;
137
+ self.cropBoxFrame = CGRectZero;
138
+ self.applyInitialCroppedImageFrame = NO;
139
+ self.editing = NO;
140
+ self.cropBoxResizeEnabled = !circularMode;
141
+ self.aspectRatio = circularMode ? (CGSize){1.0f, 1.0f} : CGSizeZero;
142
+ self.resetAspectRatioEnabled = !circularMode;
143
+ self.restoreImageCropFrame = CGRectZero;
144
+ self.restoreAngle = 0;
145
+ self.cropAdjustingDelay = kTOCropTimerDuration;
146
+ self.cropViewPadding = kTOCropViewPadding;
147
+ self.maximumZoomScale = kTOMaximumZoomScale;
148
+
149
+ // Scroll View properties
150
+ self.scrollView = [[TOCropScrollView alloc] initWithFrame:self.bounds];
151
+ self.scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
152
+ self.scrollView.alwaysBounceHorizontal = YES;
153
+ self.scrollView.alwaysBounceVertical = YES;
154
+ self.scrollView.showsHorizontalScrollIndicator = NO;
155
+ self.scrollView.showsVerticalScrollIndicator = NO;
156
+ self.scrollView.delegate = self;
157
+ [self addSubview:self.scrollView];
158
+
159
+ self.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
160
+ self.scrollView.touchesBegan = ^{ [weakSelf startEditing]; };
161
+ self.scrollView.touchesEnded = ^{ [weakSelf startResetTimer]; };
162
+ self.scrollView.touchesCancelled = ^{ [weakSelf handleScrollViewTouchCancellation]; };
163
+
164
+ // Background Image View
165
+ self.backgroundImageView = [[UIImageView alloc] initWithImage:self.image];
166
+ self.backgroundImageView.layer.minificationFilter = kCAFilterTrilinear;
167
+
168
+ // Background container view
169
+ self.backgroundContainerView = [[UIView alloc] initWithFrame:self.backgroundImageView.frame];
170
+ [self.backgroundContainerView addSubview:self.backgroundImageView];
171
+ [self.scrollView addSubview:self.backgroundContainerView];
172
+
173
+ // Grey transparent overlay view
174
+ self.overlayView = [[UIView alloc] initWithFrame:self.bounds];
175
+ self.overlayView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
176
+ self.overlayView.backgroundColor = [self.backgroundColor colorWithAlphaComponent:0.35f];
177
+ self.overlayView.hidden = NO;
178
+ self.overlayView.userInteractionEnabled = NO;
179
+ [self addSubview:self.overlayView];
180
+
181
+ // Translucency View
182
+ self.translucencyEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleDark];
183
+ self.translucencyView = [[UIVisualEffectView alloc] initWithEffect:self.translucencyEffect];
184
+ self.translucencyView.frame = self.bounds;
185
+ self.translucencyView.hidden = self.translucencyAlwaysHidden;
186
+ self.translucencyView.userInteractionEnabled = NO;
187
+ self.translucencyView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
188
+ [self addSubview:self.translucencyView];
189
+
190
+ // The forground container that holds the foreground image view
191
+ self.foregroundContainerView = [[UIView alloc] initWithFrame:(CGRect){0, 0, 200, 200}];
192
+ self.foregroundContainerView.clipsToBounds = YES;
193
+ self.foregroundContainerView.userInteractionEnabled = NO;
194
+ [self addSubview:self.foregroundContainerView];
195
+
196
+ self.foregroundImageView = [[UIImageView alloc] initWithImage:self.image];
197
+ self.foregroundImageView.layer.minificationFilter = kCAFilterTrilinear;
198
+ [self.foregroundContainerView addSubview:self.foregroundImageView];
199
+
200
+ // Disable colour inversion for the image views
201
+ self.foregroundImageView.accessibilityIgnoresInvertColors = YES;
202
+ self.backgroundImageView.accessibilityIgnoresInvertColors = YES;
203
+
204
+ // The following setup isn't needed during circular cropping
205
+ if (circularMode) {
206
+ return;
207
+ }
208
+
209
+ // The white grid overlay view
210
+ self.gridOverlayView = [[TOCropOverlayView alloc] initWithFrame:self.foregroundContainerView.frame];
211
+ self.gridOverlayView.userInteractionEnabled = NO;
212
+ self.gridOverlayView.gridHidden = YES;
213
+ [self addSubview:self.gridOverlayView];
214
+
215
+ // The pan controller to recognize gestures meant to resize the grid view
216
+ self.gridPanGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(gridPanGestureRecognized:)];
217
+ self.gridPanGestureRecognizer.delegate = self;
218
+ [self.scrollView.panGestureRecognizer requireGestureRecognizerToFail:self.gridPanGestureRecognizer];
219
+ [self addGestureRecognizer:self.gridPanGestureRecognizer];
220
+ }
221
+
222
+ - (void)dealloc {
223
+ [_resetTimer invalidate];
224
+ }
225
+
226
+ #pragma mark - View Layout -
227
+ - (void)performInitialSetup {
228
+ // Calling this more than once is potentially destructive
229
+ if (self.initialSetupPerformed) {
230
+ return;
231
+ }
232
+
233
+ // Disable from calling again
234
+ self.initialSetupPerformed = YES;
235
+
236
+ // Perform the initial layout of the image
237
+ [self layoutInitialImage];
238
+
239
+ // -- State Restoration --
240
+
241
+ // If the angle value was previously set before this point, apply it now
242
+ if (self.restoreAngle != 0) {
243
+ self.angle = self.restoreAngle;
244
+ self.restoreAngle = 0;
245
+ self.cropBoxLastEditedAngle = self.angle;
246
+ }
247
+
248
+ // If an image crop frame was also specified before creation, apply it now
249
+ if (!CGRectIsEmpty(self.restoreImageCropFrame)) {
250
+ self.imageCropFrame = self.restoreImageCropFrame;
251
+ self.restoreImageCropFrame = CGRectZero;
252
+ }
253
+
254
+ // Save the current layout state for later
255
+ [self captureStateForImageRotation];
256
+
257
+ // Check if we performed any resetabble modifications
258
+ [self checkForCanReset];
259
+ }
260
+
261
+ - (void)layoutInitialImage {
262
+ CGSize imageSize = self.imageSize;
263
+ // A zero-sized image would produce NaN geometry below, which crashes CALayer
264
+ if (imageSize.width < FLT_EPSILON || imageSize.height < FLT_EPSILON) {
265
+ return;
266
+ }
267
+ self.scrollView.contentSize = imageSize;
268
+
269
+ CGRect bounds = self.contentBounds;
270
+ CGSize boundsSize = bounds.size;
271
+
272
+ // work out the minimum scale of the object
273
+ CGFloat scale = 0.0f;
274
+
275
+ // Work out the size of the image to fit into the content bounds
276
+ scale = MIN(CGRectGetWidth(bounds) / imageSize.width, CGRectGetHeight(bounds) / imageSize.height);
277
+ CGSize scaledImageSize = (CGSize){floorf(imageSize.width * scale), floorf(imageSize.height * scale)};
278
+
279
+ // If an aspect ratio was pre-applied to the crop view, use that to work out the minimum scale the image needs to be to fit
280
+ CGSize cropBoxSize = CGSizeZero;
281
+ if (self.hasAspectRatio) {
282
+ CGFloat ratioScale = (self.aspectRatio.width / self.aspectRatio.height); // Work out the size of the width in relation to height
283
+ CGSize fullSizeRatio = (CGSize){boundsSize.height * ratioScale, boundsSize.height};
284
+ CGFloat fitScale = MIN(boundsSize.width / fullSizeRatio.width, boundsSize.height / fullSizeRatio.height);
285
+ cropBoxSize = (CGSize){fullSizeRatio.width * fitScale, fullSizeRatio.height * fitScale};
286
+
287
+ scale = MAX(cropBoxSize.width / imageSize.width, cropBoxSize.height / imageSize.height);
288
+ }
289
+
290
+ // Whether aspect ratio, or original, the final image size we'll base the rest of the calculations off
291
+ CGSize scaledSize = (CGSize){floorf(imageSize.width * scale), floorf(imageSize.height * scale)};
292
+
293
+ // Configure the scroll view
294
+ self.scrollView.minimumZoomScale = scale;
295
+ self.baseMaximumZoomScale = scale * self.maximumZoomScale;
296
+ [self updateScrollViewMaximumZoomScale];
297
+
298
+ // Set the crop box to the size we calculated and align in the middle of the screen
299
+ CGRect frame = CGRectZero;
300
+ frame.size = self.hasAspectRatio ? cropBoxSize : scaledSize;
301
+ frame.origin.x = floorf(bounds.origin.x + floorf((CGRectGetWidth(bounds) - frame.size.width) * 0.5f));
302
+ frame.origin.y = floorf(bounds.origin.y + floorf((CGRectGetHeight(bounds) - frame.size.height) * 0.5f));
303
+ self.cropBoxFrame = frame;
304
+
305
+ // set the fully zoomed out state initially
306
+ self.scrollView.zoomScale = self.scrollView.minimumZoomScale;
307
+ self.scrollView.contentSize = scaledSize;
308
+
309
+ // If we ended up with a smaller crop box than the content, line up the content so its center
310
+ // is in the center of the cropbox
311
+ if (frame.size.width < scaledSize.width - FLT_EPSILON || frame.size.height < scaledSize.height - FLT_EPSILON) {
312
+ CGPoint offset = CGPointZero;
313
+ offset.x = -floorf(CGRectGetMidX(bounds) - (scaledSize.width * 0.5f));
314
+ offset.y = -floorf(CGRectGetMidY(bounds) - (scaledSize.height * 0.5f));
315
+ self.scrollView.contentOffset = offset;
316
+ }
317
+
318
+ // save the current state for use with 90-degree rotations
319
+ self.cropBoxLastEditedAngle = 0;
320
+ [self captureStateForImageRotation];
321
+
322
+ // save the size for checking if we're in a resettable state
323
+ self.originalCropBoxSize = self.resetAspectRatioEnabled ? scaledImageSize : self.cropBoxFrame.size;
324
+ self.originalContentOffset = self.scrollView.contentOffset;
325
+
326
+ [self checkForCanReset];
327
+ [self matchForegroundToBackground];
328
+ }
329
+
330
+ - (void)prepareforRotation {
331
+ self.rotationContentOffset = self.scrollView.contentOffset;
332
+ self.rotationContentSize = self.scrollView.contentSize;
333
+ self.rotationBoundFrame = self.contentBounds;
334
+ }
335
+
336
+ - (void)performRelayoutForRotation {
337
+ CGRect cropFrame = self.cropBoxFrame;
338
+ CGRect contentFrame = self.contentBounds;
339
+
340
+ CGFloat scale = MIN(contentFrame.size.width / cropFrame.size.width, contentFrame.size.height / cropFrame.size.height);
341
+ self.scrollView.minimumZoomScale *= scale;
342
+ self.baseMaximumZoomScale *= scale;
343
+ [self updateScrollViewMaximumZoomScale];
344
+ self.scrollView.zoomScale *= scale;
345
+
346
+ // Work out the centered, upscaled version of the crop rectangle
347
+ cropFrame.size.width = floorf(cropFrame.size.width * scale);
348
+ cropFrame.size.height = floorf(cropFrame.size.height * scale);
349
+ cropFrame.origin.x = floorf(contentFrame.origin.x + ((contentFrame.size.width - cropFrame.size.width) * 0.5f));
350
+ cropFrame.origin.y = floorf(contentFrame.origin.y + ((contentFrame.size.height - cropFrame.size.height) * 0.5f));
351
+ self.cropBoxFrame = cropFrame;
352
+
353
+ [self captureStateForImageRotation];
354
+
355
+ // Work out the center point of the content before we rotated
356
+ CGPoint oldMidPoint = (CGPoint){CGRectGetMidX(self.rotationBoundFrame), CGRectGetMidY(self.rotationBoundFrame)};
357
+ CGPoint contentCenter = (CGPoint){self.rotationContentOffset.x + oldMidPoint.x, self.rotationContentOffset.y + oldMidPoint.y};
358
+
359
+ // Normalize it to a percentage we can apply to different sizes
360
+ CGPoint normalizedCenter = CGPointZero;
361
+ normalizedCenter.x = contentCenter.x / self.rotationContentSize.width;
362
+ normalizedCenter.y = contentCenter.y / self.rotationContentSize.height;
363
+
364
+ // Work out the new content offset by applying the normalized values to the new layout
365
+ CGPoint newMidPoint = (CGPoint){CGRectGetMidX(self.contentBounds), CGRectGetMidY(self.contentBounds)};
366
+
367
+ CGPoint translatedContentOffset = CGPointZero;
368
+ translatedContentOffset.x = self.scrollView.contentSize.width * normalizedCenter.x;
369
+ translatedContentOffset.y = self.scrollView.contentSize.height * normalizedCenter.y;
370
+
371
+ CGPoint offset = CGPointZero;
372
+ offset.x = floorf(translatedContentOffset.x - newMidPoint.x);
373
+ offset.y = floorf(translatedContentOffset.y - newMidPoint.y);
374
+
375
+ // Make sure it doesn't overshoot the top left corner of the crop box
376
+ offset.x = MAX(-self.scrollView.contentInset.left, offset.x);
377
+ offset.y = MAX(-self.scrollView.contentInset.top, offset.y);
378
+
379
+ // Nor undershoot the bottom right corner
380
+ CGPoint maximumOffset = CGPointZero;
381
+ maximumOffset.x = self.scrollView.contentSize.width - (self.bounds.size.width - self.scrollView.contentInset.right);
382
+ maximumOffset.y = self.scrollView.contentSize.height - (self.bounds.size.height - self.scrollView.contentInset.bottom);
383
+ offset.x = MIN(offset.x, maximumOffset.x);
384
+ offset.y = MIN(offset.y, maximumOffset.y);
385
+ self.scrollView.contentOffset = offset;
386
+
387
+ // Line up the background instance of the image
388
+ [self matchForegroundToBackground];
389
+ }
390
+
391
+ - (void)matchForegroundToBackground {
392
+ if (self.disableForgroundMatching)
393
+ return;
394
+
395
+ // We can't simply match the frames since if the images are rotated, the frame property becomes unusable
396
+ self.foregroundImageView.frame = [self.backgroundContainerView.superview
397
+ convertRect:self.backgroundContainerView.frame
398
+ toView:self.foregroundContainerView];
399
+ }
400
+
401
+ - (void)updateCropBoxFrameWithGesturePoint:(CGPoint)point {
402
+ CGRect frame = self.cropBoxFrame;
403
+ CGRect originFrame = self.cropOriginFrame;
404
+ CGRect contentFrame = self.contentBounds;
405
+
406
+ point.x = MAX(contentFrame.origin.x - self.cropViewPadding, point.x);
407
+ point.y = MAX(contentFrame.origin.y - self.cropViewPadding, point.y);
408
+
409
+ // The delta between where we first tapped, and where our finger is now
410
+ CGFloat xDelta = ceilf(point.x - self.panOriginPoint.x);
411
+ CGFloat yDelta = ceilf(point.y - self.panOriginPoint.y);
412
+
413
+ // Current aspect ratio of the crop box in case we need to clamp it
414
+ CGFloat aspectRatio = (originFrame.size.width / originFrame.size.height);
415
+
416
+ // Note whether we're being aspect transformed horizontally or vertically
417
+ BOOL aspectHorizontal = NO, aspectVertical = NO;
418
+
419
+ // Depending on which corner we drag from, set the appropriate min flag to
420
+ // ensure we can properly clamp the XY value of the box if it overruns the minimum size
421
+ //(Otherwise the image itself will slide with the drag gesture)
422
+ BOOL clampMinFromTop = NO, clampMinFromLeft = NO;
423
+
424
+ switch (self.tappedEdge) {
425
+ case TOCropViewOverlayEdgeLeft:
426
+ if (self.aspectRatioLockEnabled) {
427
+ aspectHorizontal = YES;
428
+ xDelta = MAX(xDelta, 0);
429
+ CGPoint scaleOrigin = (CGPoint){CGRectGetMaxX(originFrame), CGRectGetMidY(originFrame)};
430
+ frame.size.height = frame.size.width / aspectRatio;
431
+ frame.origin.y = scaleOrigin.y - (frame.size.height * 0.5f);
432
+ }
433
+ CGFloat newWidth = originFrame.size.width - xDelta;
434
+ CGFloat newHeight = originFrame.size.height;
435
+ if (MIN(newHeight, newWidth) / MAX(newHeight, newWidth) >= (double)_minimumAspectRatio) {
436
+ frame.origin.x = originFrame.origin.x + xDelta;
437
+ frame.size.width = originFrame.size.width - xDelta;
438
+ }
439
+
440
+ clampMinFromLeft = YES;
441
+
442
+ break;
443
+ case TOCropViewOverlayEdgeRight:
444
+ if (self.aspectRatioLockEnabled) {
445
+ aspectHorizontal = YES;
446
+ CGPoint scaleOrigin = (CGPoint){CGRectGetMinX(originFrame), CGRectGetMidY(originFrame)};
447
+ frame.size.height = frame.size.width / aspectRatio;
448
+ frame.origin.y = scaleOrigin.y - (frame.size.height * 0.5f);
449
+ frame.size.width = originFrame.size.width + xDelta;
450
+ frame.size.width = MIN(frame.size.width, contentFrame.size.height * aspectRatio);
451
+ } else {
452
+ CGFloat newWidth = originFrame.size.width + xDelta;
453
+ CGFloat newHeight = originFrame.size.height;
454
+ if (MIN(newHeight, newWidth) / MAX(newHeight, newWidth) >= (double)_minimumAspectRatio) {
455
+ frame.size.width = originFrame.size.width + xDelta;
456
+ }
457
+ }
458
+
459
+ break;
460
+ case TOCropViewOverlayEdgeBottom:
461
+ if (self.aspectRatioLockEnabled) {
462
+ aspectVertical = YES;
463
+ CGPoint scaleOrigin = (CGPoint){CGRectGetMidX(originFrame), CGRectGetMinY(originFrame)};
464
+ frame.size.width = frame.size.height * aspectRatio;
465
+ frame.origin.x = scaleOrigin.x - (frame.size.width * 0.5f);
466
+ frame.size.height = originFrame.size.height + yDelta;
467
+ frame.size.height = MIN(frame.size.height, contentFrame.size.width / aspectRatio);
468
+ } else {
469
+ CGFloat newWidth = originFrame.size.width;
470
+ CGFloat newHeight = originFrame.size.height + yDelta;
471
+
472
+ if (MIN(newHeight, newWidth) / MAX(newHeight, newWidth) >= (double)_minimumAspectRatio) {
473
+ frame.size.height = originFrame.size.height + yDelta;
474
+ }
475
+ }
476
+ break;
477
+ case TOCropViewOverlayEdgeTop:
478
+ if (self.aspectRatioLockEnabled) {
479
+ aspectVertical = YES;
480
+ yDelta = MAX(0, yDelta);
481
+ CGPoint scaleOrigin = (CGPoint){CGRectGetMidX(originFrame), CGRectGetMaxY(originFrame)};
482
+ frame.size.width = frame.size.height * aspectRatio;
483
+ frame.origin.x = scaleOrigin.x - (frame.size.width * 0.5f);
484
+ frame.origin.y = originFrame.origin.y + yDelta;
485
+ frame.size.height = originFrame.size.height - yDelta;
486
+ } else {
487
+ CGFloat newWidth = originFrame.size.width;
488
+ CGFloat newHeight = originFrame.size.height - yDelta;
489
+
490
+ if (MIN(newHeight, newWidth) / MAX(newHeight, newWidth) >= (double)_minimumAspectRatio) {
491
+ frame.origin.y = originFrame.origin.y + yDelta;
492
+ frame.size.height = originFrame.size.height - yDelta;
493
+ }
494
+ }
495
+
496
+ clampMinFromTop = YES;
497
+
498
+ break;
499
+ case TOCropViewOverlayEdgeTopLeft:
500
+ if (self.aspectRatioLockEnabled) {
501
+ xDelta = MAX(xDelta, 0);
502
+ yDelta = MAX(yDelta, 0);
503
+
504
+ CGPoint distance;
505
+ distance.x = 1.0f - (xDelta / CGRectGetWidth(originFrame));
506
+ distance.y = 1.0f - (yDelta / CGRectGetHeight(originFrame));
507
+
508
+ CGFloat scale = (distance.x + distance.y) * 0.5f;
509
+
510
+ frame.size.width = ceilf(CGRectGetWidth(originFrame) * scale);
511
+ frame.size.height = ceilf(CGRectGetHeight(originFrame) * scale);
512
+ frame.origin.x = originFrame.origin.x + (CGRectGetWidth(originFrame) - frame.size.width);
513
+ frame.origin.y = originFrame.origin.y + (CGRectGetHeight(originFrame) - frame.size.height);
514
+
515
+ aspectVertical = YES;
516
+ aspectHorizontal = YES;
517
+ } else {
518
+ CGFloat newWidth = originFrame.size.width - xDelta;
519
+ CGFloat newHeight = originFrame.size.height - yDelta;
520
+
521
+ if (MIN(newHeight, newWidth) / MAX(newHeight, newWidth) >= (double)_minimumAspectRatio) {
522
+ frame.origin.x = originFrame.origin.x + xDelta;
523
+ frame.size.width = originFrame.size.width - xDelta;
524
+ frame.origin.y = originFrame.origin.y + yDelta;
525
+ frame.size.height = originFrame.size.height - yDelta;
526
+ }
527
+ }
528
+
529
+ clampMinFromTop = YES;
530
+ clampMinFromLeft = YES;
531
+
532
+ break;
533
+ case TOCropViewOverlayEdgeTopRight:
534
+ if (self.aspectRatioLockEnabled) {
535
+ xDelta = MIN(xDelta, 0);
536
+ yDelta = MAX(yDelta, 0);
537
+
538
+ CGPoint distance;
539
+ distance.x = 1.0f - ((-xDelta) / CGRectGetWidth(originFrame));
540
+ distance.y = 1.0f - ((yDelta) / CGRectGetHeight(originFrame));
541
+
542
+ CGFloat scale = (distance.x + distance.y) * 0.5f;
543
+
544
+ frame.size.width = ceilf(CGRectGetWidth(originFrame) * scale);
545
+ frame.size.height = ceilf(CGRectGetHeight(originFrame) * scale);
546
+ frame.origin.y = originFrame.origin.y + (CGRectGetHeight(originFrame) - frame.size.height);
547
+
548
+ aspectVertical = YES;
549
+ aspectHorizontal = YES;
550
+ } else {
551
+ CGFloat newWidth = originFrame.size.width + xDelta;
552
+ CGFloat newHeight = originFrame.size.height - yDelta;
553
+
554
+ if (MIN(newHeight, newWidth) / MAX(newHeight, newWidth) >= (double)_minimumAspectRatio) {
555
+ frame.size.width = originFrame.size.width + xDelta;
556
+ frame.origin.y = originFrame.origin.y + yDelta;
557
+ frame.size.height = originFrame.size.height - yDelta;
558
+ }
559
+ }
560
+
561
+ clampMinFromTop = YES;
562
+
563
+ break;
564
+ case TOCropViewOverlayEdgeBottomLeft:
565
+ if (self.aspectRatioLockEnabled) {
566
+ CGPoint distance;
567
+ distance.x = 1.0f - (xDelta / CGRectGetWidth(originFrame));
568
+ distance.y = 1.0f - (-yDelta / CGRectGetHeight(originFrame));
569
+
570
+ CGFloat scale = (distance.x + distance.y) * 0.5f;
571
+
572
+ frame.size.width = ceilf(CGRectGetWidth(originFrame) * scale);
573
+ frame.size.height = ceilf(CGRectGetHeight(originFrame) * scale);
574
+ frame.origin.x = CGRectGetMaxX(originFrame) - frame.size.width;
575
+
576
+ aspectVertical = YES;
577
+ aspectHorizontal = YES;
578
+ } else {
579
+ CGFloat newWidth = originFrame.size.width - xDelta;
580
+ CGFloat newHeight = originFrame.size.height + yDelta;
581
+
582
+ if (MIN(newHeight, newWidth) / MAX(newHeight, newWidth) >= (double)_minimumAspectRatio) {
583
+ frame.size.height = originFrame.size.height + yDelta;
584
+ frame.origin.x = originFrame.origin.x + xDelta;
585
+ frame.size.width = originFrame.size.width - xDelta;
586
+ }
587
+ }
588
+
589
+ clampMinFromLeft = YES;
590
+
591
+ break;
592
+ case TOCropViewOverlayEdgeBottomRight:
593
+ if (self.aspectRatioLockEnabled) {
594
+ CGPoint distance;
595
+ distance.x = 1.0f - ((-1 * xDelta) / CGRectGetWidth(originFrame));
596
+ distance.y = 1.0f - ((-1 * yDelta) / CGRectGetHeight(originFrame));
597
+
598
+ CGFloat scale = (distance.x + distance.y) * 0.5f;
599
+
600
+ frame.size.width = ceilf(CGRectGetWidth(originFrame) * scale);
601
+ frame.size.height = ceilf(CGRectGetHeight(originFrame) * scale);
602
+
603
+ aspectVertical = YES;
604
+ aspectHorizontal = YES;
605
+ } else {
606
+ CGFloat newWidth = originFrame.size.width + xDelta;
607
+ CGFloat newHeight = originFrame.size.height + yDelta;
608
+
609
+ if (MIN(newHeight, newWidth) / MAX(newHeight, newWidth) >= (double)_minimumAspectRatio) {
610
+ frame.size.height = originFrame.size.height + yDelta;
611
+ frame.size.width = originFrame.size.width + xDelta;
612
+ }
613
+ }
614
+ break;
615
+ case TOCropViewOverlayEdgeNone:
616
+ break;
617
+ }
618
+
619
+ // The absolute max/min size the box may be in the bounds of the crop view
620
+ CGSize minSize = (CGSize){kTOCropViewMinimumBoxSize, kTOCropViewMinimumBoxSize};
621
+ CGSize maxSize = (CGSize){CGRectGetWidth(contentFrame), CGRectGetHeight(contentFrame)};
622
+
623
+ // clamp the box to ensure it doesn't go beyond the bounds we've set
624
+ if (self.aspectRatioLockEnabled && aspectHorizontal) {
625
+ maxSize.height = contentFrame.size.width / aspectRatio;
626
+ minSize.width = kTOCropViewMinimumBoxSize * aspectRatio;
627
+ }
628
+
629
+ if (self.aspectRatioLockEnabled && aspectVertical) {
630
+ maxSize.width = contentFrame.size.height * aspectRatio;
631
+ minSize.height = kTOCropViewMinimumBoxSize / aspectRatio;
632
+ }
633
+
634
+ // Clamp the width if it goes over
635
+ if (clampMinFromLeft) {
636
+ CGFloat maxWidth = CGRectGetMaxX(self.cropOriginFrame) - contentFrame.origin.x;
637
+ frame.size.width = MIN(frame.size.width, maxWidth);
638
+ }
639
+
640
+ if (clampMinFromTop) {
641
+ CGFloat maxHeight = CGRectGetMaxY(self.cropOriginFrame) - contentFrame.origin.y;
642
+ frame.size.height = MIN(frame.size.height, maxHeight);
643
+ }
644
+
645
+ // Clamp the minimum size
646
+ frame.size.width = MAX(frame.size.width, minSize.width);
647
+ frame.size.height = MAX(frame.size.height, minSize.height);
648
+
649
+ // Clamp the maximum size
650
+ frame.size.width = MIN(frame.size.width, maxSize.width);
651
+ frame.size.height = MIN(frame.size.height, maxSize.height);
652
+
653
+ // Clamp the X position of the box to the interior of the cropping bounds
654
+ frame.origin.x = MAX(frame.origin.x, CGRectGetMinX(contentFrame));
655
+ frame.origin.x = MIN(frame.origin.x, CGRectGetMaxX(contentFrame) - minSize.width);
656
+
657
+ // Clamp the Y postion of the box to the interior of the cropping bounds
658
+ frame.origin.y = MAX(frame.origin.y, CGRectGetMinY(contentFrame));
659
+ frame.origin.y = MIN(frame.origin.y, CGRectGetMaxY(contentFrame) - minSize.height);
660
+
661
+ // Once the box is completely shrunk, clamp its ability to move
662
+ if (clampMinFromLeft && frame.size.width <= minSize.width + FLT_EPSILON) {
663
+ frame.origin.x = CGRectGetMaxX(originFrame) - minSize.width;
664
+ }
665
+
666
+ // Once the box is completely shrunk, clamp its ability to move
667
+ if (clampMinFromTop && frame.size.height <= minSize.height + FLT_EPSILON) {
668
+ frame.origin.y = CGRectGetMaxY(originFrame) - minSize.height;
669
+ }
670
+
671
+ self.cropBoxFrame = frame;
672
+
673
+ [self checkForCanReset];
674
+ }
675
+
676
+ - (void)resetLayoutToDefaultAnimated:(BOOL)animated {
677
+ // If resetting the crop view includes resetting the aspect ratio,
678
+ // reset it to zero here. But set the ivar directly since there's no point
679
+ // in performing the relayout calculations right before a reset.
680
+ if (self.hasAspectRatio && self.resetAspectRatioEnabled) {
681
+ _aspectRatio = CGSizeZero;
682
+ }
683
+
684
+ if (animated == NO || self.angle != 0) {
685
+ // Reset all of the rotation transforms
686
+ _angle = 0;
687
+
688
+ // Set the scroll to 1.0f to reset the transform scale
689
+ self.scrollView.zoomScale = 1.0f;
690
+
691
+ CGRect imageRect = (CGRect){CGPointZero, self.image.size};
692
+
693
+ // Reset everything about the background container and image views
694
+ self.backgroundImageView.transform = CGAffineTransformIdentity;
695
+ self.backgroundContainerView.transform = CGAffineTransformIdentity;
696
+ self.backgroundImageView.frame = imageRect;
697
+ self.backgroundContainerView.frame = imageRect;
698
+
699
+ // Reset the transform ans size of just the foreground image
700
+ self.foregroundImageView.transform = CGAffineTransformIdentity;
701
+ self.foregroundImageView.frame = imageRect;
702
+
703
+ // Reset the layout
704
+ [self layoutInitialImage];
705
+
706
+ // Enable / Disable the reset button
707
+ [self checkForCanReset];
708
+
709
+ return;
710
+ }
711
+
712
+ // If we were in the middle of a reset timer, cancel it as we'll
713
+ // manually perform a restoration animation here
714
+ if (self.resetTimer) {
715
+ [self cancelResetTimer];
716
+ [self setEditing:NO resetCropBox:NO animated:NO];
717
+ }
718
+
719
+ [self setSimpleRenderMode:YES animated:NO];
720
+
721
+ // Perform an animation of the image zooming back out to its original size
722
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
723
+ [UIView animateWithDuration:0.5f
724
+ delay:0.0f
725
+ usingSpringWithDamping:1.0f
726
+ initialSpringVelocity:1.0f
727
+ options:UIViewAnimationOptionBeginFromCurrentState
728
+ animations:^{
729
+ [self layoutInitialImage];
730
+ }
731
+ completion:^(BOOL complete) {
732
+ [self setSimpleRenderMode:NO animated:YES];
733
+ }];
734
+ });
735
+ }
736
+
737
+ - (void)toggleTranslucencyViewVisible:(BOOL)visible {
738
+ [(UIVisualEffectView *)self.translucencyView setEffect:visible ? self.translucencyEffect : nil];
739
+ }
740
+
741
+ - (void)updateToImageCropFrame:(CGRect)imageCropframe {
742
+ // Convert the image crop frame's size from image space to the screen space
743
+ CGFloat minimumSize = self.scrollView.minimumZoomScale;
744
+ CGPoint scaledOffset = (CGPoint){imageCropframe.origin.x * minimumSize, imageCropframe.origin.y * minimumSize};
745
+ CGSize scaledCropSize = (CGSize){imageCropframe.size.width * minimumSize, imageCropframe.size.height * minimumSize};
746
+
747
+ // Work out the scale necessary to upscale the crop size to fit the content bounds of the crop bound
748
+ CGRect bounds = self.contentBounds;
749
+ CGFloat scale = MIN(bounds.size.width / scaledCropSize.width, bounds.size.height / scaledCropSize.height);
750
+
751
+ // Zoom into the scroll view to the appropriate size, transiently raising the
752
+ // ceiling if the restored crop requires more zoom than is normally allowed
753
+ // (the next layout-driven update re-derives the ceiling from the base value)
754
+ self.scrollView.maximumZoomScale = MAX(self.scrollView.maximumZoomScale, self.scrollView.minimumZoomScale * scale);
755
+ self.scrollView.zoomScale = self.scrollView.minimumZoomScale * scale;
756
+
757
+ CGSize contentSize = self.scrollView.contentSize;
758
+ self.scrollView.contentSize = CGSizeMake(floorf(contentSize.width), floorf(contentSize.height));
759
+
760
+ // Work out the size and offset of the upscaled crop box
761
+ CGRect frame = CGRectZero;
762
+ frame.size = (CGSize){floorf(scaledCropSize.width * scale), floorf(scaledCropSize.height * scale)};
763
+
764
+ // set the crop box
765
+ CGRect cropBoxFrame = CGRectZero;
766
+ cropBoxFrame.size = frame.size;
767
+ cropBoxFrame.origin.x = floorf(CGRectGetMidX(bounds) - (frame.size.width * 0.5f));
768
+ cropBoxFrame.origin.y = floorf(CGRectGetMidY(bounds) - (frame.size.height * 0.5f));
769
+ self.cropBoxFrame = cropBoxFrame;
770
+
771
+ frame.origin.x = ceilf((scaledOffset.x * scale) - self.scrollView.contentInset.left);
772
+ frame.origin.y = ceilf((scaledOffset.y * scale) - self.scrollView.contentInset.top);
773
+ self.scrollView.contentOffset = frame.origin;
774
+ }
775
+
776
+ #pragma mark - Gesture Recognizer -
777
+ - (void)gridPanGestureRecognized:(UIPanGestureRecognizer *)recognizer {
778
+ CGPoint point = [recognizer locationInView:self];
779
+
780
+ if (recognizer.state == UIGestureRecognizerStateBegan) {
781
+ [self startEditing];
782
+ self.panOriginPoint = point;
783
+ self.cropOriginFrame = self.cropBoxFrame;
784
+ self.tappedEdge = [self cropEdgeForPoint:self.panOriginPoint];
785
+ }
786
+
787
+ if (recognizer.state == UIGestureRecognizerStateEnded ||
788
+ recognizer.state == UIGestureRecognizerStateCancelled ||
789
+ recognizer.state == UIGestureRecognizerStateFailed) {
790
+ [self startResetTimer];
791
+ }
792
+
793
+ [self updateCropBoxFrameWithGesturePoint:point];
794
+ }
795
+
796
+ - (void)longPressGestureRecognized:(UILongPressGestureRecognizer *)recognizer {
797
+ if (recognizer.state == UIGestureRecognizerStateBegan)
798
+ [self.gridOverlayView setGridHidden:NO animated:YES];
799
+
800
+ if (recognizer.state == UIGestureRecognizerStateEnded)
801
+ [self.gridOverlayView setGridHidden:YES animated:YES];
802
+ }
803
+
804
+ - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
805
+ if (gestureRecognizer != self.gridPanGestureRecognizer)
806
+ return YES;
807
+
808
+ CGPoint tapPoint = [gestureRecognizer locationInView:self];
809
+
810
+ CGRect frame = self.gridOverlayView.frame;
811
+ CGRect innerFrame = CGRectInset(frame, 22.0f, 22.0f);
812
+ CGRect outerFrame = CGRectInset(frame, -22.0f, -22.0f);
813
+
814
+ if (CGRectContainsPoint(innerFrame, tapPoint) || !CGRectContainsPoint(outerFrame, tapPoint))
815
+ return NO;
816
+
817
+ return YES;
818
+ }
819
+
820
+ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
821
+ if (self.gridPanGestureRecognizer.state == UIGestureRecognizerStateChanged) {
822
+ return NO;
823
+ }
824
+ return YES;
825
+ }
826
+
827
+ #pragma mark - Timer -
828
+ - (void)startResetTimer {
829
+ if (self.resetTimer)
830
+ return;
831
+
832
+ // Use a block-based timer so a pending reset doesn't retain this view
833
+ // beyond its natural lifetime (eg, when dismissed mid-adjustment)
834
+ __weak typeof(self) weakSelf = self;
835
+ self.resetTimer = [NSTimer scheduledTimerWithTimeInterval:self.cropAdjustingDelay
836
+ repeats:NO
837
+ block:^(NSTimer *timer) {
838
+ [weakSelf timerTriggered];
839
+ }];
840
+ }
841
+
842
+ // A cancelled touch doesn't imply the interaction is over: the scroll view cancels the
843
+ // touches it delivered as a normal part of its own pan or pinch recognizer taking over,
844
+ // which happens with the user's finger still down. While one of those is underway the
845
+ // scroll view's delegate arms the timer for us once it finishes, so arming it here as
846
+ // well would trigger a reset mid-gesture. What's left is a touch cancelled while nothing
847
+ // is scrolling - an incoming call or system alert - which would otherwise leave the crop
848
+ // view stuck in its editing appearance.
849
+ - (void)handleScrollViewTouchCancellation {
850
+ if (self.scrollView.isDragging || self.scrollView.isZooming || self.scrollView.isDecelerating) {
851
+ return;
852
+ }
853
+
854
+ [self startResetTimer];
855
+ }
856
+
857
+ - (void)timerTriggered {
858
+ [self setEditing:NO resetCropBox:YES animated:YES];
859
+ [self.resetTimer invalidate];
860
+ self.resetTimer = nil;
861
+ }
862
+
863
+ - (void)cancelResetTimer {
864
+ [self.resetTimer invalidate];
865
+ self.resetTimer = nil;
866
+ }
867
+
868
+ - (TOCropViewOverlayEdge)cropEdgeForPoint:(CGPoint)point {
869
+ CGRect frame = self.cropBoxFrame;
870
+
871
+ // account for padding around the box
872
+ frame = CGRectInset(frame, -32.0f, -32.0f);
873
+
874
+ // Make sure the corners take priority
875
+ CGRect topLeftRect = (CGRect){frame.origin, {64, 64}};
876
+ if (CGRectContainsPoint(topLeftRect, point))
877
+ return TOCropViewOverlayEdgeTopLeft;
878
+
879
+ CGRect topRightRect = topLeftRect;
880
+ topRightRect.origin.x = CGRectGetMaxX(frame) - 64.0f;
881
+ if (CGRectContainsPoint(topRightRect, point))
882
+ return TOCropViewOverlayEdgeTopRight;
883
+
884
+ CGRect bottomLeftRect = topLeftRect;
885
+ bottomLeftRect.origin.y = CGRectGetMaxY(frame) - 64.0f;
886
+ if (CGRectContainsPoint(bottomLeftRect, point))
887
+ return TOCropViewOverlayEdgeBottomLeft;
888
+
889
+ CGRect bottomRightRect = topRightRect;
890
+ bottomRightRect.origin.y = bottomLeftRect.origin.y;
891
+ if (CGRectContainsPoint(bottomRightRect, point))
892
+ return TOCropViewOverlayEdgeBottomRight;
893
+
894
+ // Check for edges
895
+ CGRect topRect = (CGRect){frame.origin, {CGRectGetWidth(frame), 64.0f}};
896
+ if (CGRectContainsPoint(topRect, point))
897
+ return TOCropViewOverlayEdgeTop;
898
+
899
+ CGRect bottomRect = topRect;
900
+ bottomRect.origin.y = CGRectGetMaxY(frame) - 64.0f;
901
+ if (CGRectContainsPoint(bottomRect, point))
902
+ return TOCropViewOverlayEdgeBottom;
903
+
904
+ CGRect leftRect = (CGRect){frame.origin, {64.0f, CGRectGetHeight(frame)}};
905
+ if (CGRectContainsPoint(leftRect, point))
906
+ return TOCropViewOverlayEdgeLeft;
907
+
908
+ CGRect rightRect = leftRect;
909
+ rightRect.origin.x = CGRectGetMaxX(frame) - 64.0f;
910
+ if (CGRectContainsPoint(rightRect, point))
911
+ return TOCropViewOverlayEdgeRight;
912
+
913
+ return TOCropViewOverlayEdgeNone;
914
+ }
915
+
916
+ #pragma mark - Scroll View Delegate -
917
+
918
+ - (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
919
+ return self.backgroundContainerView;
920
+ }
921
+ - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
922
+ [self matchForegroundToBackground];
923
+ }
924
+
925
+ - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
926
+ [self startEditing];
927
+ self.canBeReset = YES;
928
+ }
929
+
930
+ - (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view {
931
+ [self startEditing];
932
+ self.canBeReset = YES;
933
+ }
934
+
935
+ - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
936
+ [self startResetTimer];
937
+ [self checkForCanReset];
938
+ }
939
+
940
+ - (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale {
941
+ [self startResetTimer];
942
+ [self checkForCanReset];
943
+ }
944
+
945
+ - (void)scrollViewDidZoom:(UIScrollView *)scrollView {
946
+ if (scrollView.isTracking) {
947
+ self.cropBoxLastEditedZoomScale = scrollView.zoomScale;
948
+ self.cropBoxLastEditedMinZoomScale = scrollView.minimumZoomScale;
949
+ self.cropBoxLastEditedMaxZoomScale = self.baseMaximumZoomScale;
950
+ }
951
+
952
+ [self matchForegroundToBackground];
953
+ }
954
+
955
+ - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
956
+ if (!decelerate)
957
+ [self startResetTimer];
958
+ }
959
+
960
+ #pragma mark - Accessors -
961
+
962
+ - (void)setCropBoxResizeEnabled:(BOOL)panResizeEnabled {
963
+ _cropBoxResizeEnabled = panResizeEnabled;
964
+ self.gridPanGestureRecognizer.enabled = _cropBoxResizeEnabled;
965
+ }
966
+
967
+ - (void)setCropBoxFrame:(CGRect)cropBoxFrame {
968
+ if (CGRectEqualToRect(cropBoxFrame, _cropBoxFrame)) {
969
+ return;
970
+ }
971
+
972
+ // Upon init, sometimes the box size is still 0 (or NaN), which can result in CALayer issues
973
+ CGSize frameSize = cropBoxFrame.size;
974
+ if (frameSize.width < FLT_EPSILON || frameSize.height < FLT_EPSILON) {
975
+ return;
976
+ }
977
+ if (isnan(frameSize.width) || isnan(frameSize.height)) {
978
+ return;
979
+ }
980
+
981
+ // clamp the cropping region to the inset boundaries of the screen
982
+ CGRect contentFrame = self.contentBounds;
983
+ CGFloat xOrigin = ceilf(contentFrame.origin.x);
984
+ CGFloat xDelta = cropBoxFrame.origin.x - xOrigin;
985
+ cropBoxFrame.origin.x = floorf(MAX(cropBoxFrame.origin.x, xOrigin));
986
+ 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)
987
+ cropBoxFrame.size.width += xDelta;
988
+
989
+ CGFloat yOrigin = ceilf(contentFrame.origin.y);
990
+ CGFloat yDelta = cropBoxFrame.origin.y - yOrigin;
991
+ cropBoxFrame.origin.y = floorf(MAX(cropBoxFrame.origin.y, yOrigin));
992
+ if (yDelta < -FLT_EPSILON)
993
+ cropBoxFrame.size.height += yDelta;
994
+
995
+ // 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
996
+ CGFloat maxWidth = (contentFrame.size.width + contentFrame.origin.x) - cropBoxFrame.origin.x;
997
+ cropBoxFrame.size.width = floorf(MIN(cropBoxFrame.size.width, maxWidth));
998
+
999
+ CGFloat maxHeight = (contentFrame.size.height + contentFrame.origin.y) - cropBoxFrame.origin.y;
1000
+ cropBoxFrame.size.height = floorf(MIN(cropBoxFrame.size.height, maxHeight));
1001
+
1002
+ // Make sure we can't make the crop box too small
1003
+ cropBoxFrame.size.width = MAX(cropBoxFrame.size.width, kTOCropViewMinimumBoxSize);
1004
+ cropBoxFrame.size.height = MAX(cropBoxFrame.size.height, kTOCropViewMinimumBoxSize);
1005
+
1006
+ _cropBoxFrame = cropBoxFrame;
1007
+
1008
+ self.foregroundContainerView.frame = _cropBoxFrame; // set the clipping view to match the new rect
1009
+ self.gridOverlayView.frame = _cropBoxFrame; // set the new overlay view to match the same region
1010
+
1011
+ // If the mask layer is present, adjust its transform to fit the new container view size
1012
+ if (self.croppingStyle == TOCropViewCroppingStyleCircular) {
1013
+ CGFloat halfWidth = self.foregroundContainerView.frame.size.width * 0.5f;
1014
+ self.foregroundContainerView.layer.cornerRadius = halfWidth;
1015
+ }
1016
+
1017
+ // reset the scroll view insets to match the region of the new crop rect
1018
+ self.scrollView.contentInset = (UIEdgeInsets){CGRectGetMinY(_cropBoxFrame),
1019
+ CGRectGetMinX(_cropBoxFrame),
1020
+ CGRectGetMaxY(self.bounds) - CGRectGetMaxY(_cropBoxFrame),
1021
+ CGRectGetMaxX(self.bounds) - CGRectGetMaxX(_cropBoxFrame)};
1022
+
1023
+ // if necessary, work out the new minimum size of the scroll view so it fills the crop box
1024
+ CGSize imageSize = self.backgroundContainerView.bounds.size;
1025
+ CGFloat scale = MAX(cropBoxFrame.size.height / imageSize.height, cropBoxFrame.size.width / imageSize.width);
1026
+ self.scrollView.minimumZoomScale = scale;
1027
+ [self updateScrollViewMaximumZoomScale];
1028
+
1029
+ // make sure content isn't smaller than the crop box
1030
+ CGSize size = self.scrollView.contentSize;
1031
+ size.width = floorf(size.width);
1032
+ size.height = floorf(size.height);
1033
+ self.scrollView.contentSize = size;
1034
+
1035
+ // IMPORTANT: Force the scroll view to update its content after changing the zoom scale
1036
+ self.scrollView.zoomScale = self.scrollView.zoomScale;
1037
+
1038
+ [self matchForegroundToBackground]; // re-align the background content to match
1039
+ }
1040
+
1041
+ - (void)setEditing:(BOOL)editing {
1042
+ [self setEditing:editing resetCropBox:NO animated:NO];
1043
+ }
1044
+
1045
+ - (void)setSimpleRenderMode:(BOOL)simpleMode {
1046
+ [self setSimpleRenderMode:simpleMode animated:NO];
1047
+ }
1048
+
1049
+ - (BOOL)cropBoxAspectRatioIsPortrait {
1050
+ CGRect cropFrame = self.cropBoxFrame;
1051
+ return CGRectGetWidth(cropFrame) < CGRectGetHeight(cropFrame);
1052
+ }
1053
+
1054
+ - (CGRect)imageCropFrame {
1055
+ CGSize imageSize = self.imageSize;
1056
+ CGSize contentSize = self.scrollView.contentSize;
1057
+ CGRect cropBoxFrame = self.cropBoxFrame;
1058
+ CGPoint contentOffset = self.scrollView.contentOffset;
1059
+ UIEdgeInsets edgeInsets = self.scrollView.contentInset;
1060
+ CGFloat scale = MIN(imageSize.width / contentSize.width, imageSize.height / contentSize.height);
1061
+
1062
+ CGRect frame = CGRectZero;
1063
+
1064
+ // Calculate the normalized origin, clamped inside the image so the size
1065
+ // subtraction below can never go negative (eg, during rubber-band overscroll)
1066
+ frame.origin.x = floorf((floorf(contentOffset.x) + edgeInsets.left) * (imageSize.width / contentSize.width));
1067
+ frame.origin.x = MAX(0, MIN(frame.origin.x, imageSize.width));
1068
+
1069
+ frame.origin.y = floorf((floorf(contentOffset.y) + edgeInsets.top) * (imageSize.height / contentSize.height));
1070
+ frame.origin.y = MAX(0, MIN(frame.origin.y, imageSize.height));
1071
+
1072
+ // Calculate the normalized width, clamped so the rect never extends past the image
1073
+ frame.size.width = ceilf(cropBoxFrame.size.width * scale);
1074
+ frame.size.width = MIN(imageSize.width - frame.origin.x, frame.size.width);
1075
+
1076
+ // Calculate normalized height
1077
+ if (floor(cropBoxFrame.size.width) == floor(cropBoxFrame.size.height)) {
1078
+ frame.size.height = frame.size.width;
1079
+ } else {
1080
+ frame.size.height = ceilf(cropBoxFrame.size.height * scale);
1081
+ }
1082
+ frame.size.height = MIN(imageSize.height - frame.origin.y, frame.size.height);
1083
+
1084
+ return frame;
1085
+ }
1086
+
1087
+ - (void)setImageCropFrame:(CGRect)imageCropFrame {
1088
+ if (!self.initialSetupPerformed) {
1089
+ self.restoreImageCropFrame = imageCropFrame;
1090
+ return;
1091
+ }
1092
+
1093
+ [self updateToImageCropFrame:imageCropFrame];
1094
+ }
1095
+
1096
+ - (void)setCroppingViewsHidden:(BOOL)hidden {
1097
+ [self setCroppingViewsHidden:hidden animated:NO];
1098
+ }
1099
+
1100
+ - (void)setCroppingViewsHidden:(BOOL)hidden animated:(BOOL)animated {
1101
+ if (_croppingViewsHidden == hidden)
1102
+ return;
1103
+
1104
+ _croppingViewsHidden = hidden;
1105
+
1106
+ CGFloat alpha = hidden ? 0.0f : 1.0f;
1107
+
1108
+ if (animated == NO) {
1109
+ self.backgroundImageView.alpha = alpha;
1110
+ self.foregroundContainerView.alpha = alpha;
1111
+ self.gridOverlayView.alpha = alpha;
1112
+
1113
+ [self toggleTranslucencyViewVisible:!hidden];
1114
+
1115
+ return;
1116
+ }
1117
+
1118
+ self.foregroundContainerView.alpha = alpha;
1119
+ self.backgroundImageView.alpha = alpha;
1120
+
1121
+ [UIView animateWithDuration:0.4f
1122
+ animations:^{
1123
+ [self toggleTranslucencyViewVisible:!hidden];
1124
+ self.gridOverlayView.alpha = alpha;
1125
+ }];
1126
+ }
1127
+
1128
+ - (void)setBackgroundImageViewHidden:(BOOL)hidden animated:(BOOL)animated {
1129
+ if (animated == NO) {
1130
+ self.backgroundImageView.hidden = hidden;
1131
+ return;
1132
+ }
1133
+
1134
+ CGFloat beforeAlpha = hidden ? 1.0f : 0.0f;
1135
+ CGFloat toAlpha = hidden ? 0.0f : 1.0f;
1136
+
1137
+ self.backgroundImageView.hidden = NO;
1138
+ self.backgroundImageView.alpha = beforeAlpha;
1139
+ [UIView animateWithDuration:0.5f
1140
+ animations:^{
1141
+ self.backgroundImageView.alpha = toAlpha;
1142
+ }
1143
+ completion:^(BOOL complete) {
1144
+ if (hidden) {
1145
+ self.backgroundImageView.hidden = YES;
1146
+ }
1147
+ }];
1148
+ }
1149
+
1150
+ - (void)setAlwaysShowCroppingGrid:(BOOL)alwaysShowCroppingGrid {
1151
+ if (alwaysShowCroppingGrid == _alwaysShowCroppingGrid) {
1152
+ return;
1153
+ }
1154
+ _alwaysShowCroppingGrid = alwaysShowCroppingGrid;
1155
+ [self.gridOverlayView setGridHidden:!_alwaysShowCroppingGrid animated:YES];
1156
+ }
1157
+
1158
+ - (void)setTranslucencyAlwaysHidden:(BOOL)translucencyAlwaysHidden {
1159
+ if (_translucencyAlwaysHidden == translucencyAlwaysHidden) {
1160
+ return;
1161
+ }
1162
+ _translucencyAlwaysHidden = translucencyAlwaysHidden;
1163
+ self.translucencyView.hidden = _translucencyAlwaysHidden;
1164
+ }
1165
+
1166
+ - (void)setGridOverlayHidden:(BOOL)gridOverlayHidden {
1167
+ [self setGridOverlayHidden:gridOverlayHidden animated:NO];
1168
+ }
1169
+
1170
+ - (void)setGridOverlayHidden:(BOOL)gridOverlayHidden animated:(BOOL)animated {
1171
+ _gridOverlayHidden = gridOverlayHidden;
1172
+
1173
+ if (!animated) {
1174
+ self.gridOverlayView.alpha = gridOverlayHidden ? 0.0f : 1.0f;
1175
+ return;
1176
+ }
1177
+
1178
+ self.gridOverlayView.alpha = gridOverlayHidden ? 1.0f : 0.0f;
1179
+ [UIView animateWithDuration:0.4f
1180
+ animations:^{
1181
+ self.gridOverlayView.alpha = gridOverlayHidden ? 0.0f : 1.0f;
1182
+ }];
1183
+ }
1184
+
1185
+ - (CGRect)imageViewFrame {
1186
+ CGRect frame = CGRectZero;
1187
+ frame.origin.x = -self.scrollView.contentOffset.x;
1188
+ frame.origin.y = -self.scrollView.contentOffset.y;
1189
+ frame.size = self.scrollView.contentSize;
1190
+ return frame;
1191
+ }
1192
+
1193
+ - (void)setCanBeReset:(BOOL)canReset {
1194
+ if (canReset == _canBeReset) {
1195
+ return;
1196
+ }
1197
+
1198
+ _canBeReset = canReset;
1199
+
1200
+ if (canReset) {
1201
+ if ([self.delegate respondsToSelector:@selector(cropViewDidBecomeResettable:)])
1202
+ [self.delegate cropViewDidBecomeResettable:self];
1203
+ } else {
1204
+ if ([self.delegate respondsToSelector:@selector(cropViewDidBecomeNonResettable:)])
1205
+ [self.delegate cropViewDidBecomeNonResettable:self];
1206
+ }
1207
+ }
1208
+
1209
+ - (void)setAngle:(NSInteger)angle {
1210
+ // Only multiples of 90 degrees are supported
1211
+ NSInteger newAngle = angle;
1212
+ if (angle % 90 != 0) {
1213
+ newAngle = 0;
1214
+ }
1215
+
1216
+ // Normalize to the (-360, 360) range the rotation logic produces
1217
+ // (eg, 360 wraps back around to 0), preserving direction
1218
+ newAngle %= 360;
1219
+
1220
+ // The initial layout would not have been performed yet.
1221
+ // Save the value and it will be applied when it has
1222
+ if (!self.initialSetupPerformed) {
1223
+ self.restoreAngle = newAngle;
1224
+ return;
1225
+ }
1226
+
1227
+ // Negative values are allowed, so rotate clockwise or counter clockwise depending
1228
+ // on direction. Compare the signed values since +90 and -90 are distinct states.
1229
+ const BOOL clockwise = (newAngle >= 0);
1230
+ while (self.angle != newAngle) {
1231
+ const NSInteger previousAngle = self.angle;
1232
+ [self rotateImageNinetyDegreesAnimated:NO clockwise:clockwise completion:nil];
1233
+
1234
+ // Bail out if no rotation was performed (eg, an animated rotation is
1235
+ // still in flight) rather than spinning forever on the main thread
1236
+ if (self.angle == previousAngle) {
1237
+ break;
1238
+ }
1239
+ }
1240
+ }
1241
+
1242
+ #pragma mark - Editing Mode -
1243
+ - (void)startEditing {
1244
+ [self cancelResetTimer];
1245
+ [self setEditing:YES resetCropBox:NO animated:YES];
1246
+ }
1247
+
1248
+ - (void)setEditing:(BOOL)editing resetCropBox:(BOOL)resetCropbox animated:(BOOL)animated {
1249
+ if (editing == _editing)
1250
+ return;
1251
+
1252
+ _editing = editing;
1253
+
1254
+ // Toggle the visiblity of the gridlines when not editing
1255
+ BOOL hidden = !_editing;
1256
+ if (self.alwaysShowCroppingGrid) {
1257
+ hidden = NO;
1258
+ } // Override this if the user requires
1259
+ [self.gridOverlayView setGridHidden:hidden animated:animated];
1260
+
1261
+ if (resetCropbox) {
1262
+ [self moveCroppedContentToCenterAnimated:animated];
1263
+ [self captureStateForImageRotation];
1264
+ self.cropBoxLastEditedAngle = self.angle;
1265
+ }
1266
+
1267
+ if (animated == NO) {
1268
+ [self toggleTranslucencyViewVisible:!editing];
1269
+ return;
1270
+ }
1271
+
1272
+ CGFloat duration = editing ? 0.05f : 0.35f;
1273
+ CGFloat delay = editing ? 0.0f : 0.35f;
1274
+
1275
+ if (self.croppingStyle == TOCropViewCroppingStyleCircular) {
1276
+ delay = 0.0f;
1277
+ }
1278
+
1279
+ [UIView animateKeyframesWithDuration:duration
1280
+ delay:delay
1281
+ options:0
1282
+ animations:^{
1283
+ [self toggleTranslucencyViewVisible:!editing];
1284
+ }
1285
+ completion:nil];
1286
+ }
1287
+
1288
+ - (void)moveCroppedContentToCenterAnimated:(BOOL)animated {
1289
+ if (self.internalLayoutDisabled)
1290
+ return;
1291
+
1292
+ CGRect contentRect = self.contentBounds;
1293
+ CGRect cropFrame = self.cropBoxFrame;
1294
+
1295
+ // Ensure we only proceed after the crop frame has been setup for the first time
1296
+ if (cropFrame.size.width < FLT_EPSILON || cropFrame.size.height < FLT_EPSILON) {
1297
+ return;
1298
+ }
1299
+
1300
+ // The scale we need to scale up the crop box to fit full screen
1301
+ CGFloat scale = MIN(CGRectGetWidth(contentRect) / CGRectGetWidth(cropFrame), CGRectGetHeight(contentRect) / CGRectGetHeight(cropFrame));
1302
+
1303
+ CGPoint focusPoint = (CGPoint){CGRectGetMidX(cropFrame), CGRectGetMidY(cropFrame)};
1304
+ CGPoint midPoint = (CGPoint){CGRectGetMidX(contentRect), CGRectGetMidY(contentRect)};
1305
+
1306
+ cropFrame.size.width = ceilf(cropFrame.size.width * scale);
1307
+ cropFrame.size.height = ceilf(cropFrame.size.height * scale);
1308
+ cropFrame.origin.x = contentRect.origin.x + ceilf((contentRect.size.width - cropFrame.size.width) * 0.5f);
1309
+ cropFrame.origin.y = contentRect.origin.y + ceilf((contentRect.size.height - cropFrame.size.height) * 0.5f);
1310
+
1311
+ // Work out the point on the scroll content that the focusPoint is aiming at
1312
+ CGPoint contentTargetPoint = CGPointZero;
1313
+ contentTargetPoint.x = ((focusPoint.x + self.scrollView.contentOffset.x) * scale);
1314
+ contentTargetPoint.y = ((focusPoint.y + self.scrollView.contentOffset.y) * scale);
1315
+
1316
+ // Work out where the crop box is focusing, so we can re-align to center that point
1317
+ __block CGPoint offset = CGPointZero;
1318
+ offset.x = -midPoint.x + contentTargetPoint.x;
1319
+ offset.y = -midPoint.y + contentTargetPoint.y;
1320
+
1321
+ // clamp the content so it doesn't create any seams around the grid
1322
+ offset.x = MAX(-cropFrame.origin.x, offset.x);
1323
+ offset.y = MAX(-cropFrame.origin.y, offset.y);
1324
+
1325
+ __weak typeof(self) weakSelf = self;
1326
+ void (^translateBlock)(void) = ^{
1327
+ typeof(self) strongSelf = weakSelf;
1328
+
1329
+ // Setting these scroll view properties will trigger
1330
+ // the foreground matching method via their delegates,
1331
+ // multiple times inside the same animation block, resulting
1332
+ // in glitchy animations.
1333
+ //
1334
+ // Disable matching for now, and explicitly update at the end.
1335
+ strongSelf.disableForgroundMatching = YES;
1336
+ {
1337
+ // Slight hack. This method needs to be called during `[UIViewController viewDidLayoutSubviews]`
1338
+ // in order for the crop view to resize itself during iPad split screen events.
1339
+ // On the first run, even though scale is exactly 1.0f, performing this multiplication introduces
1340
+ // a floating point noise that zooms the image in by about 5 pixels. This fixes that issue.
1341
+ if (scale < 1.0f - FLT_EPSILON || scale > 1.0f + FLT_EPSILON) {
1342
+ strongSelf.scrollView.zoomScale *= scale;
1343
+ strongSelf.scrollView.zoomScale = MIN(strongSelf.scrollView.maximumZoomScale, strongSelf.scrollView.zoomScale);
1344
+ }
1345
+
1346
+ // If it turns out the zoom operation would have exceeded the minizum zoom scale, don't apply
1347
+ // the content offset
1348
+ if (strongSelf.scrollView.zoomScale < strongSelf.scrollView.maximumZoomScale - FLT_EPSILON) {
1349
+ offset.x = MIN(-CGRectGetMaxX(cropFrame) + strongSelf.scrollView.contentSize.width, offset.x);
1350
+ offset.y = MIN(-CGRectGetMaxY(cropFrame) + strongSelf.scrollView.contentSize.height, offset.y);
1351
+ strongSelf.scrollView.contentOffset = offset;
1352
+ }
1353
+
1354
+ strongSelf.cropBoxFrame = cropFrame;
1355
+ }
1356
+ strongSelf.disableForgroundMatching = NO;
1357
+
1358
+ // Explicitly update the matching at the end of the calculations
1359
+ [strongSelf matchForegroundToBackground];
1360
+ };
1361
+
1362
+ if (!animated) {
1363
+ translateBlock();
1364
+ return;
1365
+ }
1366
+
1367
+ [self matchForegroundToBackground];
1368
+
1369
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
1370
+ [UIView animateWithDuration:0.5f
1371
+ delay:0.0f
1372
+ usingSpringWithDamping:1.0f
1373
+ initialSpringVelocity:1.0f
1374
+ options:UIViewAnimationOptionBeginFromCurrentState
1375
+ animations:translateBlock
1376
+ completion:nil];
1377
+ });
1378
+ }
1379
+
1380
+ - (void)setSimpleRenderMode:(BOOL)simpleMode animated:(BOOL)animated {
1381
+ if (simpleMode == _simpleRenderMode)
1382
+ return;
1383
+
1384
+ _simpleRenderMode = simpleMode;
1385
+
1386
+ self.editing = NO;
1387
+
1388
+ if (animated == NO) {
1389
+ [self toggleTranslucencyViewVisible:!simpleMode];
1390
+
1391
+ return;
1392
+ }
1393
+
1394
+ [UIView animateWithDuration:0.25f
1395
+ animations:^{
1396
+ [self toggleTranslucencyViewVisible:!simpleMode];
1397
+ }];
1398
+ }
1399
+
1400
+ - (void)setAspectRatio:(CGSize)aspectRatio {
1401
+ [self setAspectRatio:aspectRatio animated:NO];
1402
+ }
1403
+
1404
+ - (void)setAspectRatio:(CGSize)aspectRatio animated:(BOOL)animated {
1405
+ _aspectRatio = aspectRatio;
1406
+
1407
+ // Will be executed automatically when added to a super view
1408
+ if (!self.initialSetupPerformed) {
1409
+ return;
1410
+ }
1411
+
1412
+ BOOL zoomOut = NO;
1413
+
1414
+ // Passing in an empty size (or one with a zero component) will revert back to the image aspect ratio
1415
+ if (aspectRatio.width < FLT_EPSILON || aspectRatio.height < FLT_EPSILON) {
1416
+ aspectRatio = (CGSize){self.imageSize.width, self.imageSize.height};
1417
+ zoomOut = YES; // Prevent from steadily zooming in when cycling between alternate aspectRatios and original
1418
+ }
1419
+
1420
+ CGRect boundsFrame = self.contentBounds;
1421
+ CGRect cropBoxFrame = self.cropBoxFrame;
1422
+ CGPoint offset = self.scrollView.contentOffset;
1423
+
1424
+ BOOL cropBoxIsPortrait = NO;
1425
+ if ((NSInteger)aspectRatio.width == 1 && (NSInteger)aspectRatio.height == 1)
1426
+ cropBoxIsPortrait = self.image.size.width > self.image.size.height;
1427
+ else
1428
+ cropBoxIsPortrait = aspectRatio.width < aspectRatio.height;
1429
+
1430
+ // OneKey OK-51551: size the crop box from the content bounds. Upstream
1431
+ // derives it from the current crop box, which shrinks a little more on
1432
+ // every rotation because the rotated box is scaled down to fit.
1433
+ CGFloat boundsWidth = CGRectGetWidth(boundsFrame);
1434
+ CGFloat boundsHeight = CGRectGetHeight(boundsFrame);
1435
+
1436
+ if (cropBoxIsPortrait) {
1437
+ CGFloat targetHeight = boundsHeight;
1438
+ CGFloat newWidth = floorf(targetHeight * (aspectRatio.width / aspectRatio.height));
1439
+
1440
+ CGFloat deltaH = cropBoxFrame.size.height - targetHeight;
1441
+ cropBoxFrame.size.height = targetHeight;
1442
+ offset.y += (deltaH * 0.5f);
1443
+
1444
+ if (newWidth > boundsWidth) {
1445
+ CGFloat scale = boundsWidth / newWidth;
1446
+ CGFloat scaledHeight = targetHeight * scale;
1447
+ CGFloat deltaH2 = cropBoxFrame.size.height - scaledHeight;
1448
+ cropBoxFrame.size.height = scaledHeight;
1449
+ offset.y += (deltaH2 * 0.5f);
1450
+ newWidth = boundsWidth;
1451
+ }
1452
+
1453
+ CGFloat deltaW = cropBoxFrame.size.width - newWidth;
1454
+ cropBoxFrame.size.width = newWidth;
1455
+ offset.x += (deltaW * 0.5f);
1456
+ zoomOut = YES;
1457
+ } else {
1458
+ CGFloat targetWidth = boundsWidth;
1459
+ CGFloat newHeight = floorf(targetWidth * (aspectRatio.height / aspectRatio.width));
1460
+
1461
+ CGFloat deltaW = cropBoxFrame.size.width - targetWidth;
1462
+ cropBoxFrame.size.width = targetWidth;
1463
+ offset.x += (deltaW * 0.5f);
1464
+
1465
+ if (newHeight > boundsHeight) {
1466
+ CGFloat scale = boundsHeight / newHeight;
1467
+ CGFloat scaledWidth = targetWidth * scale;
1468
+ CGFloat deltaW2 = cropBoxFrame.size.width - scaledWidth;
1469
+ cropBoxFrame.size.width = scaledWidth;
1470
+ offset.x += (deltaW2 * 0.5f);
1471
+ newHeight = boundsHeight;
1472
+ }
1473
+
1474
+ CGFloat deltaH = cropBoxFrame.size.height - newHeight;
1475
+ cropBoxFrame.size.height = newHeight;
1476
+ offset.y += (deltaH * 0.5f);
1477
+ zoomOut = YES;
1478
+ }
1479
+
1480
+ self.cropBoxLastEditedSize = cropBoxFrame.size;
1481
+ self.cropBoxLastEditedAngle = self.angle;
1482
+
1483
+ void (^translateBlock)(void) = ^{
1484
+ self.scrollView.contentOffset = offset;
1485
+ self.cropBoxFrame = cropBoxFrame;
1486
+
1487
+ if (zoomOut) {
1488
+ self.scrollView.zoomScale = self.scrollView.minimumZoomScale;
1489
+ }
1490
+
1491
+ [self moveCroppedContentToCenterAnimated:NO];
1492
+ [self checkForCanReset];
1493
+ };
1494
+
1495
+ if (animated == NO) {
1496
+ translateBlock();
1497
+ return;
1498
+ }
1499
+
1500
+ [UIView animateWithDuration:0.5f
1501
+ delay:0.0
1502
+ usingSpringWithDamping:1.0f
1503
+ initialSpringVelocity:0.7f
1504
+ options:UIViewAnimationOptionBeginFromCurrentState
1505
+ animations:translateBlock
1506
+ completion:nil];
1507
+ }
1508
+
1509
+ - (void)rotateImageNinetyDegreesAnimated:(BOOL)animated completion:(void (^)(BOOL completed))completionHandler {
1510
+ [self rotateImageNinetyDegreesAnimated:animated clockwise:NO completion:completionHandler];
1511
+ }
1512
+
1513
+ - (void)rotateImageNinetyDegreesAnimated:(BOOL)animated clockwise:(BOOL)clockwise completion:(void (^)(BOOL completed))completionHandler {
1514
+ // Only allow one rotation animation at a time. Report the dropped call rather
1515
+ // than swallowing the handler, or callers that gate UI on it (such as the
1516
+ // toolbar's own rotation buttons) would stay disabled forever.
1517
+ if (self.rotateAnimationInProgress) {
1518
+ if (completionHandler) {
1519
+ completionHandler(NO);
1520
+ }
1521
+ return;
1522
+ }
1523
+
1524
+ // Cancel any pending resizing timers
1525
+ if (self.resetTimer) {
1526
+ [self cancelResetTimer];
1527
+ [self setEditing:NO resetCropBox:YES animated:NO];
1528
+
1529
+ self.cropBoxLastEditedAngle = self.angle;
1530
+ [self captureStateForImageRotation];
1531
+ }
1532
+
1533
+ // Work out the new angle, and wrap around once we exceed 360s
1534
+ NSInteger newAngle = self.angle;
1535
+ newAngle = clockwise ? newAngle + 90 : newAngle - 90;
1536
+ if (newAngle <= -360 || newAngle >= 360) {
1537
+ newAngle = 0;
1538
+ }
1539
+
1540
+ _angle = newAngle;
1541
+
1542
+ // Convert the new angle to radians
1543
+ CGFloat angleInRadians = 0.0f;
1544
+ switch (newAngle) {
1545
+ case 90:
1546
+ angleInRadians = M_PI_2;
1547
+ break;
1548
+ case -90:
1549
+ angleInRadians = -M_PI_2;
1550
+ break;
1551
+ case 180:
1552
+ angleInRadians = M_PI;
1553
+ break;
1554
+ case -180:
1555
+ angleInRadians = -M_PI;
1556
+ break;
1557
+ case 270:
1558
+ angleInRadians = (M_PI + M_PI_2);
1559
+ break;
1560
+ case -270:
1561
+ angleInRadians = -(M_PI + M_PI_2);
1562
+ break;
1563
+ default:
1564
+ break;
1565
+ }
1566
+
1567
+ // Set up the transformation matrix for the rotation
1568
+ CGAffineTransform rotation = CGAffineTransformRotate(CGAffineTransformIdentity, angleInRadians);
1569
+
1570
+ // Work out how much we'll need to scale everything to fit to the new rotation
1571
+ CGRect contentBounds = self.contentBounds;
1572
+ CGRect cropBoxFrame = self.cropBoxFrame;
1573
+ CGFloat scale = MIN(contentBounds.size.width / cropBoxFrame.size.height, contentBounds.size.height / cropBoxFrame.size.width);
1574
+
1575
+ // Work out which section of the image we're currently focusing at
1576
+ CGPoint cropMidPoint = (CGPoint){CGRectGetMidX(cropBoxFrame), CGRectGetMidY(cropBoxFrame)};
1577
+ CGPoint cropTargetPoint = (CGPoint){cropMidPoint.x + self.scrollView.contentOffset.x, cropMidPoint.y + self.scrollView.contentOffset.y};
1578
+
1579
+ // Work out the dimensions of the crop box when rotated
1580
+ const CGFloat oldZoomScale = self.scrollView.zoomScale;
1581
+ CGRect newCropFrame = CGRectZero;
1582
+ if (labs(self.angle) == labs(self.cropBoxLastEditedAngle) || (labs(self.angle) * -1) == ((labs(self.cropBoxLastEditedAngle) - 180) % 360)) {
1583
+ newCropFrame.size = self.cropBoxLastEditedSize;
1584
+
1585
+ // Restore the full zoom state, ceiling first so the saved zoom isn't clamped
1586
+ self.baseMaximumZoomScale = self.cropBoxLastEditedMaxZoomScale;
1587
+ self.scrollView.minimumZoomScale = self.cropBoxLastEditedMinZoomScale;
1588
+ [self updateScrollViewMaximumZoomScale];
1589
+ self.scrollView.zoomScale = self.cropBoxLastEditedZoomScale;
1590
+ } else {
1591
+ newCropFrame.size = (CGSize){floorf(self.cropBoxFrame.size.height * scale), floorf(self.cropBoxFrame.size.width * scale)};
1592
+
1593
+ // Re-adjust the scrolling dimensions of the scroll view to match the new size
1594
+ self.scrollView.minimumZoomScale *= scale;
1595
+ self.baseMaximumZoomScale *= scale;
1596
+ [self updateScrollViewMaximumZoomScale];
1597
+ self.scrollView.zoomScale *= scale;
1598
+ }
1599
+
1600
+ newCropFrame.origin.x = floorf(CGRectGetMidX(contentBounds) - (newCropFrame.size.width * 0.5f));
1601
+ newCropFrame.origin.y = floorf(CGRectGetMidY(contentBounds) - (newCropFrame.size.height * 0.5f));
1602
+
1603
+ // If we're animated, generate a snapshot view that we'll animate in place of the real view
1604
+ UIView *snapshotView = nil;
1605
+ if (animated) {
1606
+ snapshotView = [self.foregroundContainerView snapshotViewAfterScreenUpdates:NO];
1607
+ self.rotateAnimationInProgress = YES;
1608
+ }
1609
+
1610
+ // Rotate the background image view, inside its container view
1611
+ self.backgroundImageView.transform = rotation;
1612
+
1613
+ // Flip the width/height of the container view so it matches the rotated image view's size
1614
+ CGSize containerSize = self.backgroundContainerView.frame.size;
1615
+ self.backgroundContainerView.frame = (CGRect){CGPointZero, {containerSize.height, containerSize.width}};
1616
+ self.backgroundImageView.frame = (CGRect){CGPointZero, self.backgroundImageView.frame.size};
1617
+
1618
+ // Rotate the foreground image view to match
1619
+ self.foregroundContainerView.transform = CGAffineTransformIdentity;
1620
+ self.foregroundImageView.transform = rotation;
1621
+
1622
+ // Flip the content size of the scroll view to match the rotated bounds
1623
+ self.scrollView.contentSize = self.backgroundContainerView.frame.size;
1624
+
1625
+ // assign the new crop box frame and re-adjust the content to fill it
1626
+ self.cropBoxFrame = newCropFrame;
1627
+ [self moveCroppedContentToCenterAnimated:NO];
1628
+ newCropFrame = self.cropBoxFrame;
1629
+
1630
+ // work out how to line up out point of interest into the middle of the crop box.
1631
+ // Use the zoom delta that was actually applied (including any adjustment made while
1632
+ // re-centering above), which differs from the geometric scale when a previously
1633
+ // edited crop state was restored
1634
+ const CGFloat appliedScale = (oldZoomScale > FLT_EPSILON) ? (self.scrollView.zoomScale / oldZoomScale) : scale;
1635
+ cropTargetPoint.x *= appliedScale;
1636
+ cropTargetPoint.y *= appliedScale;
1637
+
1638
+ // swap the target dimensions to match a 90 degree rotation (clockwise or counterclockwise)
1639
+ CGFloat swap = cropTargetPoint.x;
1640
+ if (clockwise) {
1641
+ cropTargetPoint.x = self.scrollView.contentSize.width - cropTargetPoint.y;
1642
+ cropTargetPoint.y = swap;
1643
+ } else {
1644
+ cropTargetPoint.x = cropTargetPoint.y;
1645
+ cropTargetPoint.y = self.scrollView.contentSize.height - swap;
1646
+ }
1647
+
1648
+ // reapply the translated scroll offset to the scroll view
1649
+ CGPoint midPoint = {CGRectGetMidX(newCropFrame), CGRectGetMidY(newCropFrame)};
1650
+ CGPoint offset = CGPointZero;
1651
+ offset.x = floorf(-midPoint.x + cropTargetPoint.x);
1652
+ offset.y = floorf(-midPoint.y + cropTargetPoint.y);
1653
+ offset.x = MAX(-self.scrollView.contentInset.left, offset.x);
1654
+ offset.y = MAX(-self.scrollView.contentInset.top, offset.y);
1655
+ offset.x = MIN(self.scrollView.contentSize.width - CGRectGetMaxX(newCropFrame), offset.x);
1656
+ offset.y = MIN(self.scrollView.contentSize.height - CGRectGetMaxY(newCropFrame), offset.y);
1657
+
1658
+ // if the scroll view's new scale is 1 and the new offset is equal to the old, will not trigger the delegate 'scrollViewDidScroll:'
1659
+ // so we should call the method manually to update the foregroundImageView's frame
1660
+ if (offset.x == self.scrollView.contentOffset.x && offset.y == self.scrollView.contentOffset.y && fabs(appliedScale - 1.0) < FLT_EPSILON) {
1661
+ [self matchForegroundToBackground];
1662
+ }
1663
+ self.scrollView.contentOffset = offset;
1664
+
1665
+ // If we're animated, play an animation of the snapshot view rotating,
1666
+ // then fade it out over the live content
1667
+ if (animated) {
1668
+ snapshotView.center = (CGPoint){CGRectGetMidX(contentBounds), CGRectGetMidY(contentBounds)};
1669
+ [self addSubview:snapshotView];
1670
+
1671
+ self.backgroundContainerView.hidden = YES;
1672
+ self.foregroundContainerView.hidden = YES;
1673
+ self.translucencyView.hidden = YES;
1674
+ self.gridOverlayView.hidden = YES;
1675
+
1676
+ [UIView animateWithDuration:0.45f
1677
+ delay:0.0f
1678
+ usingSpringWithDamping:1.0f
1679
+ initialSpringVelocity:0.8f
1680
+ options:UIViewAnimationOptionBeginFromCurrentState
1681
+ animations:^{
1682
+ CGAffineTransform transform = CGAffineTransformRotate(CGAffineTransformIdentity, clockwise ? M_PI_2 : -M_PI_2);
1683
+ transform = CGAffineTransformScale(transform, scale, scale);
1684
+ snapshotView.transform = transform;
1685
+ }
1686
+ completion:^(BOOL complete) {
1687
+ self.backgroundContainerView.hidden = NO;
1688
+ self.foregroundContainerView.hidden = NO;
1689
+ self.translucencyView.hidden = self.translucencyAlwaysHidden;
1690
+ self.gridOverlayView.hidden = NO;
1691
+
1692
+ self.backgroundContainerView.alpha = 0.0f;
1693
+ self.gridOverlayView.alpha = 0.0f;
1694
+
1695
+ self.translucencyView.alpha = 1.0f;
1696
+
1697
+ [UIView animateWithDuration:0.45f
1698
+ animations:^{
1699
+ snapshotView.alpha = 0.0f;
1700
+ self.backgroundContainerView.alpha = 1.0f;
1701
+ self.gridOverlayView.alpha = 1.0f;
1702
+ }
1703
+ completion:^(BOOL complete) {
1704
+ self.rotateAnimationInProgress = NO;
1705
+ [snapshotView removeFromSuperview];
1706
+
1707
+ // If the aspect ratio lock is not enabled, allow a swap
1708
+ // If the aspect ratio lock is on, allow a aspect ratio swap
1709
+ // only if the allowDimensionSwap option is specified.
1710
+ BOOL aspectRatioCanSwapDimensions = !self.aspectRatioLockEnabled ||
1711
+ (self.aspectRatioLockEnabled && self.aspectRatioLockDimensionSwapEnabled);
1712
+
1713
+ if (!aspectRatioCanSwapDimensions) {
1714
+ // This will animate the aspect ratio back to the desired locked ratio after the image is rotated.
1715
+ [self setAspectRatio:self.aspectRatio animated:animated];
1716
+ }
1717
+
1718
+ if (completionHandler) {
1719
+ completionHandler(complete);
1720
+ }
1721
+ }];
1722
+ }];
1723
+ }
1724
+
1725
+ [self checkForCanReset];
1726
+
1727
+ // The un-animated path has already finished by this point, so there's no
1728
+ // animation completion block to defer the handler to
1729
+ if (!animated && completionHandler) {
1730
+ completionHandler(YES);
1731
+ }
1732
+ }
1733
+
1734
+ - (void)captureStateForImageRotation {
1735
+ self.cropBoxLastEditedSize = self.cropBoxFrame.size;
1736
+ self.cropBoxLastEditedZoomScale = self.scrollView.zoomScale;
1737
+ self.cropBoxLastEditedMinZoomScale = self.scrollView.minimumZoomScale;
1738
+ self.cropBoxLastEditedMaxZoomScale = self.baseMaximumZoomScale;
1739
+ }
1740
+
1741
+ // The scroll view's zoom ceiling normally sits at the layout's absolute base value,
1742
+ // but must never fall below the minimum or UIScrollView clamps the zoom beneath it
1743
+ - (void)updateScrollViewMaximumZoomScale {
1744
+ self.scrollView.maximumZoomScale = MAX(self.scrollView.minimumZoomScale, self.baseMaximumZoomScale);
1745
+ }
1746
+
1747
+ #pragma mark - Resettable State -
1748
+ - (void)checkForCanReset {
1749
+ BOOL canReset = NO;
1750
+
1751
+ if (self.angle != 0) { // Image has been rotated
1752
+ canReset = YES;
1753
+ } else if (self.scrollView.zoomScale > self.scrollView.minimumZoomScale + FLT_EPSILON) { // image has been zoomed in
1754
+ canReset = YES;
1755
+ } else if ((NSInteger)floorf(self.cropBoxFrame.size.width) != (NSInteger)floorf(self.originalCropBoxSize.width) ||
1756
+ (NSInteger)floorf(self.cropBoxFrame.size.height) != (NSInteger)floorf(self.originalCropBoxSize.height)) { // crop box has been changed
1757
+ canReset = YES;
1758
+ } else if ((NSInteger)floorf(self.scrollView.contentOffset.x) != (NSInteger)floorf(self.originalContentOffset.x) ||
1759
+ (NSInteger)floorf(self.scrollView.contentOffset.y) != (NSInteger)floorf(self.originalContentOffset.y)) {
1760
+ canReset = YES;
1761
+ }
1762
+
1763
+ self.canBeReset = canReset;
1764
+ }
1765
+
1766
+ #pragma mark - Convienience Methods -
1767
+ - (CGRect)contentBounds {
1768
+ CGRect contentRect = CGRectZero;
1769
+ contentRect.origin.x = self.cropViewPadding + self.cropRegionInsets.left;
1770
+ contentRect.origin.y = self.cropViewPadding + self.cropRegionInsets.top;
1771
+ contentRect.size.width = CGRectGetWidth(self.frame) - ((self.cropViewPadding * 2) + self.cropRegionInsets.left + self.cropRegionInsets.right);
1772
+ contentRect.size.height = CGRectGetHeight(self.frame) - ((self.cropViewPadding * 2) + self.cropRegionInsets.top + self.cropRegionInsets.bottom);
1773
+ return contentRect;
1774
+ }
1775
+
1776
+ - (CGSize)imageSize {
1777
+ if (self.angle == -90 || self.angle == -270 || self.angle == 90 || self.angle == 270)
1778
+ return (CGSize){self.image.size.height, self.image.size.width};
1779
+
1780
+ return (CGSize){self.image.size.width, self.image.size.height};
1781
+ }
1782
+
1783
+ - (BOOL)hasAspectRatio {
1784
+ return (self.aspectRatio.width > FLT_EPSILON && self.aspectRatio.height > FLT_EPSILON);
1785
+ }
1786
+
1787
+ @end