@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.
- package/LICENSE +21 -0
- package/README.md +83 -0
- package/ReactNativeImageCropPicker.podspec +40 -0
- package/android/CMakeLists.txt +24 -0
- package/android/build.gradle +144 -0
- package/android/gradle.properties +4 -0
- package/android/src/main/AndroidManifest.xml +24 -0
- package/android/src/main/cpp/cpp-adapter.cpp +6 -0
- package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ImageCropPickerException.kt +45 -0
- package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ImageCropPickerImageProcessor.kt +308 -0
- package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ImageCropPickerSession.kt +297 -0
- package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ReactNativeImageCropPicker.kt +69 -0
- package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ReactNativeImageCropPickerPackage.kt +24 -0
- package/ios/ImageCropPickerError.swift +57 -0
- package/ios/ImageCropPickerImageProcessor.swift +343 -0
- package/ios/ImageCropPickerSession.swift +407 -0
- package/ios/ReactNativeImageCropPicker.swift +65 -0
- package/ios/TOCropViewController/Categories/UIImage+CropRotate.h +39 -0
- package/ios/TOCropViewController/Categories/UIImage+CropRotate.m +94 -0
- package/ios/TOCropViewController/Constants/TOCropViewConstants.h +63 -0
- package/ios/TOCropViewController/LICENSE +21 -0
- package/ios/TOCropViewController/Models/TOActivityCroppedImageProvider.h +38 -0
- package/ios/TOCropViewController/Models/TOActivityCroppedImageProvider.m +73 -0
- package/ios/TOCropViewController/Models/TOCropViewControllerAspectRatioPreset.h +64 -0
- package/ios/TOCropViewController/Models/TOCropViewControllerAspectRatioPreset.m +121 -0
- package/ios/TOCropViewController/Models/TOCropViewControllerTransitioning.h +49 -0
- package/ios/TOCropViewController/Models/TOCropViewControllerTransitioning.m +125 -0
- package/ios/TOCropViewController/Models/TOCroppedImageAttributes.h +38 -0
- package/ios/TOCropViewController/Models/TOCroppedImageAttributes.m +45 -0
- package/ios/TOCropViewController/Resources/Base.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/PrivacyInfo.xcprivacy +14 -0
- package/ios/TOCropViewController/Resources/ar.lproj/TOCropViewControllerLocalizable.strings +9 -0
- package/ios/TOCropViewController/Resources/ca.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/cs.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/da-DK.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/de.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/en.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/es.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/fa-IR.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/fa.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/fi.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/fr.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/hu.lproj/TOCropViewControllerLocalizable.strings +9 -0
- package/ios/TOCropViewController/Resources/id.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/it.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/ja.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/ko.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/ms.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/nl.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/pl.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/pt-BR.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/pt.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/ro.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/ru.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/sk.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/tr.lproj/TOCropViewControllerLocalizable.strings +9 -0
- package/ios/TOCropViewController/Resources/uk.lproj/TOCropViewControllerLocalizable.strings +8 -0
- package/ios/TOCropViewController/Resources/vi.lproj/TOCropViewControllerLocalizable.strings +9 -0
- package/ios/TOCropViewController/Resources/zh-Hans.lproj/TOCropViewControllerLocalizable.strings +9 -0
- package/ios/TOCropViewController/Resources/zh-Hant.lproj/TOCropViewControllerLocalizable.strings +9 -0
- package/ios/TOCropViewController/TOCropViewController.h +479 -0
- package/ios/TOCropViewController/TOCropViewController.m +1355 -0
- package/ios/TOCropViewController/Views/TOCropOverlayView.h +43 -0
- package/ios/TOCropViewController/Views/TOCropOverlayView.m +240 -0
- package/ios/TOCropViewController/Views/TOCropScrollView.h +39 -0
- package/ios/TOCropViewController/Views/TOCropScrollView.m +48 -0
- package/ios/TOCropViewController/Views/TOCropToolbar.h +104 -0
- package/ios/TOCropViewController/Views/TOCropToolbar.m +783 -0
- package/ios/TOCropViewController/Views/TOCropView.h +311 -0
- package/ios/TOCropViewController/Views/TOCropView.m +1787 -0
- package/lib/module/ReactNativeImageCropPicker.nitro.js +4 -0
- package/lib/module/ReactNativeImageCropPicker.nitro.js.map +1 -0
- package/lib/module/index.js +83 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/ReactNativeImageCropPicker.nitro.d.ts +54 -0
- package/lib/typescript/src/ReactNativeImageCropPicker.nitro.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +31 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/nitro.json +23 -0
- package/nitrogen/generated/android/c++/JCropRect.hpp +69 -0
- package/nitrogen/generated/android/c++/JHybridReactNativeImageCropPickerSpec.cpp +125 -0
- package/nitrogen/generated/android/c++/JHybridReactNativeImageCropPickerSpec.hpp +66 -0
- package/nitrogen/generated/android/c++/JImageCropPickerOptions.hpp +154 -0
- package/nitrogen/generated/android/c++/JPickedImage.hpp +88 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/CropRect.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/HybridReactNativeImageCropPickerSpec.kt +69 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/ImageCropPickerOptions.kt +171 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/PickedImage.kt +86 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/reactnativeimagecroppickerOnLoad.kt +35 -0
- package/nitrogen/generated/android/reactnativeimagecroppicker+autolinking.cmake +81 -0
- package/nitrogen/generated/android/reactnativeimagecroppicker+autolinking.gradle +27 -0
- package/nitrogen/generated/android/reactnativeimagecroppickerOnLoad.cpp +54 -0
- package/nitrogen/generated/android/reactnativeimagecroppickerOnLoad.hpp +34 -0
- package/nitrogen/generated/ios/ReactNativeImageCropPicker+autolinking.rb +62 -0
- package/nitrogen/generated/ios/ReactNativeImageCropPicker-Swift-Cxx-Bridge.cpp +57 -0
- package/nitrogen/generated/ios/ReactNativeImageCropPicker-Swift-Cxx-Bridge.hpp +221 -0
- package/nitrogen/generated/ios/ReactNativeImageCropPicker-Swift-Cxx-Umbrella.hpp +55 -0
- package/nitrogen/generated/ios/ReactNativeImageCropPickerAutolinking.mm +33 -0
- package/nitrogen/generated/ios/ReactNativeImageCropPickerAutolinking.swift +26 -0
- package/nitrogen/generated/ios/c++/HybridReactNativeImageCropPickerSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridReactNativeImageCropPickerSpecSwift.hpp +116 -0
- package/nitrogen/generated/ios/swift/CropRect.swift +44 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_PickedImage.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridReactNativeImageCropPickerSpec.swift +58 -0
- package/nitrogen/generated/ios/swift/HybridReactNativeImageCropPickerSpec_cxx.swift +202 -0
- package/nitrogen/generated/ios/swift/ImageCropPickerOptions.swift +474 -0
- package/nitrogen/generated/ios/swift/PickedImage.swift +96 -0
- package/nitrogen/generated/shared/c++/CropRect.hpp +95 -0
- package/nitrogen/generated/shared/c++/HybridReactNativeImageCropPickerSpec.cpp +24 -0
- package/nitrogen/generated/shared/c++/HybridReactNativeImageCropPickerSpec.hpp +71 -0
- package/nitrogen/generated/shared/c++/ImageCropPickerOptions.hpp +180 -0
- package/nitrogen/generated/shared/c++/PickedImage.hpp +114 -0
- package/package.json +173 -0
- package/src/ReactNativeImageCropPicker.nitro.ts +72 -0
- 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
|