verdnatura-chat/ios/Pods/RSKImageCropper/RSKImageCropper/RSKImageCropViewController.m

1060 lines
42 KiB
Objective-C

//
// RSKImageCropViewController.m
//
// Copyright (c) 2014-present Ruslan Skorb, http://ruslanskorb.com/
//
// 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 "RSKImageCropViewController.h"
#import "RSKTouchView.h"
#import "RSKImageScrollView.h"
#import "RSKInternalUtility.h"
#import "UIImage+RSKImageCropper.h"
#import "CGGeometry+RSKImageCropper.h"
#import "UIApplication+RSKImageCropper.h"
static const CGFloat kResetAnimationDuration = 0.4;
static const CGFloat kLayoutImageScrollViewAnimationDuration = 0.25;
@interface RSKImageCropViewController () <UIGestureRecognizerDelegate>
@property (assign, nonatomic) BOOL originalNavigationControllerNavigationBarHidden;
@property (strong, nonatomic) UIImage *originalNavigationControllerNavigationBarShadowImage;
@property (copy, nonatomic) UIColor *originalNavigationControllerViewBackgroundColor;
@property (assign, nonatomic) BOOL originalStatusBarHidden;
@property (strong, nonatomic) RSKImageScrollView *imageScrollView;
@property (strong, nonatomic) RSKTouchView *overlayView;
@property (strong, nonatomic) CAShapeLayer *maskLayer;
@property (assign, nonatomic) CGRect maskRect;
@property (copy, nonatomic) UIBezierPath *maskPath;
@property (readonly, nonatomic) CGRect rectForMaskPath;
@property (readonly, nonatomic) CGRect rectForClipPath;
@property (readonly, nonatomic) CGRect imageRect;
@property (strong, nonatomic) UILabel *moveAndScaleLabel;
@property (strong, nonatomic) UIButton *cancelButton;
@property (strong, nonatomic) UIButton *chooseButton;
@property (strong, nonatomic) UITapGestureRecognizer *doubleTapGestureRecognizer;
@property (strong, nonatomic) UIRotationGestureRecognizer *rotationGestureRecognizer;
@property (assign, nonatomic) BOOL didSetupConstraints;
@property (strong, nonatomic) NSLayoutConstraint *moveAndScaleLabelTopConstraint;
@property (strong, nonatomic) NSLayoutConstraint *cancelButtonBottomConstraint;
@property (strong, nonatomic) NSLayoutConstraint *cancelButtonLeadingConstraint;
@property (strong, nonatomic) NSLayoutConstraint *chooseButtonBottomConstraint;
@property (strong, nonatomic) NSLayoutConstraint *chooseButtonTrailingConstraint;
@end
@implementation RSKImageCropViewController
#pragma mark - Lifecycle
- (instancetype)init
{
self = [super init];
if (self) {
_avoidEmptySpaceAroundImage = NO;
_alwaysBounceVertical = NO;
_alwaysBounceHorizontal = NO;
_applyMaskToCroppedImage = NO;
_maskLayerLineWidth = 1.0;
_rotationEnabled = NO;
_cropMode = RSKImageCropModeCircle;
_portraitCircleMaskRectInnerEdgeInset = 15.0f;
_portraitSquareMaskRectInnerEdgeInset = 20.0f;
_portraitMoveAndScaleLabelTopAndCropViewTopVerticalSpace = 64.0f;
_portraitCropViewBottomAndCancelButtonBottomVerticalSpace = 21.0f;
_portraitCropViewBottomAndChooseButtonBottomVerticalSpace = 21.0f;
_portraitCancelButtonLeadingAndCropViewLeadingHorizontalSpace = 13.0f;
_portraitCropViewTrailingAndChooseButtonTrailingHorizontalSpace = 13.0;
_landscapeCircleMaskRectInnerEdgeInset = 45.0f;
_landscapeSquareMaskRectInnerEdgeInset = 45.0f;
_landscapeMoveAndScaleLabelTopAndCropViewTopVerticalSpace = 12.0f;
_landscapeCropViewBottomAndCancelButtonBottomVerticalSpace = 12.0f;
_landscapeCropViewBottomAndChooseButtonBottomVerticalSpace = 12.0f;
_landscapeCancelButtonLeadingAndCropViewLeadingHorizontalSpace = 13.0;
_landscapeCropViewTrailingAndChooseButtonTrailingHorizontalSpace = 13.0;
}
return self;
}
- (instancetype)initWithImage:(UIImage *)originalImage
{
self = [self init];
if (self) {
_originalImage = originalImage;
}
return self;
}
- (instancetype)initWithImage:(UIImage *)originalImage cropMode:(RSKImageCropMode)cropMode
{
self = [self initWithImage:originalImage];
if (self) {
_cropMode = cropMode;
}
return self;
}
- (BOOL)prefersStatusBarHidden
{
return YES;
}
- (void)viewDidLoad
{
[super viewDidLoad];
if ([self respondsToSelector:@selector(edgesForExtendedLayout)]) {
self.edgesForExtendedLayout = UIRectEdgeNone;
}
if (@available(iOS 11.0, *)) {
self.imageScrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}
else if ([self respondsToSelector:@selector(automaticallyAdjustsScrollViewInsets)] == YES) {
self.automaticallyAdjustsScrollViewInsets = NO;
}
self.view.backgroundColor = [UIColor blackColor];
self.view.clipsToBounds = YES;
[self.view addSubview:self.imageScrollView];
[self.view addSubview:self.overlayView];
[self.view addSubview:self.moveAndScaleLabel];
[self.view addSubview:self.cancelButton];
[self.view addSubview:self.chooseButton];
[self.view addGestureRecognizer:self.doubleTapGestureRecognizer];
[self.view addGestureRecognizer:self.rotationGestureRecognizer];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if ([self respondsToSelector:@selector(prefersStatusBarHidden)] == NO) {
UIApplication *application = [UIApplication rsk_sharedApplication];
if (application) {
self.originalStatusBarHidden = application.statusBarHidden;
[application setStatusBarHidden:YES];
}
}
self.originalNavigationControllerNavigationBarHidden = self.navigationController.navigationBarHidden;
[self.navigationController setNavigationBarHidden:YES animated:NO];
self.originalNavigationControllerNavigationBarShadowImage = self.navigationController.navigationBar.shadowImage;
self.navigationController.navigationBar.shadowImage = nil;
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
self.originalNavigationControllerViewBackgroundColor = self.navigationController.view.backgroundColor;
self.navigationController.view.backgroundColor = [UIColor blackColor];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
if ([self respondsToSelector:@selector(prefersStatusBarHidden)] == NO) {
UIApplication *application = [UIApplication rsk_sharedApplication];
if (application) {
[application setStatusBarHidden:self.originalStatusBarHidden];
}
}
[self.navigationController setNavigationBarHidden:self.originalNavigationControllerNavigationBarHidden animated:animated];
self.navigationController.navigationBar.shadowImage = self.originalNavigationControllerNavigationBarShadowImage;
self.navigationController.view.backgroundColor = self.originalNavigationControllerViewBackgroundColor;
}
- (void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
[self updateMaskRect];
[self layoutImageScrollView];
[self layoutOverlayView];
[self updateMaskPath];
[self.view setNeedsUpdateConstraints];
}
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
if (!self.imageScrollView.zoomView) {
[self displayImage];
}
}
- (void)updateViewConstraints
{
[super updateViewConstraints];
if (!self.didSetupConstraints) {
// ---------------------------
// The label "Move and Scale".
// ---------------------------
NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:self.moveAndScaleLabel attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual
toItem:self.view attribute:NSLayoutAttributeCenterX multiplier:1.0f
constant:0.0f];
[self.view addConstraint:constraint];
CGFloat constant = self.portraitMoveAndScaleLabelTopAndCropViewTopVerticalSpace;
self.moveAndScaleLabelTopConstraint = [NSLayoutConstraint constraintWithItem:self.moveAndScaleLabel attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual
toItem:self.view attribute:NSLayoutAttributeTop multiplier:1.0f
constant:constant];
[self.view addConstraint:self.moveAndScaleLabelTopConstraint];
// --------------------
// The button "Cancel".
// --------------------
constant = self.portraitCancelButtonLeadingAndCropViewLeadingHorizontalSpace;
self.cancelButtonLeadingConstraint = [NSLayoutConstraint constraintWithItem:self.cancelButton attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual
toItem:self.view attribute:NSLayoutAttributeLeading multiplier:1.0f
constant:constant];
[self.view addConstraint:self.cancelButtonLeadingConstraint];
constant = self.portraitCropViewBottomAndCancelButtonBottomVerticalSpace;
self.cancelButtonBottomConstraint = [NSLayoutConstraint constraintWithItem:self.view attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual
toItem:self.cancelButton attribute:NSLayoutAttributeBottom multiplier:1.0f
constant:constant];
[self.view addConstraint:self.cancelButtonBottomConstraint];
// --------------------
// The button "Choose".
// --------------------
constant = self.portraitCropViewTrailingAndChooseButtonTrailingHorizontalSpace;
self.chooseButtonTrailingConstraint = [NSLayoutConstraint constraintWithItem:self.view attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual
toItem:self.chooseButton attribute:NSLayoutAttributeTrailing multiplier:1.0f
constant:constant];
[self.view addConstraint:self.chooseButtonTrailingConstraint];
constant = self.portraitCropViewBottomAndChooseButtonBottomVerticalSpace;
self.chooseButtonBottomConstraint = [NSLayoutConstraint constraintWithItem:self.view attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual
toItem:self.chooseButton attribute:NSLayoutAttributeBottom multiplier:1.0f
constant:constant];
[self.view addConstraint:self.chooseButtonBottomConstraint];
self.didSetupConstraints = YES;
} else {
if ([self isPortraitInterfaceOrientation]) {
self.moveAndScaleLabelTopConstraint.constant = self.portraitMoveAndScaleLabelTopAndCropViewTopVerticalSpace;
self.cancelButtonBottomConstraint.constant = self.portraitCropViewBottomAndCancelButtonBottomVerticalSpace;
self.cancelButtonLeadingConstraint.constant = self.portraitCancelButtonLeadingAndCropViewLeadingHorizontalSpace;
self.chooseButtonBottomConstraint.constant = self.portraitCropViewBottomAndChooseButtonBottomVerticalSpace;
self.chooseButtonTrailingConstraint.constant = self.portraitCropViewTrailingAndChooseButtonTrailingHorizontalSpace;
} else {
self.moveAndScaleLabelTopConstraint.constant = self.landscapeMoveAndScaleLabelTopAndCropViewTopVerticalSpace;
self.cancelButtonBottomConstraint.constant = self.landscapeCropViewBottomAndCancelButtonBottomVerticalSpace;
self.cancelButtonLeadingConstraint.constant = self.landscapeCancelButtonLeadingAndCropViewLeadingHorizontalSpace;
self.chooseButtonBottomConstraint.constant = self.landscapeCropViewBottomAndChooseButtonBottomVerticalSpace;
self.chooseButtonTrailingConstraint.constant = self.landscapeCropViewTrailingAndChooseButtonTrailingHorizontalSpace;
}
}
}
#pragma mark - Custom Accessors
- (RSKImageScrollView *)imageScrollView
{
if (!_imageScrollView) {
_imageScrollView = [[RSKImageScrollView alloc] init];
_imageScrollView.clipsToBounds = NO;
_imageScrollView.aspectFill = self.avoidEmptySpaceAroundImage;
_imageScrollView.alwaysBounceHorizontal = self.alwaysBounceHorizontal;
_imageScrollView.alwaysBounceVertical = self.alwaysBounceVertical;
}
return _imageScrollView;
}
- (RSKTouchView *)overlayView
{
if (!_overlayView) {
_overlayView = [[RSKTouchView alloc] init];
_overlayView.receiver = self.imageScrollView;
[_overlayView.layer addSublayer:self.maskLayer];
}
return _overlayView;
}
- (CAShapeLayer *)maskLayer
{
if (!_maskLayer) {
_maskLayer = [CAShapeLayer layer];
_maskLayer.fillRule = kCAFillRuleEvenOdd;
_maskLayer.fillColor = self.maskLayerColor.CGColor;
_maskLayer.lineWidth = self.maskLayerLineWidth;
_maskLayer.strokeColor = self.maskLayerStrokeColor.CGColor;
}
return _maskLayer;
}
- (UIColor *)maskLayerColor
{
if (!_maskLayerColor) {
_maskLayerColor = [UIColor colorWithRed:0.0f green:0.0f blue:0.0f alpha:0.7f];
}
return _maskLayerColor;
}
- (UILabel *)moveAndScaleLabel
{
if (!_moveAndScaleLabel) {
_moveAndScaleLabel = [[UILabel alloc] init];
_moveAndScaleLabel.translatesAutoresizingMaskIntoConstraints = NO;
_moveAndScaleLabel.backgroundColor = [UIColor clearColor];
_moveAndScaleLabel.text = RSKLocalizedString(@"Move and Scale", @"Move and Scale label");
_moveAndScaleLabel.textColor = [UIColor whiteColor];
_moveAndScaleLabel.opaque = NO;
}
return _moveAndScaleLabel;
}
- (UIButton *)cancelButton
{
if (!_cancelButton) {
_cancelButton = [[UIButton alloc] init];
_cancelButton.translatesAutoresizingMaskIntoConstraints = NO;
[_cancelButton setTitle:RSKLocalizedString(@"Cancel", @"Cancel button") forState:UIControlStateNormal];
[_cancelButton addTarget:self action:@selector(onCancelButtonTouch:) forControlEvents:UIControlEventTouchUpInside];
_cancelButton.opaque = NO;
}
return _cancelButton;
}
- (UIButton *)chooseButton
{
if (!_chooseButton) {
_chooseButton = [[UIButton alloc] init];
_chooseButton.translatesAutoresizingMaskIntoConstraints = NO;
[_chooseButton setTitle:RSKLocalizedString(@"Choose", @"Choose button") forState:UIControlStateNormal];
[_chooseButton addTarget:self action:@selector(onChooseButtonTouch:) forControlEvents:UIControlEventTouchUpInside];
_chooseButton.opaque = NO;
}
return _chooseButton;
}
- (UITapGestureRecognizer *)doubleTapGestureRecognizer
{
if (!_doubleTapGestureRecognizer) {
_doubleTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleDoubleTap:)];
_doubleTapGestureRecognizer.delaysTouchesEnded = NO;
_doubleTapGestureRecognizer.numberOfTapsRequired = 2;
_doubleTapGestureRecognizer.delegate = self;
}
return _doubleTapGestureRecognizer;
}
- (UIRotationGestureRecognizer *)rotationGestureRecognizer
{
if (!_rotationGestureRecognizer) {
_rotationGestureRecognizer = [[UIRotationGestureRecognizer alloc] initWithTarget:self action:@selector(handleRotation:)];
_rotationGestureRecognizer.delaysTouchesEnded = NO;
_rotationGestureRecognizer.delegate = self;
_rotationGestureRecognizer.enabled = self.isRotationEnabled;
}
return _rotationGestureRecognizer;
}
- (CGRect)imageRect
{
float zoomScale = 1.0 / self.imageScrollView.zoomScale;
CGRect imageRect = CGRectZero;
imageRect.origin.x = self.imageScrollView.contentOffset.x * zoomScale;
imageRect.origin.y = self.imageScrollView.contentOffset.y * zoomScale;
imageRect.size.width = CGRectGetWidth(self.imageScrollView.bounds) * zoomScale;
imageRect.size.height = CGRectGetHeight(self.imageScrollView.bounds) * zoomScale;
imageRect = RSKRectNormalize(imageRect);
CGSize imageSize = self.originalImage.size;
CGFloat x = CGRectGetMinX(imageRect);
CGFloat y = CGRectGetMinY(imageRect);
CGFloat width = CGRectGetWidth(imageRect);
CGFloat height = CGRectGetHeight(imageRect);
UIImageOrientation imageOrientation = self.originalImage.imageOrientation;
if (imageOrientation == UIImageOrientationRight || imageOrientation == UIImageOrientationRightMirrored) {
imageRect.origin.x = y;
imageRect.origin.y = floor(imageSize.width - CGRectGetWidth(imageRect) - x);
imageRect.size.width = height;
imageRect.size.height = width;
} else if (imageOrientation == UIImageOrientationLeft || imageOrientation == UIImageOrientationLeftMirrored) {
imageRect.origin.x = floor(imageSize.height - CGRectGetHeight(imageRect) - y);
imageRect.origin.y = x;
imageRect.size.width = height;
imageRect.size.height = width;
} else if (imageOrientation == UIImageOrientationDown || imageOrientation == UIImageOrientationDownMirrored) {
imageRect.origin.x = floor(imageSize.width - CGRectGetWidth(imageRect) - x);
imageRect.origin.y = floor(imageSize.height - CGRectGetHeight(imageRect) - y);
}
CGFloat imageScale = self.originalImage.scale;
imageRect = CGRectApplyAffineTransform(imageRect, CGAffineTransformMakeScale(imageScale, imageScale));
return imageRect;
}
- (CGRect)cropRect
{
CGRect maskRect = self.maskRect;
CGFloat rotationAngle = self.rotationAngle;
CGRect rotatedImageScrollViewFrame = self.imageScrollView.frame;
float zoomScale = 1.0 / self.imageScrollView.zoomScale;
CGAffineTransform imageScrollViewTransform = self.imageScrollView.transform;
self.imageScrollView.transform = CGAffineTransformIdentity;
CGPoint imageScrollViewContentOffset = self.imageScrollView.contentOffset;
CGRect imageScrollViewFrame = self.imageScrollView.frame;
self.imageScrollView.frame = self.maskRect;
CGRect imageFrame = CGRectZero;
imageFrame.origin.x = CGRectGetMinX(maskRect) - self.imageScrollView.contentOffset.x;
imageFrame.origin.y = CGRectGetMinY(maskRect) - self.imageScrollView.contentOffset.y;
imageFrame.size = self.imageScrollView.contentSize;
CGFloat tx = CGRectGetMinX(imageFrame) + self.imageScrollView.contentOffset.x + CGRectGetWidth(maskRect) * 0.5f;
CGFloat ty = CGRectGetMinY(imageFrame) + self.imageScrollView.contentOffset.y + CGRectGetHeight(maskRect) * 0.5f;
CGFloat sx = CGRectGetWidth(rotatedImageScrollViewFrame) / CGRectGetWidth(imageScrollViewFrame);
CGFloat sy = CGRectGetHeight(rotatedImageScrollViewFrame) / CGRectGetHeight(imageScrollViewFrame);
CGAffineTransform t1 = CGAffineTransformMakeTranslation(-tx, -ty);
CGAffineTransform t2 = CGAffineTransformMakeRotation(rotationAngle);
CGAffineTransform t3 = CGAffineTransformMakeScale(sx, sy);
CGAffineTransform t4 = CGAffineTransformMakeTranslation(tx, ty);
CGAffineTransform t1t2 = CGAffineTransformConcat(t1, t2);
CGAffineTransform t1t2t3 = CGAffineTransformConcat(t1t2, t3);
CGAffineTransform t1t2t3t4 = CGAffineTransformConcat(t1t2t3, t4);
imageFrame = CGRectApplyAffineTransform(imageFrame, t1t2t3t4);
CGRect cropRect = CGRectMake(0.0, 0.0, CGRectGetWidth(maskRect), CGRectGetHeight(maskRect));
cropRect.origin.x = -CGRectGetMinX(imageFrame) + CGRectGetMinX(maskRect);
cropRect.origin.y = -CGRectGetMinY(imageFrame) + CGRectGetMinY(maskRect);
cropRect = CGRectApplyAffineTransform(cropRect, CGAffineTransformMakeScale(zoomScale, zoomScale));
cropRect = RSKRectNormalize(cropRect);
CGFloat imageScale = self.originalImage.scale;
cropRect = CGRectApplyAffineTransform(cropRect, CGAffineTransformMakeScale(imageScale, imageScale));
self.imageScrollView.frame = imageScrollViewFrame;
self.imageScrollView.contentOffset = imageScrollViewContentOffset;
self.imageScrollView.transform = imageScrollViewTransform;
return cropRect;
}
- (CGRect)rectForClipPath
{
if (!self.maskLayerStrokeColor) {
return self.overlayView.frame;
} else {
CGFloat maskLayerLineHalfWidth = self.maskLayerLineWidth / 2.0;
return CGRectInset(self.overlayView.frame, -maskLayerLineHalfWidth, -maskLayerLineHalfWidth);
}
}
- (CGRect)rectForMaskPath
{
if (!self.maskLayerStrokeColor) {
return self.maskRect;
} else {
CGFloat maskLayerLineHalfWidth = self.maskLayerLineWidth / 2.0;
return CGRectInset(self.maskRect, maskLayerLineHalfWidth, maskLayerLineHalfWidth);
}
}
- (CGFloat)rotationAngle
{
CGAffineTransform transform = self.imageScrollView.transform;
CGFloat rotationAngle = atan2(transform.b, transform.a);
return rotationAngle;
}
- (CGFloat)zoomScale
{
return self.imageScrollView.zoomScale;
}
- (void)setAvoidEmptySpaceAroundImage:(BOOL)avoidEmptySpaceAroundImage
{
if (_avoidEmptySpaceAroundImage != avoidEmptySpaceAroundImage) {
_avoidEmptySpaceAroundImage = avoidEmptySpaceAroundImage;
self.imageScrollView.aspectFill = avoidEmptySpaceAroundImage;
}
}
- (void)setAlwaysBounceVertical:(BOOL)alwaysBounceVertical
{
if (_alwaysBounceVertical != alwaysBounceVertical) {
_alwaysBounceVertical = alwaysBounceVertical;
self.imageScrollView.alwaysBounceVertical = alwaysBounceVertical;
}
}
- (void)setAlwaysBounceHorizontal:(BOOL)alwaysBounceHorizontal
{
if (_alwaysBounceHorizontal != alwaysBounceHorizontal) {
_alwaysBounceHorizontal = alwaysBounceHorizontal;
self.imageScrollView.alwaysBounceHorizontal = alwaysBounceHorizontal;
}
}
- (void)setCropMode:(RSKImageCropMode)cropMode
{
if (_cropMode != cropMode) {
_cropMode = cropMode;
if (self.imageScrollView.zoomView) {
[self reset:NO];
}
}
}
- (void)setOriginalImage:(UIImage *)originalImage
{
if (![_originalImage isEqual:originalImage]) {
_originalImage = originalImage;
if (self.isViewLoaded && self.view.window) {
[self displayImage];
}
}
}
- (void)setMaskPath:(UIBezierPath *)maskPath
{
if (![_maskPath isEqual:maskPath]) {
_maskPath = maskPath;
UIBezierPath *clipPath = [UIBezierPath bezierPathWithRect:self.rectForClipPath];
[clipPath appendPath:maskPath];
clipPath.usesEvenOddFillRule = YES;
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
pathAnimation.duration = [CATransaction animationDuration];
pathAnimation.timingFunction = [CATransaction animationTimingFunction];
[self.maskLayer addAnimation:pathAnimation forKey:@"path"];
self.maskLayer.path = [clipPath CGPath];
}
}
- (void)setRotationAngle:(CGFloat)rotationAngle
{
if (self.rotationAngle != rotationAngle) {
CGFloat rotation = (rotationAngle - self.rotationAngle);
CGAffineTransform transform = CGAffineTransformRotate(self.imageScrollView.transform, rotation);
self.imageScrollView.transform = transform;
[self layoutImageScrollView];
}
}
- (void)setRotationEnabled:(BOOL)rotationEnabled
{
if (_rotationEnabled != rotationEnabled) {
_rotationEnabled = rotationEnabled;
self.rotationGestureRecognizer.enabled = rotationEnabled;
}
}
- (void)setZoomScale:(CGFloat)zoomScale
{
self.imageScrollView.zoomScale = zoomScale;
}
#pragma mark - Action handling
- (void)onCancelButtonTouch:(UIBarButtonItem *)sender
{
[self cancelCrop];
}
- (void)onChooseButtonTouch:(UIBarButtonItem *)sender
{
[self cropImage];
}
- (void)handleDoubleTap:(UITapGestureRecognizer *)gestureRecognizer
{
[self reset:YES];
}
- (void)handleRotation:(UIRotationGestureRecognizer *)gestureRecognizer
{
CGFloat rotation = gestureRecognizer.rotation;
CGAffineTransform transform = CGAffineTransformRotate(self.imageScrollView.transform, rotation);
self.imageScrollView.transform = transform;
gestureRecognizer.rotation = 0;
if (gestureRecognizer.state == UIGestureRecognizerStateEnded) {
[UIView animateWithDuration:kLayoutImageScrollViewAnimationDuration
delay:0.0
options:UIViewAnimationOptionBeginFromCurrentState
animations:^{
[self layoutImageScrollView];
}
completion:nil];
}
}
- (void)zoomToRect:(CGRect)rect animated:(BOOL)animated
{
[self.imageScrollView zoomToRect:rect animated:animated];
}
#pragma mark - Public
- (BOOL)isPortraitInterfaceOrientation
{
return CGRectGetHeight(self.view.bounds) > CGRectGetWidth(self.view.bounds);
}
#pragma mark - Private
- (void)reset:(BOOL)animated
{
if (animated) {
[UIView beginAnimations:@"rsk_reset" context:NULL];
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
[UIView setAnimationDuration:kResetAnimationDuration];
[UIView setAnimationBeginsFromCurrentState:YES];
}
[self resetRotation];
[self resetZoomScale];
[self resetContentOffset];
if (animated) {
[UIView commitAnimations];
}
}
- (void)resetContentOffset
{
CGSize boundsSize = self.imageScrollView.bounds.size;
CGRect frameToCenter = self.imageScrollView.zoomView.frame;
CGPoint contentOffset;
if (CGRectGetWidth(frameToCenter) > boundsSize.width) {
contentOffset.x = (CGRectGetWidth(frameToCenter) - boundsSize.width) * 0.5f;
} else {
contentOffset.x = 0;
}
if (CGRectGetHeight(frameToCenter) > boundsSize.height) {
contentOffset.y = (CGRectGetHeight(frameToCenter) - boundsSize.height) * 0.5f;
} else {
contentOffset.y = 0;
}
self.imageScrollView.contentOffset = contentOffset;
}
- (void)resetRotation
{
[self setRotationAngle:0.0];
}
- (void)resetZoomScale
{
CGFloat zoomScale;
if (CGRectGetWidth(self.view.bounds) > CGRectGetHeight(self.view.bounds)) {
zoomScale = CGRectGetHeight(self.view.bounds) / self.originalImage.size.height;
} else {
zoomScale = CGRectGetWidth(self.view.bounds) / self.originalImage.size.width;
}
self.imageScrollView.zoomScale = zoomScale;
}
- (NSArray *)intersectionPointsOfLineSegment:(RSKLineSegment)lineSegment withRect:(CGRect)rect
{
RSKLineSegment top = RSKLineSegmentMake(CGPointMake(CGRectGetMinX(rect), CGRectGetMinY(rect)),
CGPointMake(CGRectGetMaxX(rect), CGRectGetMinY(rect)));
RSKLineSegment right = RSKLineSegmentMake(CGPointMake(CGRectGetMaxX(rect), CGRectGetMinY(rect)),
CGPointMake(CGRectGetMaxX(rect), CGRectGetMaxY(rect)));
RSKLineSegment bottom = RSKLineSegmentMake(CGPointMake(CGRectGetMinX(rect), CGRectGetMaxY(rect)),
CGPointMake(CGRectGetMaxX(rect), CGRectGetMaxY(rect)));
RSKLineSegment left = RSKLineSegmentMake(CGPointMake(CGRectGetMinX(rect), CGRectGetMinY(rect)),
CGPointMake(CGRectGetMinX(rect), CGRectGetMaxY(rect)));
CGPoint p0 = RSKLineSegmentIntersection(top, lineSegment);
CGPoint p1 = RSKLineSegmentIntersection(right, lineSegment);
CGPoint p2 = RSKLineSegmentIntersection(bottom, lineSegment);
CGPoint p3 = RSKLineSegmentIntersection(left, lineSegment);
NSMutableArray *intersectionPoints = [@[] mutableCopy];
if (!RSKPointIsNull(p0)) {
[intersectionPoints addObject:[NSValue valueWithCGPoint:p0]];
}
if (!RSKPointIsNull(p1)) {
[intersectionPoints addObject:[NSValue valueWithCGPoint:p1]];
}
if (!RSKPointIsNull(p2)) {
[intersectionPoints addObject:[NSValue valueWithCGPoint:p2]];
}
if (!RSKPointIsNull(p3)) {
[intersectionPoints addObject:[NSValue valueWithCGPoint:p3]];
}
return [intersectionPoints copy];
}
- (void)displayImage
{
if (self.originalImage) {
[self.imageScrollView displayImage:self.originalImage];
[self reset:NO];
if ([self.delegate respondsToSelector:@selector(imageCropViewControllerDidDisplayImage:)]) {
[self.delegate imageCropViewControllerDidDisplayImage:self];
}
}
}
- (void)centerImageScrollViewZoomView
{
// center imageScrollView.zoomView as it becomes smaller than the size of the screen
CGPoint contentOffset = self.imageScrollView.contentOffset;
// center vertically
if (self.imageScrollView.contentSize.height < CGRectGetHeight(self.imageScrollView.bounds)) {
contentOffset.y = -(CGRectGetHeight(self.imageScrollView.bounds) - self.imageScrollView.contentSize.height) * 0.5f;
}
// center horizontally
if (self.imageScrollView.contentSize.width < CGRectGetWidth(self.imageScrollView.bounds)) {
contentOffset.x = -(CGRectGetWidth(self.imageScrollView.bounds) - self.imageScrollView.contentSize.width) * 0.5f;;
}
self.imageScrollView.contentOffset = contentOffset;
}
- (void)layoutImageScrollView
{
CGRect frame = CGRectZero;
// The bounds of the image scroll view should always fill the mask area.
switch (self.cropMode) {
case RSKImageCropModeSquare: {
if (self.rotationAngle == 0.0) {
frame = self.maskRect;
} else {
// Step 1: Rotate the left edge of the initial rect of the image scroll view clockwise around the center by `rotationAngle`.
CGRect initialRect = self.maskRect;
CGFloat rotationAngle = self.rotationAngle;
CGPoint leftTopPoint = CGPointMake(initialRect.origin.x, initialRect.origin.y);
CGPoint leftBottomPoint = CGPointMake(initialRect.origin.x, initialRect.origin.y + initialRect.size.height);
RSKLineSegment leftLineSegment = RSKLineSegmentMake(leftTopPoint, leftBottomPoint);
CGPoint pivot = RSKRectCenterPoint(initialRect);
CGFloat alpha = fabs(rotationAngle);
RSKLineSegment rotatedLeftLineSegment = RSKLineSegmentRotateAroundPoint(leftLineSegment, pivot, alpha);
// Step 2: Find the points of intersection of the rotated edge with the initial rect.
NSArray *points = [self intersectionPointsOfLineSegment:rotatedLeftLineSegment withRect:initialRect];
// Step 3: If the number of intersection points more than one
// then the bounds of the rotated image scroll view does not completely fill the mask area.
// Therefore, we need to update the frame of the image scroll view.
// Otherwise, we can use the initial rect.
if (points.count > 1) {
// We have a right triangle.
// Step 4: Calculate the altitude of the right triangle.
if ((alpha > M_PI_2) && (alpha < M_PI)) {
alpha = alpha - M_PI_2;
} else if ((alpha > (M_PI + M_PI_2)) && (alpha < (M_PI + M_PI))) {
alpha = alpha - (M_PI + M_PI_2);
}
CGFloat sinAlpha = sin(alpha);
CGFloat cosAlpha = cos(alpha);
CGFloat hypotenuse = RSKPointDistance([points[0] CGPointValue], [points[1] CGPointValue]);
CGFloat altitude = hypotenuse * sinAlpha * cosAlpha;
// Step 5: Calculate the target width.
CGFloat initialWidth = CGRectGetWidth(initialRect);
CGFloat targetWidth = initialWidth + altitude * 2;
// Step 6: Calculate the target frame.
CGFloat scale = targetWidth / initialWidth;
CGPoint center = RSKRectCenterPoint(initialRect);
frame = RSKRectScaleAroundPoint(initialRect, center, scale, scale);
// Step 7: Avoid floats.
frame.origin.x = floor(CGRectGetMinX(frame));
frame.origin.y = floor(CGRectGetMinY(frame));
frame = CGRectIntegral(frame);
} else {
// Step 4: Use the initial rect.
frame = initialRect;
}
}
break;
}
case RSKImageCropModeCircle: {
frame = self.maskRect;
break;
}
case RSKImageCropModeCustom: {
frame = [self.dataSource imageCropViewControllerCustomMovementRect:self];
break;
}
}
CGAffineTransform transform = self.imageScrollView.transform;
self.imageScrollView.transform = CGAffineTransformIdentity;
self.imageScrollView.frame = frame;
[self centerImageScrollViewZoomView];
self.imageScrollView.transform = transform;
}
- (void)layoutOverlayView
{
CGRect frame = CGRectMake(0, 0, CGRectGetWidth(self.view.bounds) * 2, CGRectGetHeight(self.view.bounds) * 2);
self.overlayView.frame = frame;
}
- (void)updateMaskRect
{
switch (self.cropMode) {
case RSKImageCropModeCircle: {
CGFloat viewWidth = CGRectGetWidth(self.view.bounds);
CGFloat viewHeight = CGRectGetHeight(self.view.bounds);
CGFloat diameter;
if ([self isPortraitInterfaceOrientation]) {
diameter = MIN(viewWidth, viewHeight) - self.portraitCircleMaskRectInnerEdgeInset * 2;
} else {
diameter = MIN(viewWidth, viewHeight) - self.landscapeCircleMaskRectInnerEdgeInset * 2;
}
CGSize maskSize = CGSizeMake(diameter, diameter);
self.maskRect = CGRectMake((viewWidth - maskSize.width) * 0.5f,
(viewHeight - maskSize.height) * 0.5f,
maskSize.width,
maskSize.height);
break;
}
case RSKImageCropModeSquare: {
CGFloat viewWidth = CGRectGetWidth(self.view.bounds);
CGFloat viewHeight = CGRectGetHeight(self.view.bounds);
CGFloat length;
if ([self isPortraitInterfaceOrientation]) {
length = MIN(viewWidth, viewHeight) - self.portraitSquareMaskRectInnerEdgeInset * 2;
} else {
length = MIN(viewWidth, viewHeight) - self.landscapeSquareMaskRectInnerEdgeInset * 2;
}
CGSize maskSize = CGSizeMake(length, length);
self.maskRect = CGRectMake((viewWidth - maskSize.width) * 0.5f,
(viewHeight - maskSize.height) * 0.5f,
maskSize.width,
maskSize.height);
break;
}
case RSKImageCropModeCustom: {
self.maskRect = [self.dataSource imageCropViewControllerCustomMaskRect:self];
break;
}
}
}
- (void)updateMaskPath
{
switch (self.cropMode) {
case RSKImageCropModeCircle: {
self.maskPath = [UIBezierPath bezierPathWithOvalInRect:self.rectForMaskPath];
break;
}
case RSKImageCropModeSquare: {
self.maskPath = [UIBezierPath bezierPathWithRect:self.rectForMaskPath];
break;
}
case RSKImageCropModeCustom: {
self.maskPath = [self.dataSource imageCropViewControllerCustomMaskPath:self];
break;
}
}
}
- (UIImage *)imageWithImage:(UIImage *)image inRect:(CGRect)rect scale:(CGFloat)scale imageOrientation:(UIImageOrientation)imageOrientation
{
if (!image.images) {
CGImageRef cgImage = CGImageCreateWithImageInRect(image.CGImage, rect);
UIImage *image = [UIImage imageWithCGImage:cgImage scale:scale orientation:imageOrientation];
CGImageRelease(cgImage);
return image;
} else {
UIImage *animatedImage = image;
NSMutableArray *images = [NSMutableArray array];
for (UIImage *animatedImageImage in animatedImage.images) {
UIImage *image = [self imageWithImage:animatedImageImage inRect:rect scale:scale imageOrientation:imageOrientation];
[images addObject:image];
}
return [UIImage animatedImageWithImages:images duration:image.duration];
}
}
- (UIImage *)croppedImage:(UIImage *)originalImage cropMode:(RSKImageCropMode)cropMode cropRect:(CGRect)cropRect imageRect:(CGRect)imageRect rotationAngle:(CGFloat)rotationAngle zoomScale:(CGFloat)zoomScale maskPath:(UIBezierPath *)maskPath applyMaskToCroppedImage:(BOOL)applyMaskToCroppedImage
{
// Step 1: create an image using the data contained within the specified rect.
UIImage *image = [self imageWithImage:originalImage inRect:imageRect scale:originalImage.scale imageOrientation:originalImage.imageOrientation];
// Step 2: fix orientation of the image.
image = [image fixOrientation];
// Step 3: If current mode is `RSKImageCropModeSquare` and the original image is not rotated
// or mask should not be applied to the image after cropping and the original image is not rotated,
// we can return the image immediately.
// Otherwise, we must further process the image.
if ((cropMode == RSKImageCropModeSquare || !applyMaskToCroppedImage) && rotationAngle == 0.0) {
// Step 4: return the image immediately.
return image;
} else {
// Step 4: create a new context.
CGSize contextSize = cropRect.size;
UIGraphicsBeginImageContextWithOptions(contextSize, NO, originalImage.scale);
// Step 5: apply the mask if needed.
if (applyMaskToCroppedImage) {
// 5a: scale the mask to the size of the crop rect.
UIBezierPath *maskPathCopy = [maskPath copy];
CGFloat scale = 1.0 / zoomScale;
[maskPathCopy applyTransform:CGAffineTransformMakeScale(scale, scale)];
// 5b: center the mask.
CGPoint translation = CGPointMake(-CGRectGetMinX(maskPathCopy.bounds) + (CGRectGetWidth(cropRect) - CGRectGetWidth(maskPathCopy.bounds)) * 0.5f,
-CGRectGetMinY(maskPathCopy.bounds) + (CGRectGetHeight(cropRect) - CGRectGetHeight(maskPathCopy.bounds)) * 0.5f);
[maskPathCopy applyTransform:CGAffineTransformMakeTranslation(translation.x, translation.y)];
// 5c: apply the mask.
[maskPathCopy addClip];
}
// Step 6: rotate the image if needed.
if (rotationAngle != 0) {
image = [image rotateByAngle:rotationAngle];
}
// Step 7: draw the image.
CGPoint point = CGPointMake(floor((contextSize.width - image.size.width) * 0.5f),
floor((contextSize.height - image.size.height) * 0.5f));
[image drawAtPoint:point];
// Step 8: get the cropped image affter processing from the context.
UIImage *croppedImage = UIGraphicsGetImageFromCurrentImageContext();
// Step 9: remove the context.
UIGraphicsEndImageContext();
croppedImage = [UIImage imageWithCGImage:croppedImage.CGImage scale:originalImage.scale orientation:image.imageOrientation];
// Step 10: return the cropped image affter processing.
return croppedImage;
}
}
- (void)cropImage
{
if ([self.delegate respondsToSelector:@selector(imageCropViewController:willCropImage:)]) {
[self.delegate imageCropViewController:self willCropImage:self.originalImage];
}
UIImage *originalImage = self.originalImage;
RSKImageCropMode cropMode = self.cropMode;
CGRect cropRect = self.cropRect;
CGRect imageRect = self.imageRect;
CGFloat rotationAngle = self.rotationAngle;
CGFloat zoomScale = self.imageScrollView.zoomScale;
UIBezierPath *maskPath = self.maskPath;
BOOL applyMaskToCroppedImage = self.applyMaskToCroppedImage;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
UIImage *croppedImage = [self croppedImage:originalImage cropMode:cropMode cropRect:cropRect imageRect:imageRect rotationAngle:rotationAngle zoomScale:zoomScale maskPath:maskPath applyMaskToCroppedImage:applyMaskToCroppedImage];
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate imageCropViewController:self didCropImage:croppedImage usingCropRect:cropRect rotationAngle:rotationAngle];
});
});
}
- (void)cancelCrop
{
if ([self.delegate respondsToSelector:@selector(imageCropViewControllerDidCancelCrop:)]) {
[self.delegate imageCropViewControllerDidCancelCrop:self];
}
}
#pragma mark - UIGestureRecognizerDelegate
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return YES;
}
@end