1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210 |
- //
- // TOCropView.m
- //
- // Copyright 2015 Timothy Oliver. All rights reserved.
- //
- // Permission is hereby granted, free of charge, to any person obtaining a copy
- // of this software and associated documentation files (the "Software"), to
- // deal in the Software without restriction, including without limitation the
- // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
- // sell copies of the Software, and to permit persons to whom the Software is
- // furnished to do so, subject to the following conditions:
- //
- // The above copyright notice and this permission notice shall be included in
- // all copies or substantial portions of the Software.
- //
- // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
- // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
- // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
- // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- #import "TOCropView.h"
- #import "TOCropOverlayView.h"
- #import "TOCropScrollView.h"
- #define TOCROPVIEW_BACKGROUND_COLOR [UIColor colorWithWhite:0.12f alpha:1.0f]
- static const CGFloat kTOCropViewPadding = 14.0f;
- static const NSTimeInterval kTOCropTimerDuration = 0.8f;
- static const CGFloat kTOCropViewMinimumBoxSize = 42.0f;
- /* When the user taps down to resize the box, this state is used
- to determine where they tapped and how to manipulate the box */
- typedef NS_ENUM(NSInteger, TOCropViewOverlayEdge) {
- TOCropViewOverlayEdgeNone,
- TOCropViewOverlayEdgeTopLeft,
- TOCropViewOverlayEdgeTop,
- TOCropViewOverlayEdgeTopRight,
- TOCropViewOverlayEdgeRight,
- TOCropViewOverlayEdgeBottomRight,
- TOCropViewOverlayEdgeBottom,
- TOCropViewOverlayEdgeBottomLeft,
- TOCropViewOverlayEdgeLeft
- };
- @interface TOCropView () <UIScrollViewDelegate, UIGestureRecognizerDelegate>
- @property (nonatomic, strong, readwrite) UIImage *image;
- /* Views */
- @property (nonatomic, strong) UIImageView *backgroundImageView; /* The main image view, placed within the scroll view */
- @property (nonatomic, strong) UIView *backgroundContainerView; /* A view which contains the background image view, to separate its transforms from the scroll view. */
- @property (nonatomic, strong) UIImageView *foregroundImageView; /* A copy of the background image view, placed over the dimming views */
- @property (nonatomic, strong) UIView *foregroundContainerView; /* A container view that clips the foreground image view to the crop box frame */
- @property (nonatomic, strong) TOCropScrollView *scrollView; /* The scroll view in charge of panning/zooming the image. */
- @property (nonatomic, strong) UIView *overlayView; /* A semi-transparent grey view, overlaid on top of the background image */
- @property (nonatomic, strong) UIView *translucencyView; /* A blur view that is made visible when the user isn't interacting with the crop view */
- @property (nonatomic, strong) TOCropOverlayView *gridOverlayView; /* A grid view overlaid on top of the foreground image view's container. */
- @property (nonatomic, strong) UIPanGestureRecognizer *gridPanGestureRecognizer; /* The gesture recognizer in charge of controlling the resizing of the crop view */
- /* Crop box handling */
- @property (nonatomic, assign) TOCropViewOverlayEdge tappedEdge; /* The edge region that the user tapped on, to resize the cropping region */
- @property (nonatomic, assign) CGRect cropOriginFrame; /* When resizing, this is the original frame of the crop box. */
- @property (nonatomic, assign) CGPoint panOriginPoint; /* The initial touch point of the pan gesture recognizer */
- @property (nonatomic, assign, readwrite) CGRect cropBoxFrame; /* The frame, in relation to to this view where the grid, and crop container view are aligned */
- @property (nonatomic, strong) NSTimer *resetTimer; /* The timer used to reset the view after the user stops interacting with it */
- @property (nonatomic, assign) BOOL editing; /* Used to denote the active state of the user manipulating the content */
- @property (nonatomic, assign, readwrite) NSInteger angle;
- /* Pre-screen-rotation state information */
- @property (nonatomic, assign) CGPoint rotationContentOffset;
- @property (nonatomic, assign) CGSize rotationContentSize;
- @property (nonatomic, readonly) CGRect contentBounds; /* Give the current screen real-estate, the frame that the scroll view is allowed to use */
- @property (nonatomic, readonly) CGSize imageSize; /* Given the current rotation of the image, the size of the image */
- /* 90-degree rotation state data */
- @property (nonatomic, assign) CGSize cropBoxLastEditedSize; /* When performing 90-degree rotations, remember what our last manual size was to use that as a base */
- @property (nonatomic, assign) NSInteger cropBoxLastEditedAngle; /* Remember which angle we were at when we saved the editing size */
- @property (nonatomic, assign) BOOL rotateAnimationInProgress; /* Disallow any input while the rotation animation is playing */
- /* Reset state data */
- @property (nonatomic, assign) CGSize originalCropBoxSize; /* Save the original crop box size so we can tell when the content has been edited */
- @property (nonatomic, assign, readwrite) BOOL canReset;
- - (void)setup;
- /* Image layout */
- - (void)layoutInitialImage;
- - (void)matchForegroundToBackground;
- /* Crop box handling */
- - (TOCropViewOverlayEdge)cropEdgeForPoint:(CGPoint)point;
- - (void)updateCropBoxFrameWithGesturePoint:(CGPoint)point;
- /* Editing state */
- - (void)setEditing:(BOOL)editing animated:(BOOL)animated;
- - (void)moveCroppedContentToCenterAnimated:(BOOL)animated;
- - (void)startEditing;
- /* Timer handling */
- - (void)startResetTimer;
- - (void)timerTriggered;
- - (void)cancelResetTimer;
- /* Gesture Recognizers */
- - (void)gridPanGestureRecognized:(UIPanGestureRecognizer *)recognizer;
- - (void)longPressGestureRecognized:(UILongPressGestureRecognizer *)recognizer;
- /* Reset state */
- - (void)checkForCanReset;
- @end
- @implementation TOCropView
- - (instancetype)initWithImage:(UIImage *)image
- {
- if (self = [super init]) {
- _image = image;
- [self setup];
- }
-
- return self;
- }
- - (void)setup
- {
- __weak typeof(self) weakSelf = self;
-
- //View properties
- self.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
- self.backgroundColor = TOCROPVIEW_BACKGROUND_COLOR;
- self.cropBoxFrame = CGRectZero;
-
- //Scroll View properties
- self.scrollView = [[TOCropScrollView alloc] initWithFrame:self.bounds];
- self.scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
- self.scrollView.alwaysBounceHorizontal = YES;
- self.scrollView.alwaysBounceVertical = YES;
- self.scrollView.showsHorizontalScrollIndicator = NO;
- self.scrollView.showsVerticalScrollIndicator = NO;
- self.scrollView.delegate = self;
- [self addSubview:self.scrollView];
-
- self.scrollView.touchesBegan = ^{ [weakSelf startEditing]; };
- self.scrollView.touchesEnded = ^{ [weakSelf startResetTimer]; };
-
- //Background Image View
- self.backgroundImageView = [[UIImageView alloc] initWithImage:self.image];
- //self.backgroundImageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
- //Background container view
- self.backgroundContainerView = [[UIView alloc] initWithFrame:self.backgroundImageView.frame];
- [self.backgroundContainerView addSubview:self.backgroundImageView];
- [self.scrollView addSubview:self.backgroundContainerView];
-
- //Grey transparent overlay view
- self.overlayView = [[UIView alloc] initWithFrame:self.bounds];
- self.overlayView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
- self.overlayView.backgroundColor = [self.backgroundColor colorWithAlphaComponent:0.35f];
- self.overlayView.hidden = NO;
- self.overlayView.userInteractionEnabled = NO;
- [self addSubview:self.overlayView];
-
- //Translucency View
- if (NSClassFromString(@"UIVisualEffectView")) {
- self.translucencyView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleDark]];
- self.translucencyView.frame = self.bounds;
- }
- else {
- UIToolbar *toolbar = [[UIToolbar alloc] init];
- toolbar.barStyle = UIBarStyleBlack;
- self.translucencyView = toolbar;
- self.translucencyView.frame = CGRectInset(self.bounds, -1.0f, -1.0f);
- }
-
- self.translucencyView.hidden = NO;
- self.translucencyView.userInteractionEnabled = NO;
- self.translucencyView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
- [self addSubview:self.translucencyView];
-
- self.foregroundContainerView = [[UIView alloc] initWithFrame:(CGRect){0,0,200,200}];
- self.foregroundContainerView.clipsToBounds = YES;
- self.foregroundContainerView.userInteractionEnabled = NO;
- [self addSubview:self.foregroundContainerView];
-
- self.gridOverlayView = [[TOCropOverlayView alloc] initWithFrame:self.foregroundContainerView.frame];
- self.gridOverlayView.userInteractionEnabled = NO;
- self.gridOverlayView.gridHidden = YES;
- [self addSubview:self.gridOverlayView];
-
- self.foregroundImageView = [[UIImageView alloc] initWithImage:self.image];
- [self.foregroundContainerView addSubview:self.foregroundImageView];
-
- self.gridPanGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(gridPanGestureRecognized:)];
- self.gridPanGestureRecognizer.delegate = self;
- [self.scrollView.panGestureRecognizer requireGestureRecognizerToFail:self.gridPanGestureRecognizer];
- [self addGestureRecognizer:self.gridPanGestureRecognizer];
-
- self.editing = NO;
- }
- #pragma mark - View Layout -
- - (void)didMoveToSuperview
- {
- [super didMoveToSuperview];
- [self layoutInitialImage];
- }
- - (void)layoutInitialImage
- {
- CGSize imageSize = self.imageSize;
- self.scrollView.contentSize = imageSize;
-
- CGRect bounds = self.contentBounds;
- //work out the max and min scale of the image
- CGFloat scale = MIN(CGRectGetWidth(bounds)/imageSize.width, CGRectGetHeight(bounds)/imageSize.height);
- CGSize scaledSize = (CGSize){floorf(imageSize.width * scale), floorf(imageSize.height * scale)};
- self.scrollView.minimumZoomScale = scale;
- self.scrollView.maximumZoomScale = 15.0f;
- //set the fully zoomed out state initially
- self.scrollView.zoomScale = self.scrollView.minimumZoomScale;
- self.scrollView.contentSize = scaledSize;
-
- //Relayout the image in the scroll view
- CGRect frame = CGRectZero;
- frame.size = scaledSize;
- frame.origin.x = bounds.origin.x + floorf((CGRectGetWidth(bounds) - frame.size.width) * 0.5f);
- frame.origin.y = bounds.origin.y + floorf((CGRectGetHeight(bounds) - frame.size.height) * 0.5f);
- self.cropBoxFrame = frame;
-
- //save the current state for use with 90-degree rotations
- self.cropBoxLastEditedSize = self.cropBoxFrame.size;
- self.cropBoxLastEditedAngle = 0;
-
- //save the size for checking if we're in a resettable state
- self.originalCropBoxSize = self.cropBoxFrame.size;
-
- [self matchForegroundToBackground];
- }
- - (void)prepareforRotation
- {
- self.rotationContentOffset = self.scrollView.contentOffset;
- self.rotationContentSize = self.scrollView.contentSize;
- }
- - (void)performRelayoutForRotation
- {
- CGRect cropFrame = self.cropBoxFrame;
- CGRect contentFrame = self.contentBounds;
-
- //Work out the portion of the image we were focused on
- CGPoint cropMidPoint = (CGPoint){CGRectGetMidX(cropFrame), CGRectGetMidY(cropFrame)};
- CGPoint cropTargetPoint = (CGPoint){cropMidPoint.x + self.rotationContentOffset.x, cropMidPoint.y + self.rotationContentOffset.y};
-
- CGFloat scale = MIN(contentFrame.size.width / cropFrame.size.width, contentFrame.size.height / cropFrame.size.height);
- self.scrollView.minimumZoomScale *= scale;
- self.scrollView.zoomScale *= scale;
-
- //Work out the centered, upscaled version of the crop rectangle
- cropFrame.size.width = floorf(cropFrame.size.width * scale);
- cropFrame.size.height = floorf(cropFrame.size.height * scale);
- cropFrame.origin.x = floorf(contentFrame.origin.x + ((contentFrame.size.width - cropFrame.size.width) * 0.5f));
- cropFrame.origin.y = floorf(contentFrame.origin.y + ((contentFrame.size.height - cropFrame.size.height) * 0.5f));
- self.cropBoxFrame = cropFrame;
-
- self.cropBoxLastEditedSize = self.cropBoxFrame.size;
-
- //work out how to line up out point of interest into the middle of the crop box
- cropTargetPoint.x *= scale;
- cropTargetPoint.y *= scale;
-
- CGPoint midPoint = {floorf(CGRectGetMidX(cropFrame)), floorf(CGRectGetMidY(cropFrame))};
- CGPoint offset = CGPointZero;
- offset.x = floorf(-midPoint.x + cropTargetPoint.x);
- offset.y = floorf(-midPoint.y + cropTargetPoint.y);
- offset.x = MAX(-self.scrollView.contentInset.left, offset.x);
- offset.y = MAX(-self.scrollView.contentInset.top, offset.y);
- self.scrollView.contentOffset = offset;
-
- [self matchForegroundToBackground];
- }
- - (void)matchForegroundToBackground
- {
- //We can't simply match the frames since if the images are rotated, the frame property becomes unusable
- self.foregroundImageView.frame = [self.backgroundContainerView.superview convertRect:self.backgroundContainerView.frame toView:self.foregroundContainerView];
- }
- - (void)updateCropBoxFrameWithGesturePoint:(CGPoint)point
- {
- CGRect frame = self.cropBoxFrame;
- CGRect originFrame = self.cropOriginFrame;
- CGRect contentFrame = self.contentBounds;
-
- point.x = MAX(contentFrame.origin.x, point.x);
- point.y = MAX(contentFrame.origin.y, point.y);
-
- //The delta between where we first tapped, and where our finger is now
- CGFloat xDelta = ceilf(point.x - self.panOriginPoint.x);
- CGFloat yDelta = ceilf(point.y - self.panOriginPoint.y);
-
- //Current aspect ratio of the crop box in case we need to clamp it
- CGFloat aspectRatio = (originFrame.size.width / originFrame.size.height);
-
- BOOL aspectHorizontal = NO, aspectVertical = NO;
-
- switch (self.tappedEdge) {
- case TOCropViewOverlayEdgeLeft:
- if (self.aspectLockEnabled) {
- aspectHorizontal = YES;
- xDelta = MAX(xDelta, 0);
- CGPoint scaleOrigin = (CGPoint){CGRectGetMaxX(originFrame), CGRectGetMidY(originFrame)};
- frame.size.height = frame.size.width / aspectRatio;
- frame.origin.y = scaleOrigin.y - (frame.size.height * 0.5f);
- }
-
- frame.origin.x = originFrame.origin.x + xDelta;
- frame.size.width = originFrame.size.width - xDelta;
- break;
- case TOCropViewOverlayEdgeRight:
- if (self.aspectLockEnabled) {
- aspectHorizontal = YES;
- CGPoint scaleOrigin = (CGPoint){CGRectGetMinX(originFrame), CGRectGetMidY(originFrame)};
- frame.size.height = frame.size.width / aspectRatio;
- frame.origin.y = scaleOrigin.y - (frame.size.height * 0.5f);
- frame.size.width = originFrame.size.width + xDelta;
- frame.size.width = MIN(frame.size.width, contentFrame.size.height * aspectRatio);
- }
- else {
- frame.size.width = originFrame.size.width + xDelta;
- }
-
- break;
- case TOCropViewOverlayEdgeBottom:
- if (self.aspectLockEnabled) {
- aspectVertical = YES;
- CGPoint scaleOrigin = (CGPoint){CGRectGetMidX(originFrame), CGRectGetMinY(originFrame)};
- frame.size.width = frame.size.height * aspectRatio;
- frame.origin.x = scaleOrigin.x - (frame.size.width * 0.5f);
- frame.size.height = originFrame.size.height + yDelta;
- frame.size.height = MIN(frame.size.height, contentFrame.size.width / aspectRatio);
- }
- else {
- frame.size.height = originFrame.size.height + yDelta;
- }
- break;
- case TOCropViewOverlayEdgeTop:
- if (self.aspectLockEnabled) {
- aspectVertical = YES;
- yDelta = MAX(0,yDelta);
- CGPoint scaleOrigin = (CGPoint){CGRectGetMidX(originFrame), CGRectGetMaxY(originFrame)};
- frame.size.width = frame.size.height * aspectRatio;
- frame.origin.x = scaleOrigin.x - (frame.size.width * 0.5f);
- frame.origin.y = originFrame.origin.y + yDelta;
- frame.size.height = originFrame.size.height - yDelta;
- }
- else {
- frame.origin.y = originFrame.origin.y + yDelta;
- frame.size.height = originFrame.size.height - yDelta;
- }
- break;
- case TOCropViewOverlayEdgeTopLeft:
- if (self.aspectLockEnabled) {
- xDelta = MAX(xDelta, 0);
- yDelta = MAX(yDelta, 0);
-
- CGPoint distance;
- distance.x = 1.0f - (xDelta / CGRectGetWidth(originFrame));
- distance.y = 1.0f - (yDelta / CGRectGetHeight(originFrame));
-
- CGFloat scale = (distance.x + distance.y) * 0.5f;
-
- frame.size.width = ceilf(CGRectGetWidth(originFrame) * scale);
- frame.size.height = ceilf(CGRectGetHeight(originFrame) * scale);
- frame.origin.x = originFrame.origin.x + (CGRectGetWidth(originFrame) - frame.size.width);
- frame.origin.y = originFrame.origin.y + (CGRectGetHeight(originFrame) - frame.size.height);
-
- aspectVertical = YES;
- aspectHorizontal = YES;
- }
- else {
- frame.origin.x = originFrame.origin.x + xDelta;
- frame.size.width = originFrame.size.width - xDelta;
- frame.origin.y = originFrame.origin.y + yDelta;
- frame.size.height = originFrame.size.height - yDelta;
- }
- break;
- case TOCropViewOverlayEdgeTopRight:
- if (self.aspectLockEnabled) {
- xDelta = MAX(xDelta, 0);
- yDelta = MAX(yDelta, 0);
-
- CGPoint distance;
- distance.x = 1.0f - ((-xDelta) / CGRectGetWidth(originFrame));
- distance.y = 1.0f - ((yDelta) / CGRectGetHeight(originFrame));
-
- CGFloat scale = (distance.x + distance.y) * 0.5f;
- scale = MIN(1.0f, scale);
-
- frame.size.width = ceilf(CGRectGetWidth(originFrame) * scale);
- frame.size.height = ceilf(CGRectGetHeight(originFrame) * scale);
- frame.origin.y = CGRectGetMaxY(originFrame) - frame.size.height;
-
- aspectVertical = YES;
- aspectHorizontal = YES;
- }
- else {
- frame.size.width = originFrame.size.width + xDelta;
- frame.origin.y = originFrame.origin.y + yDelta;
- frame.size.height = originFrame.size.height - yDelta;
- }
- break;
- case TOCropViewOverlayEdgeBottomLeft:
- if (self.aspectLockEnabled) {
- CGPoint distance;
- distance.x = 1.0f - (xDelta / CGRectGetWidth(originFrame));
- distance.y = 1.0f - (-yDelta / CGRectGetHeight(originFrame));
-
- CGFloat scale = (distance.x + distance.y) * 0.5f;
-
- frame.size.width = ceilf(CGRectGetWidth(originFrame) * scale);
- frame.size.height = ceilf(CGRectGetHeight(originFrame) * scale);
- frame.origin.x = CGRectGetMaxX(originFrame) - frame.size.width;
-
- aspectVertical = YES;
- aspectHorizontal = YES;
- }
- else {
- frame.size.height = originFrame.size.height + yDelta;
- frame.origin.x = originFrame.origin.x + xDelta;
- frame.size.width = originFrame.size.width - xDelta;
- }
- break;
- case TOCropViewOverlayEdgeBottomRight:
- if (self.aspectLockEnabled) {
-
- CGPoint distance;
- distance.x = 1.0f - ((-1 * xDelta) / CGRectGetWidth(originFrame));
- distance.y = 1.0f - ((-1 * yDelta) / CGRectGetHeight(originFrame));
-
- CGFloat scale = (distance.x + distance.y) * 0.5f;
-
- frame.size.width = ceilf(CGRectGetWidth(originFrame) * scale);
- frame.size.height = ceilf(CGRectGetHeight(originFrame) * scale);
-
- aspectVertical = YES;
- aspectHorizontal = YES;
- }
- else {
- frame.size.height = originFrame.size.height + yDelta;
- frame.size.width = originFrame.size.width + xDelta;
- }
- break;
- case TOCropViewOverlayEdgeNone: break;
- }
-
- //Work out the limits the box may be scaled before it starts to overlap itself
- CGSize minSize = CGSizeZero;
- minSize.width = kTOCropViewMinimumBoxSize;
- minSize.height = kTOCropViewMinimumBoxSize;
-
- CGSize maxSize = CGSizeZero;
- maxSize.width = CGRectGetWidth(contentFrame);
- maxSize.height = CGRectGetHeight(contentFrame);
-
- //clamp the box to ensure it doesn't go beyond the bounds we've set
- if (self.aspectLockEnabled && aspectHorizontal) {
- maxSize.height = contentFrame.size.width / aspectRatio;
- minSize.width = kTOCropViewMinimumBoxSize * aspectRatio;
- }
-
- if (self.aspectLockEnabled && aspectVertical) {
- maxSize.width = contentFrame.size.height * aspectRatio;
- minSize.height = kTOCropViewMinimumBoxSize / aspectRatio;
- }
-
- //Clamp the minimum size
- frame.size.width = MAX(frame.size.width, minSize.width);
- frame.size.height = MAX(frame.size.height, minSize.height);
-
- //Clamp the maximum size
- frame.size.width = MIN(frame.size.width, maxSize.width);
- frame.size.height = MIN(frame.size.height, maxSize.height);
-
- frame.origin.x = MAX(frame.origin.x, CGRectGetMinX(contentFrame));
- frame.origin.x = MIN(frame.origin.x, CGRectGetMaxX(contentFrame) - minSize.width);
- frame.origin.y = MAX(frame.origin.y, CGRectGetMinY(contentFrame));
- frame.origin.y = MIN(frame.origin.y, CGRectGetMaxY(contentFrame) - minSize.height);
-
- self.cropBoxFrame = frame;
-
- [self checkForCanReset];
- }
- - (void)resetLayoutToDefaultAnimated:(BOOL)animated
- {
- if (animated == NO || self.angle < 0) {
- self.angle = 0;
- self.foregroundImageView.transform = CGAffineTransformIdentity;
- self.backgroundImageView.transform = CGAffineTransformIdentity;
-
- self.scrollView.zoomScale = 1.0f;
- self.backgroundContainerView.frame = (CGRect){CGPointZero, self.backgroundImageView.frame.size};
- self.backgroundImageView.frame = self.backgroundContainerView.frame;
- self.foregroundImageView.frame = self.backgroundContainerView.frame;
-
- [self layoutInitialImage];
- [self checkForCanReset];
- return;
- }
-
- [UIView animateWithDuration:0.5f delay:0.0f usingSpringWithDamping:1.0f initialSpringVelocity:0.7f options:0 animations:^{
- [self layoutInitialImage];
- [self checkForCanReset];
- } completion:nil];
- }
- #pragma mark - Gesture Recognizer -
- - (void)gridPanGestureRecognized:(UIPanGestureRecognizer *)recognizer
- {
- CGPoint point = [recognizer locationInView:self];
-
- if (recognizer.state == UIGestureRecognizerStateBegan) {
- [self startEditing];
- self.panOriginPoint = point;
- self.cropOriginFrame = self.cropBoxFrame;
- self.tappedEdge = [self cropEdgeForPoint:self.panOriginPoint];
- }
-
- if (recognizer.state == UIGestureRecognizerStateEnded)
- [self startResetTimer];
-
- [self updateCropBoxFrameWithGesturePoint:point];
- }
- - (void)longPressGestureRecognized:(UILongPressGestureRecognizer *)recognizer
- {
- if (recognizer.state == UIGestureRecognizerStateBegan)
- [self.gridOverlayView setGridHidden:NO animated:YES];
-
- if (recognizer.state == UIGestureRecognizerStateEnded)
- [self.gridOverlayView setGridHidden:YES animated:YES];
- }
- - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
- {
- if (gestureRecognizer != self.gridPanGestureRecognizer)
- return YES;
-
- CGPoint tapPoint = [gestureRecognizer locationInView:self];
-
- CGRect frame = self.gridOverlayView.frame;
- CGRect innerFrame = CGRectInset(frame, 22.0f, 22.0f);
- CGRect outerFrame = CGRectInset(frame, -22.0f, -22.0f);
-
- if (CGRectContainsPoint(innerFrame, tapPoint) || !CGRectContainsPoint(outerFrame, tapPoint))
- return NO;
-
- return YES;
- }
- #pragma mark - Timer -
- - (void)startResetTimer
- {
- if (self.resetTimer)
- return;
-
- self.resetTimer = [NSTimer scheduledTimerWithTimeInterval:kTOCropTimerDuration target:self selector:@selector(timerTriggered) userInfo:nil repeats:NO];
- }
- - (void)timerTriggered
- {
- [self setEditing:NO animated:YES];
- [self.resetTimer invalidate];
- self.resetTimer = nil;
- }
- - (void)cancelResetTimer
- {
- [self.resetTimer invalidate];
- self.resetTimer = nil;
- }
- - (TOCropViewOverlayEdge)cropEdgeForPoint:(CGPoint)point
- {
- CGRect frame = self.cropBoxFrame;
-
- //account for padding around the box
- frame = CGRectInset(frame, -22.0f, -22.0f);
-
- //Make sure the corners take priority
- CGRect topLeftRect = (CGRect){frame.origin, {44,44}};
- if (CGRectContainsPoint(topLeftRect, point))
- return TOCropViewOverlayEdgeTopLeft;
-
- CGRect topRightRect = topLeftRect;
- topRightRect.origin.x = CGRectGetMaxX(frame) - 44.0f;
- if (CGRectContainsPoint(topRightRect, point))
- return TOCropViewOverlayEdgeTopRight;
-
- CGRect bottomLeftRect = topLeftRect;
- bottomLeftRect.origin.y = CGRectGetMaxY(frame) - 44.0f;
- if (CGRectContainsPoint(bottomLeftRect, point))
- return TOCropViewOverlayEdgeBottomLeft;
-
- CGRect bottomRightRect = topRightRect;
- bottomRightRect.origin.y = bottomLeftRect.origin.y;
- if (CGRectContainsPoint(bottomRightRect, point))
- return TOCropViewOverlayEdgeBottomRight;
-
- //Check for edges
- CGRect topRect = (CGRect){frame.origin, {CGRectGetWidth(frame), 44.0f}};
- if (CGRectContainsPoint(topRect, point))
- return TOCropViewOverlayEdgeTop;
-
- CGRect bottomRect = topRect;
- bottomRect.origin.y = CGRectGetMaxY(frame) - 44.0f;
- if (CGRectContainsPoint(bottomRect, point))
- return TOCropViewOverlayEdgeBottom;
-
- CGRect leftRect = (CGRect){frame.origin, {44.0f, CGRectGetHeight(frame)}};
- if (CGRectContainsPoint(leftRect, point))
- return TOCropViewOverlayEdgeLeft;
-
- CGRect rightRect = leftRect;
- rightRect.origin.x = CGRectGetMaxX(frame) - 44.0f;
- if (CGRectContainsPoint(rightRect, point))
- return TOCropViewOverlayEdgeRight;
-
- return TOCropViewOverlayEdgeNone;
- }
- #pragma mark - Scroll View Delegate -
- - (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView { return self.backgroundContainerView; }
- - (void)scrollViewDidScroll:(UIScrollView *)scrollView { [self matchForegroundToBackground]; }
- - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { [self startEditing]; }
- - (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view { [self startEditing]; }
- - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { [self startResetTimer]; }
- - (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale { [self startResetTimer]; }
- - (void)scrollViewDidZoom:(UIScrollView *)scrollView
- {
- [self checkForCanReset];
- [self matchForegroundToBackground];
- }
- - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
- {
- if (!decelerate)
- [self startResetTimer];
- }
- #pragma mark - Accessors -
- - (void)setCropBoxFrame:(CGRect)cropBoxFrame
- {
- if (CGRectEqualToRect(cropBoxFrame, _cropBoxFrame))
- return;
-
- //Upon init, sometimes the box size is still 0, which can result in CALayer issues
- if (cropBoxFrame.size.width < FLT_EPSILON || cropBoxFrame.size.height < FLT_EPSILON)
- return;
-
- //clamp the cropping region to the inset boundaries of the screen
- CGRect contentFrame = self.contentBounds;
- CGFloat xOrigin = ceilf(contentFrame.origin.x);
- CGFloat xDelta = cropBoxFrame.origin.x - xOrigin;
- cropBoxFrame.origin.x = floorf(MAX(cropBoxFrame.origin.x, xOrigin));
- 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)
- cropBoxFrame.size.width += xDelta;
-
- CGFloat yOrigin = ceilf(contentFrame.origin.y);
- CGFloat yDelta = cropBoxFrame.origin.y - yOrigin;
- cropBoxFrame.origin.y = floorf(MAX(cropBoxFrame.origin.y, yOrigin));
- if (yDelta < -FLT_EPSILON)
- cropBoxFrame.size.height += yDelta;
-
- //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
- CGFloat maxWidth = (contentFrame.size.width + contentFrame.origin.x) - cropBoxFrame.origin.x;
- cropBoxFrame.size.width = floorf(MIN(cropBoxFrame.size.width, maxWidth));
-
- CGFloat maxHeight = (contentFrame.size.height + contentFrame.origin.y) - cropBoxFrame.origin.y;
- cropBoxFrame.size.height = floorf(MIN(cropBoxFrame.size.height, maxHeight));
-
- //Make sure we can't make the crop box too small
- cropBoxFrame.size.width = MAX(cropBoxFrame.size.width, kTOCropViewMinimumBoxSize);
- cropBoxFrame.size.height = MAX(cropBoxFrame.size.height, kTOCropViewMinimumBoxSize);
-
- _cropBoxFrame = cropBoxFrame;
-
- self.foregroundContainerView.frame = _cropBoxFrame; //set the clipping view to match the new rect
- self.gridOverlayView.frame = _cropBoxFrame; //set the new overlay view to match the same region
-
- //reset the scroll view insets to match the region of the new crop rect
- self.scrollView.contentInset = (UIEdgeInsets){CGRectGetMinY(_cropBoxFrame),
- CGRectGetMinX(_cropBoxFrame),
- CGRectGetMaxY(self.bounds) - CGRectGetMaxY(_cropBoxFrame),
- CGRectGetMaxX(self.bounds) - CGRectGetMaxX(_cropBoxFrame)};
- //if necessary, work out the new minimum size of the scroll view so it fills the crop box
- CGSize imageSize = self.backgroundContainerView.bounds.size;
- CGFloat scale = MAX(cropBoxFrame.size.height/imageSize.height, cropBoxFrame.size.width/imageSize.width);
- self.scrollView.minimumZoomScale = scale;
-
- //make sure content isn't smaller than the crop box
- CGSize size = self.scrollView.contentSize;
- size.width = floorf(size.width);
- size.height = floorf(size.height);
- //self.backgroundContainerView.frame = (CGRect){CGPointZero, size};
- self.scrollView.contentSize = size;\
-
- //IMPORTANT: Force the scroll view to update its content after changing the zoom scale
- self.scrollView.zoomScale = self.scrollView.zoomScale;
-
- [self matchForegroundToBackground]; //re-align the background content to match
- }
- - (void)setEditing:(BOOL)editing
- {
- [self setEditing:editing animated:NO];
- }
- - (void)setSimpleMode:(BOOL)simpleMode
- {
- [self setSimpleMode:simpleMode animated:NO];
- }
- - (BOOL)cropBoxAspectRatioIsPortrait
- {
- CGRect cropFrame = self.cropBoxFrame;
- return CGRectGetWidth(cropFrame) < CGRectGetHeight(cropFrame);
- }
- - (CGRect)croppedImageFrame
- {
- CGSize imageSize = self.imageSize;
- CGSize contentSize = self.scrollView.contentSize;
- CGRect cropBoxFrame = self.cropBoxFrame;
- CGPoint contentOffset = self.scrollView.contentOffset;
- UIEdgeInsets edgeInsets = self.scrollView.contentInset;
-
- CGRect frame = CGRectZero;
- frame.origin.x = floorf((contentOffset.x + edgeInsets.left) * (imageSize.width / contentSize.width));
- frame.origin.x = MAX(0, frame.origin.x);
-
- frame.origin.y = floorf((contentOffset.y + edgeInsets.top) * (imageSize.height / contentSize.height));
- frame.origin.y = MAX(0, frame.origin.y);
-
- frame.size.width = ceilf(cropBoxFrame.size.width * (imageSize.width / contentSize.width));
- frame.size.width = MIN(imageSize.width, frame.size.width);
-
- frame.size.height = ceilf(cropBoxFrame.size.height * (imageSize.height / contentSize.height));
- frame.size.height = MIN(imageSize.height, frame.size.height);
-
- return frame;
- }
- - (void)setCroppingViewsHidden:(BOOL)hidden
- {
- [self setCroppingViewsHidden:hidden animated:NO];
- }
- - (void)setCroppingViewsHidden:(BOOL)hidden animated:(BOOL)animated
- {
- if (_croppingViewsHidden == hidden)
- return;
-
- _croppingViewsHidden = hidden;
-
- CGFloat alpha = hidden ? 0.0f : 1.0f;
-
- if (animated == NO) {
- self.backgroundImageView.alpha = alpha;
- self.translucencyView.alpha = alpha;
- self.foregroundContainerView.alpha = alpha;
- self.gridOverlayView.alpha = alpha;
- return;
- }
-
- self.foregroundContainerView.alpha = alpha;
- self.backgroundImageView.alpha = alpha;
-
- [UIView animateWithDuration:0.5f animations:^{
- self.translucencyView.alpha = alpha;
- self.gridOverlayView.alpha = alpha;
- }];
- }
- - (void)setGridOverlayHidden:(BOOL)gridOverlayHidden
- {
- [self setGridOverlayHidden:_gridOverlayHidden animated:NO];
- }
- - (void)setGridOverlayHidden:(BOOL)gridOverlayHidden animated:(BOOL)animated
- {
- _gridOverlayHidden = gridOverlayHidden;
- self.gridOverlayView.alpha = gridOverlayHidden ? 1.0f : 0.0f;
-
- [UIView animateWithDuration:0.4f animations:^{
- self.gridOverlayView.alpha = gridOverlayHidden ? 0.0f : 1.0f;
- }];
- }
- - (CGRect)imageViewFrame
- {
- CGRect frame = CGRectZero;
- frame.origin.x = -self.scrollView.contentOffset.x;
- frame.origin.y = -self.scrollView.contentOffset.y;
- frame.size = self.scrollView.contentSize;
- return frame;
- }
- #pragma mark - Editing Mode -
- - (void)startEditing
- {
- [self cancelResetTimer];
- [self setEditing:YES animated:YES];
- }
- - (void)setEditing:(BOOL)editing animated:(BOOL)animated
- {
- if (editing == _editing)
- return;
-
- _editing = editing;
-
- [self.gridOverlayView setGridHidden:!editing animated:animated];
-
- if (editing == NO) {
- [self moveCroppedContentToCenterAnimated:animated];
- self.cropBoxLastEditedSize = self.cropBoxFrame.size;
- self.cropBoxLastEditedAngle = self.angle;
- }
-
- if (animated == NO) {
- self.translucencyView.alpha = editing ? 0.0f : 1.0f;
- return;
- }
-
- [UIView animateWithDuration:editing?0.2f:0.35f animations:^{
- self.translucencyView.alpha = editing ? 0.0f : 1.0f;
- }];
- }
- - (void)moveCroppedContentToCenterAnimated:(BOOL)animated
- {
- if (self.simpleMode)
- return;
-
- CGRect contentRect = self.contentBounds;
- CGRect cropFrame = self.cropBoxFrame;
-
- CGPoint focusPoint = (CGPoint){CGRectGetMidX(cropFrame), CGRectGetMidY(cropFrame)};
- CGPoint midPoint = (CGPoint){CGRectGetMidX(contentRect), CGRectGetMidY(contentRect)};
-
- //The scale we need to scale up the crop box to fit full screen
- CGFloat scale = MIN(CGRectGetWidth(contentRect)/CGRectGetWidth(cropFrame), CGRectGetHeight(contentRect)/CGRectGetHeight(cropFrame));
- cropFrame.size.width = floorf(cropFrame.size.width * scale);
- cropFrame.size.height = floorf(cropFrame.size.height * scale);
- cropFrame.origin.x = contentRect.origin.x + floorf((contentRect.size.width - cropFrame.size.width) * 0.5f);
- cropFrame.origin.y = contentRect.origin.y + floorf((contentRect.size.height - cropFrame.size.height) * 0.5f);
-
- //Work out the point on the scroll content that the focusPoint is aiming at
- CGPoint contentTargetPoint = CGPointZero;
- contentTargetPoint.x = ((focusPoint.x + self.scrollView.contentOffset.x) * scale);
- contentTargetPoint.y = ((focusPoint.y + self.scrollView.contentOffset.y) * scale);
-
- //Work out where the crop box is focusing, so we can re-align to center that point
- __block CGPoint offset = CGPointZero;
- offset.x = -midPoint.x + contentTargetPoint.x;
- offset.y = -midPoint.y + contentTargetPoint.y;
-
- //clamp the content so it doesn't create any seams around the grid
- offset.x = MAX(-cropFrame.origin.x, offset.x);
- offset.y = MAX(-cropFrame.origin.y, offset.y);
-
- __weak typeof(self) weakSelf = self;
- void (^translateBlock)() = ^{
- typeof(self) strongSelf = weakSelf;
-
- strongSelf.scrollView.zoomScale *= scale;
-
- offset.x = MIN(-CGRectGetMaxX(cropFrame)+strongSelf.scrollView.contentSize.width, offset.x);
- offset.y = MIN(-CGRectGetMaxY(cropFrame)+strongSelf.scrollView.contentSize.height, offset.y);
- strongSelf.scrollView.contentOffset = offset;
-
- strongSelf.cropBoxFrame = cropFrame;
- };
-
- if (!animated) {
- translateBlock();
- return;
- }
-
- [UIView animateWithDuration:0.5f delay:0.0f usingSpringWithDamping:1.0f initialSpringVelocity:1.0f options:0 animations:translateBlock completion:nil];
- }
- - (void)setSimpleMode:(BOOL)simpleMode animated:(BOOL)animated
- {
- if (simpleMode == _simpleMode)
- return;
-
- _simpleMode = simpleMode;
-
- self.editing = NO;
-
- if (animated == NO) {
- self.translucencyView.alpha = simpleMode ? 0.0f : 1.0f;
-
- return;
- }
-
- [UIView animateWithDuration:0.35f animations:^{
- self.translucencyView.alpha = simpleMode ? 0.0f : 1.0f;
- }];
- }
- - (void)setAspectLockEnabledWithAspectRatio:(CGSize)aspectRatio animated:(BOOL)animated
- {
- if (aspectRatio.width < FLT_EPSILON && aspectRatio.height < FLT_EPSILON)
- aspectRatio = (CGSize){self.imageSize.width, self.imageSize.height};
-
- CGRect boundsFrame = self.contentBounds;
- CGRect cropBoxFrame = self.cropBoxFrame;
- CGPoint offset = self.scrollView.contentOffset;
-
- BOOL cropBoxIsPortrait = NO;
- if ((NSInteger)aspectRatio.width == 1 && (NSInteger)aspectRatio.height == 1)
- cropBoxIsPortrait = self.image.size.width > self.image.size.height;
- else
- cropBoxIsPortrait = aspectRatio.width < aspectRatio.height;
-
- BOOL zoomOut = NO;
- if (cropBoxIsPortrait) {
- CGFloat newWidth = cropBoxFrame.size.height * (aspectRatio.width/aspectRatio.height);
- CGFloat delta = cropBoxFrame.size.width - newWidth;
- cropBoxFrame.size.width = newWidth;
- offset.x += (delta * 0.5f);
-
- CGFloat boundsWidth = CGRectGetWidth(boundsFrame);
- if (newWidth > boundsWidth) {
- CGFloat scale = boundsWidth / newWidth;
- cropBoxFrame.size.height *= scale;
- cropBoxFrame.size.width = boundsWidth;
- zoomOut = YES;
- }
- }
- else {
- CGFloat newHeight = cropBoxFrame.size.width * (aspectRatio.height/aspectRatio.width);
- CGFloat delta = cropBoxFrame.size.height - newHeight;
- cropBoxFrame.size.height = newHeight;
- offset.y += (delta * 0.5f);
-
- CGFloat boundsHeight = CGRectGetHeight(boundsFrame);
- if (newHeight > boundsHeight) {
- CGFloat scale = boundsHeight / newHeight;
- cropBoxFrame.size.width *= scale;
- cropBoxFrame.size.height = boundsHeight;
- zoomOut = YES;
- }
- }
-
- self.aspectLockEnabled = YES;
-
- if (animated == NO) {
- self.scrollView.contentOffset = offset;
- self.cropBoxFrame = cropBoxFrame;
-
- if (zoomOut)
- self.scrollView.zoomScale = self.scrollView.minimumZoomScale;
-
- [self moveCroppedContentToCenterAnimated:NO];
- [self checkForCanReset];
- return;
- }
-
- [UIView animateWithDuration:0.5f delay:0.0 usingSpringWithDamping:1.0f initialSpringVelocity:0.7f options:0 animations:^{
- self.scrollView.contentOffset = offset;
- self.cropBoxFrame = cropBoxFrame;
- [self checkForCanReset];
-
- if (zoomOut)
- self.scrollView.zoomScale = self.scrollView.minimumZoomScale;
- [self moveCroppedContentToCenterAnimated:NO];
- } completion:nil];
- }
- - (void)rotateImageNinetyDegreesAnimated:(BOOL)animated
- {
- //Only allow one rotation animation at a time
- if (self.rotateAnimationInProgress)
- return;
-
- //Cancel any pending resizing timers
- if (self.resetTimer) {
- [self cancelResetTimer];
- [self.gridOverlayView setGridHidden:YES];
- [self moveCroppedContentToCenterAnimated:NO];
-
- self.cropBoxLastEditedAngle = self.angle;
- self.cropBoxLastEditedSize = self.cropBoxFrame.size;
- }
-
- //Work out the new angle, and wrap around once we exceed 360s
- NSInteger newAngle = self.angle;
- newAngle -= 90;
- if (newAngle <= -360)
- newAngle = 0;
-
- self.angle = newAngle;
-
- //Convert the new angle to radians
- CGFloat angleInRadians = 0.0f;
- switch (newAngle) {
- case -90:
- angleInRadians = M_PI_2;
- break;
- case -180:
- angleInRadians = M_PI;
- break;
- case -270:
- angleInRadians = (M_PI + M_PI_2);
- break;
- default:
- angleInRadians = (M_PI * 2);
- break;
- }
-
- // Set up the transformation matrix for the rotation
- CGAffineTransform rotation = CGAffineTransformRotate(CGAffineTransformIdentity, -angleInRadians);
-
- //Work out how much we'll need to scale everything to fit to the new rotation
- CGRect contentBounds = self.contentBounds;
- CGRect cropBoxFrame = self.cropBoxFrame;
- CGFloat scale = MIN(contentBounds.size.width / cropBoxFrame.size.height, contentBounds.size.height / cropBoxFrame.size.width);
-
- //Work out which section of the image we're currently focusing at
- CGPoint cropMidPoint = (CGPoint){CGRectGetMidX(cropBoxFrame), CGRectGetMidY(cropBoxFrame)};
- CGPoint cropTargetPoint = (CGPoint){cropMidPoint.x + self.scrollView.contentOffset.x, cropMidPoint.y + self.scrollView.contentOffset.y};
-
- //Work out the dimensions of the crop box when rotated
- CGRect newCropFrame = CGRectZero;
- if (self.angle == self.cropBoxLastEditedAngle || self.angle == ((self.cropBoxLastEditedAngle - 180) % 360)) {
- newCropFrame.size = self.cropBoxLastEditedSize;
- }
- else {
- newCropFrame.size = (CGSize){floorf(self.cropBoxLastEditedSize.height * scale), floorf(self.cropBoxLastEditedSize.width * scale)};
- //update last edited size
- self.cropBoxLastEditedSize = cropBoxFrame.size;
- }
-
- newCropFrame.origin.x = floorf((CGRectGetWidth(self.bounds) - newCropFrame.size.width) * 0.5f);
- newCropFrame.origin.y = floorf((CGRectGetHeight(self.bounds) - newCropFrame.size.height) * 0.5f);
-
- //If we're animated, generate a snapshot view that we'll animate in place of the real view
- UIView *snapshotView = nil;
- if (animated) {
- snapshotView = [self.foregroundContainerView snapshotViewAfterScreenUpdates:NO];
- self.rotateAnimationInProgress = YES;
- }
-
- //Re-adjust the scrolling dimensions of the scroll view to match the new size
- self.scrollView.minimumZoomScale *= scale;
- self.scrollView.zoomScale *= scale;
-
- //Rotate the background image view, inside its container view
- self.backgroundImageView.transform = rotation;
-
- //Flip the width/height of the container view so it matches the rotated image view's size
- CGSize containerSize = self.backgroundContainerView.frame.size;
- self.backgroundContainerView.frame = (CGRect){CGPointZero, {containerSize.height, containerSize.width}};
- self.backgroundImageView.frame = (CGRect){CGPointZero, self.backgroundImageView.frame.size};
- //Rotate the foreground image view to match
- self.foregroundContainerView.transform = CGAffineTransformIdentity;
- self.foregroundImageView.transform = rotation;
-
- //Flip the content size of the scroll view to match the rotated bounds
- self.scrollView.contentSize = self.backgroundContainerView.frame.size;
-
- //assign the new crop box frame and re-adjust the content to fill it
- self.cropBoxFrame = newCropFrame;
- [self moveCroppedContentToCenterAnimated:NO];
- newCropFrame = self.cropBoxFrame;
-
- //work out how to line up out point of interest into the middle of the crop box
- cropTargetPoint.x *= scale;
- cropTargetPoint.y *= scale;
-
- //swap the target dimensions to match a -90 degree rotation
- CGFloat swap = cropTargetPoint.x;
- cropTargetPoint.x = cropTargetPoint.y;
- cropTargetPoint.y = self.scrollView.contentSize.height - swap;
-
- //reapply the translated scroll offset to the scroll view
- CGPoint midPoint = {CGRectGetMidX(newCropFrame), CGRectGetMidY(newCropFrame)};
- CGPoint offset = CGPointZero;
- offset.x = floorf(-midPoint.x + cropTargetPoint.x);
- offset.y = floorf(-midPoint.y + cropTargetPoint.y);
- offset.x = MAX(-self.scrollView.contentInset.left, offset.x);
- offset.y = MAX(-self.scrollView.contentInset.top, offset.y);
-
- //if the scroll view's new scale is 1 and the new offset is equal to the old, will not trigger the delegate 'scrollViewDidScroll:'
- //so we should call the method manually to update the foregroundImageView's frame
- if (offset.x == self.scrollView.contentOffset.x && offset.y == self.scrollView.contentOffset.y && scale == 1) {
- [self matchForegroundToBackground];
- }
- self.scrollView.contentOffset = offset;
-
- //If we're animated, play an animation of the snapshot view rotating,
- //then fade it out over the live content
- if (animated) {
- snapshotView.center = self.scrollView.center;
- [self addSubview:snapshotView];
-
- self.backgroundContainerView.hidden = YES;
- self.foregroundContainerView.hidden = YES;
- self.translucencyView.hidden = YES;
- self.gridOverlayView.hidden = YES;
-
- [UIView animateWithDuration:0.45f delay:0.0f usingSpringWithDamping:1.0f initialSpringVelocity:0.8f options:0 animations:^{
- CGAffineTransform transform = CGAffineTransformRotate(CGAffineTransformIdentity, -M_PI_2);
- transform = CGAffineTransformScale(transform, scale, scale);
- snapshotView.transform = transform;
-
- } completion:^(BOOL complete) {
- self.backgroundContainerView.hidden = NO;
- self.foregroundContainerView.hidden = NO;
- self.translucencyView.hidden = NO;
- self.gridOverlayView.hidden = NO;
-
- self.backgroundContainerView.alpha = 0.0f;
- self.gridOverlayView.alpha = 0.0f;
-
- self.translucencyView.alpha = 1.0f;
-
- [UIView animateWithDuration:0.45f animations:^{
- snapshotView.alpha = 0.0f;
- self.backgroundContainerView.alpha = 1.0f;
- self.gridOverlayView.alpha = 1.0f;
- } completion:^(BOOL complete) {
- self.rotateAnimationInProgress = NO;
- [snapshotView removeFromSuperview];
- }];
- }];
- }
-
- [self checkForCanReset];
- }
- #pragma mark - Resettable State -
- - (void)checkForCanReset
- {
- BOOL canReset = NO;
-
- if (self.angle != 0) { //Image has been rotated
- canReset = YES;
- }
- else if (self.scrollView.zoomScale > self.scrollView.minimumZoomScale + FLT_EPSILON) { //image has been zoomed in
- canReset = YES;
- }
- else if ((NSInteger)floorf(self.cropBoxFrame.size.width) != (NSInteger)floorf(self.originalCropBoxSize.width) || (NSInteger)floorf(self.cropBoxFrame.size.height) != (NSInteger)floorf(self.originalCropBoxSize.height)) { //crop box has been changed
- canReset = YES;
- }
-
- if (canReset && self.canReset == NO) {
- self.canReset = YES;
-
- if ([self.delegate respondsToSelector:@selector(cropViewDidBecomeResettable:)])
- [self.delegate cropViewDidBecomeResettable:self];
- }
- else if (!canReset && self.canReset) {
- self.canReset = NO;
-
- if ([self.delegate respondsToSelector:@selector(cropViewDidBecomeNonResettable:)])
- [self.delegate cropViewDidBecomeNonResettable:self];
- }
- }
- #pragma mark - Convienience Methods -
- - (CGRect)contentBounds
- {
- CGRect contentRect = CGRectZero;
- contentRect.origin.x = kTOCropViewPadding + self.cropRegionInsets.left;
- contentRect.origin.y = kTOCropViewPadding + self.cropRegionInsets.top;
- contentRect.size.width = CGRectGetWidth(self.bounds) - ((kTOCropViewPadding * 2) + self.cropRegionInsets.left + self.cropRegionInsets.right);
- contentRect.size.height = CGRectGetHeight(self.bounds) - ((kTOCropViewPadding * 2) + self.cropRegionInsets.top + self.cropRegionInsets.bottom);
- return contentRect;
- }
- - (CGSize)imageSize
- {
- if (self.angle == -90 || self.angle == -270)
- return (CGSize){self.image.size.height, self.image.size.width};
- return (CGSize){self.image.size.width, self.image.size.height};
- }
- @end
|