1
0
mirror of https://github.com/danog/Telegram.git synced 2025-01-22 05:52:06 +01:00
Telegram/Telegraph/TGPhotoCropView.m
2015-10-01 19:19:52 +03:00

1291 lines
44 KiB
Objective-C

#import "TGPhotoCropView.h"
#import "TGPhotoEditorAnimation.h"
#import "TGPhotoEditorInterfaceAssets.h"
#import "UIControl+HitTestEdgeInsets.h"
#import "TGPhotoEditorUtils.h"
#import "TGImageUtils.h"
#import "TGTimer.h"
#import "TGPhotoCropScrollView.h"
#import "TGPhotoCropAreaView.h"
#import "TGPhotoCropRotationView.h"
const CGFloat TGPhotoCropViewOverscreenSize = 1000;
@interface TGPhotoCropView () <UIScrollViewDelegate>
{
CGSize _originalSize;
TGTimer *_confirmTimer;
bool _animatingConfirm;
bool _cropAreaChanged;
UIView *_overlayWrapperView;
UIView *_topOverlayView;
UIView *_leftOverlayView;
UIView *_rightOverlayView;
UIView *_bottomOverlayView;
UIControl *_areaWrapperView;
TGPhotoCropAreaView *_areaView;
CGRect _previousAreaFrame;
TGPhotoCropScrollView *_scrollView;
UIView *_contentWrapperView;
UIImageView *_backdropView;
UIImageView *_imageView;
UIView *_snapshotView;
CGSize _snapshotSize;
UIImage *_backdropImage;
bool _hasArbitraryRotation;
bool _animatingRotation;
UIView *_rotationSnapshotView;
TGPhotoCropRotationView *_rotationView;
UIInterfaceOrientation _previousInterfaceOrientation;
}
@end
@implementation TGPhotoCropView
- (instancetype)initWithOriginalSize:(CGSize)originalSize hasArbitraryRotation:(bool)hasArbitraryRotation
{
self = [self initWithFrame:CGRectZero];
if (self != nil)
{
_hasArbitraryRotation = hasArbitraryRotation;
_originalSize = originalSize;
_cropRect = CGRectMake(0.0f, 0.0f, originalSize.width, originalSize.height);
_cropOrientation = UIImageOrientationUp;
__weak TGPhotoCropView *weakSelf = self;
self.hitTestEdgeInsets = UIEdgeInsetsMake(-16, -100, -100, -100);
_areaWrapperView = [[UIControl alloc] initWithFrame:CGRectZero];
_areaWrapperView.hitTestEdgeInsets = UIEdgeInsetsMake(-16, -100, -100, -100);
[self addSubview:_areaWrapperView];
_scrollView = [[TGPhotoCropScrollView alloc] initWithFrame:CGRectZero];
_scrollView.shouldBeginChanging = ^bool
{
__strong TGPhotoCropView *strongSelf = weakSelf;
if (strongSelf == nil)
return true;
return !strongSelf->_animatingConfirm && !strongSelf->_areaView.isTracking && !strongSelf->_rotationView.isTracking;
};
_scrollView.didBeginChanging = ^
{
__strong TGPhotoCropView *strongSelf = weakSelf;
if (strongSelf == nil)
return;
[strongSelf resetBackdropViewsAnimated:true];
[strongSelf cancelConfirmCountdown];
[strongSelf->_areaView setGridMode:TGPhotoCropViewGridModeMajor animated:true];
if (strongSelf.interactionBegan != nil)
strongSelf.interactionBegan();
};
_scrollView.didEndChanging = ^
{
__strong TGPhotoCropView *strongSelf = weakSelf;
if (strongSelf == nil)
return;
[strongSelf startConfirmCountdown];
if (strongSelf.croppingChanged != nil)
strongSelf.croppingChanged();
};
[_areaWrapperView addSubview:_scrollView];
_overlayWrapperView = [[UIView alloc] initWithFrame:CGRectZero];
_overlayWrapperView.userInteractionEnabled = false;
[_areaWrapperView addSubview:_overlayWrapperView];
_topOverlayView = [[UIView alloc] initWithFrame:CGRectZero];
_topOverlayView.backgroundColor = [TGPhotoEditorInterfaceAssets cropTransparentOverlayColor];
[_overlayWrapperView addSubview:_topOverlayView];
_leftOverlayView = [[UIView alloc] initWithFrame:CGRectZero];
_leftOverlayView.backgroundColor = [TGPhotoEditorInterfaceAssets cropTransparentOverlayColor];
[_overlayWrapperView addSubview:_leftOverlayView];
_rightOverlayView = [[UIView alloc] initWithFrame:CGRectZero];
_rightOverlayView.backgroundColor = [TGPhotoEditorInterfaceAssets cropTransparentOverlayColor];
[_overlayWrapperView addSubview:_rightOverlayView];
_bottomOverlayView = [[UIView alloc] initWithFrame:CGRectZero];
_bottomOverlayView.backgroundColor = [TGPhotoEditorInterfaceAssets cropTransparentOverlayColor];
[_overlayWrapperView addSubview:_bottomOverlayView];
_areaView = [[TGPhotoCropAreaView alloc] initWithFrame:self.bounds];
_areaView.shouldBeginEditing = ^bool
{
__strong TGPhotoCropView *strongSelf = weakSelf;
if (strongSelf == nil)
return true;
return !strongSelf->_animatingConfirm && !strongSelf->_scrollView.isTracking && !strongSelf->_rotationView.isTracking;
};
_areaView.didBeginEditing = ^
{
__strong TGPhotoCropView *strongSelf = weakSelf;
if (strongSelf == nil)
return;
strongSelf->_previousAreaFrame = strongSelf->_areaView.frame;
[strongSelf resetBackdropViewsAnimated:true];
[strongSelf cancelConfirmCountdown];
[strongSelf->_areaView setGridMode:TGPhotoCropViewGridModeMajor animated:true];
[strongSelf setIntefaceHidden:true animated:true];
if (strongSelf.interactionBegan != nil)
strongSelf.interactionBegan();
};
_areaView.didEndEditing = ^
{
__strong TGPhotoCropView *strongSelf = weakSelf;
if (strongSelf == nil)
return;
[strongSelf startConfirmCountdown];
};
_areaView.areaChanged = ^
{
__strong TGPhotoCropView *strongSelf = weakSelf;
if (strongSelf == nil)
return;
[strongSelf handleCropAreaChanged];
};
[_areaWrapperView addSubview:_areaView];
if (_hasArbitraryRotation)
{
_rotationView = [[TGPhotoCropRotationView alloc] initWithFrame:CGRectZero];
_rotationView.hitTestEdgeInsets = UIEdgeInsetsMake(10, 10, 0, 10);
[self addSubview:_rotationView];
_rotationView.shouldBeginChanging = ^bool
{
__strong TGPhotoCropView *strongSelf = weakSelf;
if (strongSelf == nil)
return true;
return !strongSelf->_animatingConfirm && !strongSelf->_scrollView.isTracking && !strongSelf->_areaView.isTracking;
};
_rotationView.didBeginChanging = ^
{
__strong TGPhotoCropView *strongSelf = weakSelf;
if (strongSelf == nil)
return;
[strongSelf resetBackdropViewsAnimated:true];
[strongSelf cancelConfirmCountdown];
[strongSelf->_areaView setGridMode:TGPhotoCropViewGridModeMinor animated:true];
if (strongSelf.interactionBegan != nil)
strongSelf.interactionBegan();
};
_rotationView.didEndChanging = ^
{
__strong TGPhotoCropView *strongSelf = weakSelf;
if (strongSelf == nil)
return;
[strongSelf startConfirmCountdown];
[strongSelf->_areaView setGridMode:TGPhotoCropViewGridModeNone animated:true];
if (strongSelf.croppingChanged != nil)
strongSelf.croppingChanged();
if (strongSelf.interactionEnded != nil)
strongSelf.interactionEnded();
};
_rotationView.angleChanged = ^(CGFloat angle, bool resetting)
{
__strong TGPhotoCropView *strongSelf = weakSelf;
if (strongSelf == nil)
return;
[strongSelf setRotation:angle resetting:resetting];
};
}
}
return self;
}
- (void)dealloc
{
if (_confirmTimer != nil)
{
[_confirmTimer invalidate];
_confirmTimer = nil;
}
}
#pragma mark - Transition
- (void)animateTransitionIn
{
_scrollView.hidden = true;
_overlayWrapperView.alpha = 0.0f;
_areaView.alpha = 0.0f;
_rotationView.alpha = 0.0f;
}
- (void)animateTransitionOut
{
if (_confirmTimer != nil)
{
[_confirmTimer invalidate];
_confirmTimer = nil;
}
_scrollView.hidden = true;
[UIView animateWithDuration:0.1f delay:0.0f options:UIViewAnimationOptionCurveEaseOut animations:^
{
_areaView.alpha = 0.0f;
_rotationView.alpha = 0.0f;
} completion:nil];
_overlayWrapperView.alpha = 0.0f;
}
- (void)transitionInFinishedAnimated:(bool)animated completion:(void (^)(void))completion
{
_overlayWrapperView.alpha = 1.0f;
_scrollView.hidden = false;
if (animated)
_scrollView.alpha = 0.0f;
[UIView animateWithDuration:0.25f delay:0.0f options:UIViewAnimationOptionCurveEaseInOut animations:^
{
if (animated)
_scrollView.alpha = 1.0f;
_areaView.alpha = 1.0f;
_rotationView.alpha = 1.0f;
} completion:^(__unused BOOL finished)
{
if (completion != nil)
completion();
}];
[self showBackdropViewAnimated:false];
}
- (bool)isAnimating
{
return _animatingConfirm || _animatingRotation || _scrollView.animating;
}
- (bool)isAnimatingRotation
{
return _animatingRotation;
}
#pragma mark - Setup
- (void)setup
{
_contentWrapperView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, _originalSize.width, _originalSize.height)];
_scrollView.contentSize = _originalSize;
[_scrollView setContentView:_contentWrapperView];
_imageView = [[UIImageView alloc] initWithFrame:_contentWrapperView.bounds];
_imageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_imageView.contentMode = UIViewContentModeScaleAspectFit;
[_contentWrapperView addSubview:_imageView];
_scrollView.imageView = _imageView;
_backdropView = [[UIImageView alloc] initWithFrame:CGRectMake(-_contentWrapperView.frame.size.width / 2,
-_contentWrapperView.frame.size.height / 2,
_contentWrapperView.frame.size.width * 2,
_contentWrapperView.frame.size.height * 2)];
if (_backdropImage != nil)
_backdropView.image = _backdropImage;
_backdropView.alpha = 0.0f;
[_contentWrapperView addSubview:_backdropView];
[self _layoutAreaViewAnimated:false completion:nil];
[self _zoomToCropRectWithFrame:_scrollView.bounds animated:false completion:nil];
if (_snapshotView != nil)
{
CGAffineTransform transform = _contentWrapperView.transform;
_contentWrapperView.transform = CGAffineTransformIdentity;
CGRect rotatedRect = [_contentWrapperView convertRect:_scrollView.bounds fromView:_scrollView];
CGRect frame = [_scrollView zoomedRect];
_snapshotView.frame = frame;
_snapshotView.center = CGPointMake(CGRectGetMidX(rotatedRect), CGRectGetMidY(rotatedRect));
_snapshotView.transform = CGAffineTransformRotate(_snapshotView.transform, -self.rotation);
[_contentWrapperView addSubview:_snapshotView];
_contentWrapperView.transform = transform;
}
}
- (void)setImage:(UIImage *)image
{
_image = image;
[_imageView setImage:image];
[_snapshotView removeFromSuperview];
_snapshotView = nil;
}
- (void)setSnapshotImage:(UIImage *)snapshotImage
{
[_snapshotView removeFromSuperview];
_snapshotSize = snapshotImage.size;
UIImageView *imageSnapshotView = [[UIImageView alloc] initWithImage:snapshotImage];
_snapshotView = imageSnapshotView;
}
- (void)setSnapshotView:(UIView *)snapshotView
{
[_snapshotView removeFromSuperview];
_snapshotView = snapshotView;
}
#pragma mark - Crop Area
- (void)handleCropAreaChanged
{
_cropAreaChanged = true;
_areaView.frame = [self _cappedAreaViewRectWithRect:_areaView.frame];
CGPoint translationOffset = CGPointMake(_previousAreaFrame.origin.x - _areaView.frame.origin.x,
_previousAreaFrame.origin.y - _areaView.frame.origin.y);
[_scrollView translateContentViewWithOffset:translationOffset];
_previousAreaFrame = _areaView.frame;
_scrollView.frame = _areaView.frame;
[_scrollView fitContentInsideBoundsAllowScale:true animated:false completion:nil];
[self _layoutRotationView];
[self _layoutOverlayViewsWithFrame:_areaView.frame animated:false];
}
- (CGRect)_cappedAreaViewRectWithRect:(CGRect)cropRect
{
CGRect cappedRect = [self convertRect:cropRect fromView:_areaWrapperView];
CGFloat aspectRatio = _lockedAspectRatio;
if (aspectRatio > 0 && (_cropOrientation == UIImageOrientationLeft || _cropOrientation == UIImageOrientationRight))
aspectRatio = 1.0f / aspectRatio;
if (CGRectGetMaxX(cappedRect) > self.frame.size.width)
{
cappedRect.origin.x = MIN(self.frame.size.width - TGPhotoCropCornerControlSize.width, cappedRect.origin.x);
cappedRect.size.width = MAX(TGPhotoCropCornerControlSize.width, self.frame.size.width - cappedRect.origin.x);
if (aspectRatio > 0)
cappedRect.size.height = cappedRect.size.width * aspectRatio;
}
else if (CGRectGetMinX(cappedRect) < 0)
{
cappedRect.size.width = CGRectGetMaxX(cappedRect);
if (aspectRatio > 0)
cappedRect.size.height = cappedRect.size.width * aspectRatio;
cappedRect.origin.x = 0;
}
if (CGRectGetMaxY(cappedRect) > self.frame.size.height)
{
cappedRect.origin.y = MIN(self.frame.size.height - TGPhotoCropCornerControlSize.height, cappedRect.origin.y);
cappedRect.size.height = MAX(TGPhotoCropCornerControlSize.height, self.frame.size.height - cappedRect.origin.y);
if (aspectRatio > 0)
cappedRect.size.width = cappedRect.size.height / aspectRatio;
}
else if (CGRectGetMinY(cappedRect) < 0)
{
cappedRect.size.height = CGRectGetMaxY(cappedRect);
if (aspectRatio > 0)
cappedRect.size.width = cappedRect.size.height / aspectRatio;
cappedRect.origin.y = 0;
}
return [self convertRect:cappedRect toView:_areaWrapperView];
}
#pragma mark - Backdrop
- (void)setBackdropImage:(UIImage *)backdropImage
{
_backdropImage = backdropImage;
if (_backdropView != nil)
_backdropView.image = backdropImage;
}
- (void)showBackdropViewAnimated:(bool)animated
{
if (_imageView.image == nil)
return;
[_scrollView setSnapshotViewEnabled:true];
if (animated)
{
[UIView animateWithDuration:0.16f delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^
{
_backdropView.alpha = 1.0f;
} completion:nil];
}
else
{
_backdropView.alpha = 1.0f;
}
}
- (void)resetBackdropViewsAnimated:(bool)animated
{
if (animated)
{
if (_backdropView.alpha == 0)
{
[_scrollView setSnapshotViewEnabled:false];
return;
}
[UIView animateWithDuration:0.16f delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^
{
_backdropView.alpha = 0.0f;
} completion:^(BOOL finished)
{
if (finished)
[_scrollView setSnapshotViewEnabled:false];
}];
}
else
{
[_scrollView setSnapshotViewEnabled:false];
_backdropView.alpha = 0.0f;
}
}
#pragma mark - Area Confirmation
- (void)startConfirmCountdown
{
if (_areaView.isTracking)
return;
__weak TGPhotoCropView *weakSelf = self;
_confirmTimer = [[TGTimer alloc] initWithTimeout:1.0f repeat:false completion:^
{
__strong TGPhotoCropView *strongSelf = weakSelf;
if (strongSelf == nil)
return;
[strongSelf performConfirmAnimated:true];
} queue:dispatch_get_main_queue()];
[_confirmTimer start];
}
- (void)cancelConfirmCountdown
{
[_confirmTimer invalidate];
_confirmTimer = nil;
}
- (void)performConfirmAnimated:(bool)animated
{
[self performConfirmAnimated:animated updateInterface:true];
}
- (void)performConfirmAnimated:(bool)animated updateInterface:(bool)updateInterface
{
if (_areaView.isTracking || _animatingConfirm)
return;
[_confirmTimer invalidate];
_confirmTimer = nil;
_cropRect = [_scrollView zoomedRect];
CGSize minimumSizes = CGSizeMake(_originalSize.width / _scrollView.maximumZoomScale,
_originalSize.height / _scrollView.maximumZoomScale);
CGRect constrainedCropRect = _cropRect;
if (_cropRect.size.width < minimumSizes.width && _cropRect.size.height < minimumSizes.height)
{
if (_cropRect.size.width > _cropRect.size.height)
{
constrainedCropRect.size.width = minimumSizes.width;
constrainedCropRect.size.height = _cropRect.size.height * constrainedCropRect.size.width / _cropRect.size.width;
}
else
{
constrainedCropRect.size.height = minimumSizes.height;
constrainedCropRect.size.width = _cropRect.size.width * constrainedCropRect.size.height / _cropRect.size.height;
}
CGSize rotatedContentSize = TGRotatedContentSize(_scrollView.contentSize, _scrollView.contentRotation);
if (CGRectGetMaxX(constrainedCropRect) > rotatedContentSize.width)
constrainedCropRect.origin.x = rotatedContentSize.width - constrainedCropRect.size.width;
if (CGRectGetMaxY(constrainedCropRect) > rotatedContentSize.height)
constrainedCropRect.origin.y = rotatedContentSize.height - constrainedCropRect.size.height;
}
_cropRect = constrainedCropRect;
[_areaView setGridMode:TGPhotoCropViewGridModeNone animated:animated];
if (updateInterface)
{
[self setIntefaceHidden:false animated:animated];
}
else
{
if (_rotationView.hidden)
[self setCropAreaHidden:true animated:false];
}
if (!_cropAreaChanged)
{
[self showBackdropViewAnimated:animated];
if (self.croppingChanged != nil)
self.croppingChanged();
if (self.interactionEnded != nil)
self.interactionEnded();
return;
}
_cropAreaChanged = false;
[self evenlyFillAreaViewAnimated:animated reset:false completion:^
{
if (self.interactionEnded != nil)
self.interactionEnded();
}];
if (self.croppingChanged != nil)
self.croppingChanged();
}
- (void)evenlyFillAreaViewAnimated:(bool)animated reset:(bool)reset completion:(void (^)(void))completion
{
if (animated)
{
[self _layoutRotationView];
_animatingConfirm = true;
NSMutableSet *animations = [NSMutableSet set];
void (^onAnimationCompletion)(id) = ^(id object)
{
[animations removeObject:object];
if (animations.count == 0)
{
_animatingConfirm = false;
[_scrollView fitContentInsideBoundsAllowScale:false animated:true completion:^
{
[self showBackdropViewAnimated:true];
if (completion != nil)
completion();
}];
}
};
[animations addObject:@1];
CGRect frame = [self _layoutAreaViewAnimated:true completion:^
{
onAnimationCompletion(@1);
}];
if (reset)
{
[animations addObject:@2];
[_scrollView resetAnimatedWithFrame:frame completion:^
{
onAnimationCompletion(@2);
}];
}
else
{
[animations addObject:@2];
[self _zoomToCropRectWithFrame:frame animated:true completion:^
{
onAnimationCompletion(@2);
}];
}
}
else
{
[self _layoutAreaViewAnimated:false completion:nil];
if (reset)
[_scrollView reset];
else
[self _zoomToCropRectWithFrame:_scrollView.bounds animated:false completion:nil];
[self showBackdropViewAnimated:true];
if (completion != nil)
completion();
}
}
- (void)setCropAreaHidden:(bool)hidden animated:(bool)animated
{
if (animated)
{
_areaView.hidden = false;
[UIView animateWithDuration:0.2f delay:0.0f options:UIViewAnimationOptionCurveEaseInOut animations:^
{
_areaView.alpha = hidden ? 0.0f : 1.0f;
} completion:^(BOOL finished)
{
if (finished)
_areaView.hidden = hidden;
}];
}
else
{
_areaView.hidden = hidden;
_areaView.alpha = hidden ? 0.0f : 1.0f;
}
}
- (void)setIntefaceHidden:(bool)hidden animated:(bool)animated
{
if (animated)
{
_rotationView.hidden = false;
[UIView animateWithDuration:0.2f delay:0.0f options:UIViewAnimationOptionCurveEaseInOut animations:^
{
_rotationView.alpha = hidden ? 0.0f : 1.0f;
} completion:^(BOOL finished)
{
if (finished)
_rotationView.hidden = hidden;
}];
}
else
{
_rotationView.hidden = hidden;
_rotationView.alpha = hidden ? 0.0f : 1.0f;
}
}
#pragma mark - Cropping
- (UIView *)cropSnapshotView
{
UIView *snapshotView = [_scrollView snapshotViewAfterScreenUpdates:false];
snapshotView.transform = CGAffineTransformMakeRotation(TGRotationForOrientation(_cropOrientation));
return snapshotView;
}
- (CGRect)cropRectFrameForView:(UIView *)view
{
return [_areaWrapperView convertRect:_scrollView.frame toView:view];
}
- (UIImage *)croppedImageWithMaxSize:(CGSize)maxSize
{
return TGPhotoEditorCrop(_imageView.image, self.cropOrientation, self.rotation, self.cropRect, maxSize, _originalSize);
}
#pragma mark - Rotation
- (bool)hasArbitraryRotation
{
return (_rotationView != nil);
}
- (void)setRotation:(CGFloat)rotation
{
[_rotationView setAngle:rotation];
[self setRotation:rotation resetting:false];
}
- (void)setRotation:(CGFloat)rotation resetting:(bool)resetting
{
_rotation = rotation;
[_scrollView setContentRotation:rotation resetting:resetting];
}
- (void)setRotation:(CGFloat)rotation animated:(bool)animated
{
[_rotationView setAngle:rotation animated:animated];
}
- (void)rotate90DegreesCCWAnimated:(bool)animated
{
if (_animatingConfirm || _scrollView.animating)
return;
[self resetBackdropViewsAnimated:true];
[self cancelConfirmCountdown];
UIView *snapshotView = nil;
if (_rotationSnapshotView != nil)
{
snapshotView = _rotationSnapshotView;
}
else
{
if (animated)
{
snapshotView = [self cropSnapshotView];
snapshotView.transform = _areaWrapperView.transform;
snapshotView.frame = [self convertRect:_scrollView.frame fromView:_areaWrapperView];
[self addSubview:snapshotView];
_rotationSnapshotView = snapshotView;
}
_cropRect = [_scrollView zoomedRect];
}
_scrollView.hidden = true;
[self resetBackdropViewsAnimated:false];
[self setIntefaceHidden:true animated:false];
[self setCropAreaHidden:true animated:false];
self.cropOrientation = TGNextCCWOrientationForOrientation(self.cropOrientation);
CGSize areaSize = [self _areaSizeForCropRect:_cropRect orientation:_cropOrientation];
CGRect areaBounds = CGRectMake(0, 0, areaSize.width, areaSize.height);
if (self.cropOrientation == UIImageOrientationLeft || self.cropOrientation == UIImageOrientationRight)
areaBounds = CGRectMake(0, 0, areaSize.height, areaSize.width);
_areaWrapperView.backgroundColor = [TGPhotoEditorInterfaceAssets toolbarBackgroundColor];
if (animated)
{
_animatingRotation = true;
POPSpringAnimation *centerAnimation = [POPSpringAnimation animationWithPropertyNamed:kPOPViewCenter];
centerAnimation.fromValue = [NSValue valueWithCGPoint:snapshotView.center];
centerAnimation.toValue = [NSValue valueWithCGPoint:CGPointMake(self.frame.size.width / 2, self.frame.size.height / 2)];
centerAnimation.springSpeed = 7;
centerAnimation.springBounciness = 1;
[snapshotView pop_addAnimation:centerAnimation forKey:@"center"];
POPSpringAnimation *boundsAnimation = [POPSpringAnimation animationWithPropertyNamed:kPOPViewBounds];
boundsAnimation.fromValue = [NSValue valueWithCGRect:snapshotView.bounds];
boundsAnimation.toValue = [NSValue valueWithCGRect:areaBounds];
boundsAnimation.springSpeed = 7;
boundsAnimation.springBounciness = 1;
[snapshotView pop_addAnimation:boundsAnimation forKey:@"bounds"];
CGFloat currentRotation = [[snapshotView.layer valueForKeyPath:@"transform.rotation.z"] floatValue];
CGFloat targetRotation = TGRotationForOrientation(self.cropOrientation);
if (fabs(currentRotation - targetRotation) > M_PI)
targetRotation = -2 * (CGFloat)M_PI + targetRotation;
POPSpringAnimation *rotationAnimation = [POPSpringAnimation animationWithPropertyNamed:kPOPLayerRotation];
rotationAnimation.fromValue = @(currentRotation);
rotationAnimation.toValue = @(targetRotation);
rotationAnimation.springSpeed = 7;
rotationAnimation.springBounciness = 1;
[snapshotView.layer pop_addAnimation:rotationAnimation forKey:@"rotation"];
[TGPhotoEditorAnimation performBlock:^(__unused bool allFinished)
{
if (!allFinished)
return;
_animatingRotation = false;
[snapshotView removeFromSuperview];
if (_rotationSnapshotView == snapshotView)
_rotationSnapshotView = nil;
_areaWrapperView.backgroundColor = [UIColor clearColor];
[self evenlyFillAreaViewAnimated:false reset:false completion:nil];
_scrollView.hidden = false;
[self setIntefaceHidden:false animated:true];
[self setCropAreaHidden:false animated:true];
[self showBackdropViewAnimated:false];
_backdropView.alpha = 0.0f;
_imageView.alpha = 0.0f;
[UIView animateWithDuration:0.16f delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^
{
_backdropView.alpha = 1.0f;
} completion:^(__unused BOOL finished)
{
_imageView.alpha = 1.0f;
}];
} whenCompletedAllAnimations:@[ centerAnimation, boundsAnimation, rotationAnimation ]];
}
else
{
[snapshotView removeFromSuperview];
if (_rotationSnapshotView == snapshotView)
_rotationSnapshotView = nil;
_areaWrapperView.backgroundColor = [UIColor clearColor];
[self _layoutAreaViewAnimated:false completion:nil];
[self _zoomToCropRectWithFrame:_scrollView.bounds animated:false completion:nil];
_scrollView.hidden = false;
[self setIntefaceHidden:false animated:false];
[self setCropAreaHidden:false animated:false];
[self showBackdropViewAnimated:false];
}
if (self.croppingChanged != nil)
self.croppingChanged();
}
#pragma mark - Aspect Ratio Lock
- (void)setLockedAspectRatio:(CGFloat)aspectRatio performResize:(bool)performResize animated:(bool)animated
{
_lockedAspectRatio = aspectRatio;
_areaView.lockAspectRatio = true;
_areaView.aspectRatio = aspectRatio;
if (!performResize)
return;
CGPoint currentCenter = CGPointMake(CGRectGetMidX(_cropRect), CGRectGetMidY(_cropRect));
CGRect availableRect = [_scrollView availableRect];
CGRect newCropRect = _cropRect;
newCropRect.size.height = newCropRect.size.width * aspectRatio;
if (newCropRect.size.height > availableRect.size.height)
{
newCropRect.size.height = availableRect.size.height;
newCropRect.size.width = newCropRect.size.height / aspectRatio;
}
newCropRect.origin.x = currentCenter.x - newCropRect.size.width / 2;
newCropRect.origin.y = currentCenter.y - newCropRect.size.height / 2;
if (newCropRect.origin.x < availableRect.origin.x)
newCropRect.origin.x = availableRect.origin.x;
if (newCropRect.origin.y < availableRect.origin.y)
newCropRect.origin.y = availableRect.origin.y;
if (CGRectGetMaxX(newCropRect) > CGRectGetMaxX(availableRect))
newCropRect.origin.x = CGRectGetMaxX(availableRect) - newCropRect.size.width;
if (CGRectGetMaxY(newCropRect) > CGRectGetMaxY(availableRect))
newCropRect.origin.y = CGRectGetMaxY(availableRect) - newCropRect.size.height;
if (!_CGRectEqualToRectWithEpsilon(newCropRect, _cropRect, [self _cropRectEpsilon]))
[self resetBackdropViewsAnimated:false];
_cropRect = newCropRect;
[self evenlyFillAreaViewAnimated:animated reset:false completion:nil];
if (self.croppingChanged != nil)
self.croppingChanged();
}
- (void)unlockAspectRatio
{
_lockedAspectRatio = 0;
_areaView.lockAspectRatio = false;
_areaView.aspectRatio = 0;
if (self.croppingChanged != nil)
self.croppingChanged();
}
- (bool)isAspectRatioLocked
{
return (_lockedAspectRatio > FLT_EPSILON);
}
- (CGFloat)_cropRectEpsilon
{
return MAX(_originalSize.width, _originalSize.height) * 0.005f;
}
#pragma mark - Reset
- (void)resetAnimated:(bool)animated
{
if (_animatingConfirm)
return;
_animatingConfirm = true;
if (self.cropOrientation != UIImageOrientationUp && (ABS(_rotation) > FLT_EPSILON || !_CGRectEqualToRectWithEpsilon(_cropRect, CGRectMake(0, 0, _originalSize.width, _originalSize.height), FLT_EPSILON)))
animated = false;
_cropAreaChanged = false;
[self cancelConfirmCountdown];
_lockedAspectRatio = 0;
_areaView.lockAspectRatio = false;
_areaView.aspectRatio = 0;
CGRect originalCropRect = CGRectMake(0.0f, 0.0f, _originalSize.width, _originalSize.height);
if (!_CGRectEqualToRectWithEpsilon(_cropRect, originalCropRect, FLT_EPSILON) || fabs(_rotation) > 0 || _cropOrientation != UIImageOrientationUp)
{
[self resetBackdropViewsAnimated:false];
}
_cropRect = originalCropRect;
_rotation = 0.0f;
_cropOrientation = UIImageOrientationUp;
if (animated && _cropOrientation != UIImageOrientationUp)
{
[self setIntefaceHidden:true animated:false];
[self setCropAreaHidden:true animated:false];
}
[_rotationView resetAnimated:animated];
[_areaView setGridMode:TGPhotoCropViewGridModeNone animated:animated];
[self evenlyFillAreaViewAnimated:animated reset:true completion:^
{
[self setIntefaceHidden:false animated:true];
[self setCropAreaHidden:false animated:true];
_animatingConfirm = false;
}];
if (self.croppingChanged != nil)
self.croppingChanged();
}
#pragma mark - Layout
- (bool)isTracking
{
return _rotationView.isTracking || _areaView.isTracking || _scrollView.isTracking;
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *view = [super hitTest:point withEvent:event];
if (view == self || view == _areaWrapperView)
return _scrollView;
return view;
}
- (void)setInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
_interfaceOrientation = interfaceOrientation;
_rotationView.interfaceOrientation = interfaceOrientation;
}
- (void)layoutSubviews
{
if (_imageView == nil)
{
[self setup];
}
else if (_previousInterfaceOrientation != self.interfaceOrientation)
{
bool performedConfirm = false;
if (_confirmTimer != nil)
{
[UIView performWithoutAnimation:^
{
[self performConfirmAnimated:false];
}];
performedConfirm = true;
}
void (^layoutBlock)(void) = ^
{
if (!_areaView.isTracking && !_animatingConfirm)
[self _layoutAreaViewAnimated:false completion:nil];
[self _zoomToCropRectWithFrame:_scrollView.bounds animated:false completion:nil];
};
if (performedConfirm)
[UIView performWithoutAnimation:layoutBlock];
else
layoutBlock();
}
else
{
if (!_areaView.isTracking && !_animatingConfirm)
[self _layoutAreaViewAnimated:false completion:nil];
if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad)
[self _zoomToCropRectWithFrame:_scrollView.bounds animated:false completion:nil];
}
_previousInterfaceOrientation = self.interfaceOrientation;
}
- (void)_layoutRotationView
{
CGRect initialAreaFrame = _areaView.frame;
[self _layoutRotationViewWithWrapperFrame:_areaWrapperView.frame areaFrame:initialAreaFrame animated:false];
}
- (void)_layoutRotationViewWithWrapperFrame:(CGRect)wrapperFrame areaFrame:(CGRect)areaFrame animated:(bool)animated
{
CGRect rotationViewFrame = CGRectZero;
CGPoint areaOrigin = areaFrame.origin;
CGSize areaSize = areaFrame.size;
switch (self.cropOrientation)
{
case UIImageOrientationDown:
{
areaOrigin = CGPointMake(wrapperFrame.size.width - areaFrame.size.width, wrapperFrame.size.height - areaFrame.size.height);
areaSize = CGSizeMake(areaFrame.size.width - areaFrame.origin.x, areaFrame.size.height - areaFrame.origin.y);
}
break;
case UIImageOrientationLeft:
{
areaOrigin = CGPointMake(areaFrame.origin.y, wrapperFrame.size.height - areaFrame.size.width);
areaSize = CGSizeMake(areaFrame.size.height, areaFrame.size.width - areaFrame.origin.x);
}
break;
case UIImageOrientationRight:
{
areaOrigin = CGPointMake(wrapperFrame.size.width - areaFrame.size.height, areaFrame.origin.x);
areaSize = CGSizeMake(areaFrame.size.height - areaFrame.origin.y, areaFrame.size.width);
}
break;
default:
break;
}
areaFrame.origin = areaOrigin;
areaFrame.size = areaSize;
switch (self.interfaceOrientation)
{
case UIInterfaceOrientationLandscapeLeft:
{
rotationViewFrame = CGRectMake(wrapperFrame.origin.x - 100, wrapperFrame.origin.y + areaFrame.origin.y + (areaFrame.size.height - self.frame.size.height) / 2, 100, self.frame.size.height);
}
break;
case UIInterfaceOrientationLandscapeRight:
{
rotationViewFrame = CGRectMake(wrapperFrame.origin.x + areaFrame.origin.x + areaFrame.size.width, wrapperFrame.origin.y + areaFrame.origin.y + (areaFrame.size.height - self.frame.size.height) / 2, 100, self.frame.size.height);
}
break;
default:
{
rotationViewFrame = CGRectMake(wrapperFrame.origin.x + areaFrame.origin.x + (areaFrame.size.width - self.frame.size.width) / 2, wrapperFrame.origin.y + areaFrame.origin.y + areaFrame.size.height, self.frame.size.width, 100);
}
break;
}
if (animated)
{
POPSpringAnimation *animation = [POPSpringAnimation animationWithPropertyNamed:kPOPViewFrame];
animation.fromValue = [NSValue valueWithCGRect:_rotationView.frame];
animation.toValue = [NSValue valueWithCGRect:rotationViewFrame];
animation.springSpeed = 7;
animation.springBounciness = 1;
[_rotationView pop_addAnimation:animation forKey:@"frameAnimation"];
}
else
{
_rotationView.frame = rotationViewFrame;
}
[_rotationView setNeedsLayout];
}
- (void)_layoutOverlayViewsWithFrame:(CGRect)frame animated:(bool)animated
{
CGRect overlayWrapperFrame = frame;
CGRect topOverlayFrame = CGRectMake(0, -TGPhotoCropViewOverscreenSize, overlayWrapperFrame.size.width, TGPhotoCropViewOverscreenSize);
CGRect leftOverlayFrame = CGRectMake(-TGPhotoCropViewOverscreenSize, -TGPhotoCropViewOverscreenSize, TGPhotoCropViewOverscreenSize, overlayWrapperFrame.size.height + 2 * TGPhotoCropViewOverscreenSize);
CGRect rightOverlayFrame = CGRectMake(overlayWrapperFrame.size.width, -TGPhotoCropViewOverscreenSize, TGPhotoCropViewOverscreenSize, overlayWrapperFrame.size.height + 2 * TGPhotoCropViewOverscreenSize);
CGRect bottomOverlayFrame = CGRectMake(0, overlayWrapperFrame.size.height, overlayWrapperFrame.size.width, TGPhotoCropViewOverscreenSize);
if (animated)
{
POPSpringAnimation *wrapperAnimation = [POPSpringAnimation animationWithPropertyNamed:kPOPViewFrame];
wrapperAnimation.fromValue = [NSValue valueWithCGRect:_overlayWrapperView.frame];
wrapperAnimation.toValue = [NSValue valueWithCGRect:overlayWrapperFrame];
wrapperAnimation.springSpeed = 7;
wrapperAnimation.springBounciness = 1;
[_overlayWrapperView pop_addAnimation:wrapperAnimation forKey:@"frameAnimation"];
POPSpringAnimation *topAnimation = [POPSpringAnimation animationWithPropertyNamed:kPOPViewFrame];
topAnimation.fromValue = [NSValue valueWithCGRect:_topOverlayView.frame];
topAnimation.toValue = [NSValue valueWithCGRect:topOverlayFrame];
topAnimation.springSpeed = 7;
topAnimation.springBounciness = 1;
[_topOverlayView pop_addAnimation:topAnimation forKey:@"frameAnimation"];
POPSpringAnimation *leftAnimation = [POPSpringAnimation animationWithPropertyNamed:kPOPViewFrame];
leftAnimation.fromValue = [NSValue valueWithCGRect:_leftOverlayView.frame];
leftAnimation.toValue = [NSValue valueWithCGRect:leftOverlayFrame];
leftAnimation.springSpeed = 7;
leftAnimation.springBounciness = 1;
[_leftOverlayView pop_addAnimation:leftAnimation forKey:@"frameAnimation"];
POPSpringAnimation *rightAnimation = [POPSpringAnimation animationWithPropertyNamed:kPOPViewFrame];
rightAnimation.fromValue = [NSValue valueWithCGRect:_rightOverlayView.frame];
rightAnimation.toValue = [NSValue valueWithCGRect:rightOverlayFrame];
rightAnimation.springSpeed = 7;
rightAnimation.springBounciness = 1;
[_rightOverlayView pop_addAnimation:rightAnimation forKey:@"frameAnimation"];
POPSpringAnimation *bottomAnimation = [POPSpringAnimation animationWithPropertyNamed:kPOPViewFrame];
bottomAnimation.fromValue = [NSValue valueWithCGRect:_bottomOverlayView.frame];
bottomAnimation.toValue = [NSValue valueWithCGRect:bottomOverlayFrame];
bottomAnimation.springSpeed = 7;
bottomAnimation.springBounciness = 1;
[_bottomOverlayView pop_addAnimation:bottomAnimation forKey:@"frameAnimation"];
}
else
{
_overlayWrapperView.frame = overlayWrapperFrame;
_topOverlayView.frame = topOverlayFrame;
_leftOverlayView.frame = leftOverlayFrame;
_rightOverlayView.frame = rightOverlayFrame;
_bottomOverlayView.frame = bottomOverlayFrame;
}
}
- (CGSize)_areaSizeForCropRect:(CGRect)cropRect orientation:(UIImageOrientation)orientation
{
CGSize resultSize = cropRect.size;
CGSize rotatedSize = resultSize;
if (orientation == UIImageOrientationLeft || orientation == UIImageOrientationRight)
rotatedSize = CGSizeMake(rotatedSize.height, rotatedSize.width);
CGSize areaSize = TGScaleToSize(rotatedSize, self.bounds.size);
return areaSize;
}
- (CGRect)_layoutAreaViewAnimated:(bool)animated completion:(void (^)(void))completion
{
CGSize areaSize = [self _areaSizeForCropRect:_cropRect orientation:_cropOrientation];
CGRect areaWrapperFrame = CGRectMake((self.frame.size.width - areaSize.width) / 2, (self.frame.size.height - areaSize.height) / 2, areaSize.width, areaSize.height);
CGRect areaWrapperBounds = CGRectMake(0, 0, areaWrapperFrame.size.width, areaWrapperFrame.size.height);
switch (self.cropOrientation)
{
case UIImageOrientationUp:
{
_areaWrapperView.transform = CGAffineTransformIdentity;
}
break;
case UIImageOrientationDown:
{
_areaWrapperView.transform = CGAffineTransformMakeRotation((CGFloat)M_PI);
}
break;
case UIImageOrientationLeft:
{
_areaWrapperView.transform = CGAffineTransformMakeRotation((CGFloat)-M_PI_2);
areaWrapperBounds = CGRectMake(0, 0, areaWrapperBounds.size.height, areaWrapperBounds.size.width);
}
break;
case UIImageOrientationRight:
{
_areaWrapperView.transform = CGAffineTransformMakeRotation((CGFloat)M_PI_2);
areaWrapperBounds = CGRectMake(0, 0, areaWrapperBounds.size.height, areaWrapperBounds.size.width);
}
break;
default:
break;
}
if (animated)
{
POPSpringAnimation *wrapperAnimation = [POPSpringAnimation animationWithPropertyNamed:kPOPViewFrame];
wrapperAnimation.fromValue = [NSValue valueWithCGRect:_areaWrapperView.frame];
wrapperAnimation.toValue = [NSValue valueWithCGRect:areaWrapperFrame];
wrapperAnimation.springSpeed = 7;
wrapperAnimation.springBounciness = 1;
POPSpringAnimation *areaAnimation = [POPSpringAnimation animationWithPropertyNamed:kPOPViewFrame];
areaAnimation.fromValue = [NSValue valueWithCGRect:_areaView.frame];
areaAnimation.toValue = [NSValue valueWithCGRect:areaWrapperBounds];
areaAnimation.springSpeed = 7;
areaAnimation.springBounciness = 1;
POPSpringAnimation *scrollViewAnimation = [POPSpringAnimation animationWithPropertyNamed:kPOPViewFrame];
scrollViewAnimation.fromValue = [NSValue valueWithCGRect:_scrollView.frame];
scrollViewAnimation.toValue = [NSValue valueWithCGRect:areaWrapperBounds];
scrollViewAnimation.springSpeed = 7;
scrollViewAnimation.springBounciness = 1;
[TGPhotoEditorAnimation performBlock:^(__unused bool allFinished)
{
if (completion != nil)
completion();
} whenCompletedAllAnimations:@[ wrapperAnimation, areaAnimation, scrollViewAnimation ]];
[_areaWrapperView pop_addAnimation:wrapperAnimation forKey:@"frameAnimation"];
[_areaView pop_addAnimation:areaAnimation forKey:@"frameAnimation"];
[_scrollView pop_addAnimation:scrollViewAnimation forKey:@"frameAnimation"];
}
else
{
_areaWrapperView.frame = areaWrapperFrame;
_areaView.frame = areaWrapperBounds;
_scrollView.frame = areaWrapperBounds;
if (completion != nil)
completion();
}
[self _layoutOverlayViewsWithFrame:areaWrapperBounds animated:animated];
[self _layoutRotationViewWithWrapperFrame:areaWrapperFrame areaFrame:CGRectMake(0, 0, areaWrapperBounds.size.width, areaWrapperBounds.size.height) animated:animated];
return areaWrapperBounds;
}
- (void)_zoomToCropRectWithFrame:(CGRect)frame animated:(bool)animated completion:(void (^)(void))completion
{
[_scrollView zoomToRect:_cropRect withFrame:frame animated:animated completion:completion];
}
@end