981 lines
38 KiB
Objective-C
981 lines
38 KiB
Objective-C
//
|
|
// zhPopupController.m
|
|
// <https://github.com/snail-z/zhPopupController.git>
|
|
//
|
|
// Created by zhanghao on 2016/11/15.
|
|
// Copyright © 2017年 snail-z. All rights reserved.
|
|
//
|
|
|
|
#import "zhPopupController.h"
|
|
#import <objc/runtime.h>
|
|
|
|
static NSMutableSet *gShowingZhPopupControllers;
|
|
|
|
@interface zhPopupController () <UIGestureRecognizerDelegate>
|
|
|
|
@property (nonatomic, strong, readonly) UIView *superview;
|
|
@property (nonatomic, strong, readonly) UIView *maskView;
|
|
@property (nonatomic, strong, readonly) UIView *contentView;
|
|
@property (nonatomic, assign, readonly) CGFloat dropAngle;
|
|
@property (nonatomic, assign, readonly) CGPoint markerCenter;
|
|
@property (nonatomic, assign, readonly) zhPopupMaskType maskType;
|
|
|
|
@end
|
|
|
|
static void *zhPopupControllerParametersKey = &zhPopupControllerParametersKey;
|
|
static void *zhPopupControllerNSTimerKey = &zhPopupControllerNSTimerKey;
|
|
|
|
@implementation zhPopupController
|
|
|
|
+ (instancetype)popupControllerWithMaskType:(zhPopupMaskType)maskType {
|
|
return [[self alloc] initWithMaskType:maskType];
|
|
}
|
|
|
|
- (instancetype)init {
|
|
return [self initWithMaskType:zhPopupMaskTypeBlackTranslucent];
|
|
}
|
|
|
|
- (instancetype)initWithMaskType:(zhPopupMaskType)maskType {
|
|
if (self = [super init]) {
|
|
_isPresenting = NO;
|
|
_maskType = maskType;
|
|
_layoutType = zhPopupLayoutTypeCenter;
|
|
_dismissOnMaskTouched = YES;
|
|
_autoAdjustOffsetWhenKeyboardFrameChanged = YES;
|
|
|
|
// setter
|
|
self.maskAlpha = 0.5f;
|
|
self.slideStyle = zhPopupSlideStyleFade;
|
|
self.dismissOppositeDirection = NO;
|
|
self.allowPan = NO;
|
|
|
|
// superview
|
|
_superview = [self frontWindow];
|
|
|
|
// maskView
|
|
if (maskType == zhPopupMaskTypeBlackBlur || maskType == zhPopupMaskTypeWhiteBlur) {
|
|
if ([[UIDevice currentDevice].systemVersion compare:@"8.0" options:NSNumericSearch] == NSOrderedAscending) {
|
|
_maskView = [[UIToolbar alloc] initWithFrame:_superview.bounds];
|
|
} else {
|
|
_maskView = [[UIView alloc] initWithFrame:_superview.bounds];
|
|
UIVisualEffectView *visualEffectView;
|
|
visualEffectView = [[UIVisualEffectView alloc] init];
|
|
visualEffectView.effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
|
|
visualEffectView.frame = _superview.bounds;
|
|
[_maskView insertSubview:visualEffectView atIndex:0];
|
|
}
|
|
} else {
|
|
_maskView = [[UIView alloc] initWithFrame:_superview.bounds];
|
|
}
|
|
|
|
switch (maskType) {
|
|
case zhPopupMaskTypeBlackBlur: {
|
|
if ([_maskView isKindOfClass:[UIToolbar class]]) {
|
|
[(UIToolbar *)_maskView setBarStyle:UIBarStyleBlack];
|
|
} else {
|
|
UIVisualEffectView *effectView = (UIVisualEffectView *)_maskView.subviews.firstObject;
|
|
effectView.effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleDark];
|
|
}
|
|
} break;
|
|
case zhPopupMaskTypeWhiteBlur: {
|
|
if ([_maskView isKindOfClass:[UIToolbar class]]) {
|
|
[(UIToolbar *)_maskView setBarStyle:UIBarStyleDefault];
|
|
} else {
|
|
UIVisualEffectView *effectView = (UIVisualEffectView *)_maskView.subviews.firstObject;
|
|
effectView.effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
|
|
}
|
|
} break;
|
|
case zhPopupMaskTypeWhite:
|
|
_maskView.backgroundColor = [UIColor whiteColor];
|
|
break;
|
|
case zhPopupMaskTypeClear:
|
|
_maskView.backgroundColor = [UIColor clearColor];
|
|
break;
|
|
default: // zhPopupMaskTypeBlackTranslucent
|
|
_maskView.backgroundColor = [UIColor colorWithWhite:0.0 alpha:_maskAlpha];
|
|
break;
|
|
}
|
|
|
|
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self
|
|
action:@selector(handleTap:)];
|
|
tap.delegate = self;
|
|
[_maskView addGestureRecognizer:tap];
|
|
|
|
// popupView
|
|
_popupView = [[UIView alloc] init];
|
|
_popupView.backgroundColor = [UIColor clearColor];
|
|
|
|
// addSubview
|
|
[_maskView addSubview:_popupView];
|
|
[_superview addSubview:_maskView];
|
|
|
|
// Observer statusBar orientation changes.
|
|
[self bindNotificationEvent];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
#pragma mark - Setter
|
|
|
|
- (void)setDismissOppositeDirection:(BOOL)dismissOppositeDirection {
|
|
_dismissOppositeDirection = dismissOppositeDirection;
|
|
objc_setAssociatedObject(self, _cmd, @(dismissOppositeDirection), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
}
|
|
|
|
- (void)setSlideStyle:(zhPopupSlideStyle)slideStyle {
|
|
_slideStyle = slideStyle;
|
|
objc_setAssociatedObject(self, _cmd, @(slideStyle), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
}
|
|
|
|
- (void)setMaskAlpha:(CGFloat)maskAlpha {
|
|
if (_maskType != zhPopupMaskTypeBlackTranslucent) return;
|
|
_maskAlpha = maskAlpha;
|
|
_maskView.backgroundColor = [UIColor colorWithWhite:0.0 alpha:_maskAlpha];
|
|
}
|
|
|
|
- (void)setAllowPan:(BOOL)allowPan {
|
|
if (!allowPan) return;
|
|
if (_allowPan != allowPan) {
|
|
_allowPan = allowPan;
|
|
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
|
|
[_popupView addGestureRecognizer:pan];
|
|
}
|
|
}
|
|
|
|
- (void)setAlpha:(CGFloat)alpha
|
|
{
|
|
_alpha = alpha;
|
|
|
|
_maskView.alpha = alpha;
|
|
}
|
|
|
|
#pragma mark - Present
|
|
|
|
- (void)presentContentView:(UIView *)contentView {
|
|
[self presentContentView:contentView duration:0.25 springAnimated:NO];
|
|
}
|
|
|
|
- (void)presentContentView:(UIView *)contentView displayTime:(NSTimeInterval)displayTime {
|
|
[self presentContentView:contentView duration:0.25 springAnimated:NO inView:nil displayTime:displayTime];
|
|
}
|
|
|
|
- (void)presentContentView:(UIView *)contentView duration:(NSTimeInterval)duration springAnimated:(BOOL)isSpringAnimated {
|
|
[self presentContentView:contentView duration:duration springAnimated:isSpringAnimated inView:nil];
|
|
}
|
|
|
|
- (void)presentContentView:(UIView *)contentView
|
|
duration:(NSTimeInterval)duration
|
|
springAnimated:(BOOL)isSpringAnimated
|
|
inView:(UIView *)sView {
|
|
[self presentContentView:contentView duration:duration springAnimated:isSpringAnimated inView:sView displayTime:0];
|
|
}
|
|
|
|
- (void)presentContentView:(UIView *)contentView
|
|
duration:(NSTimeInterval)duration
|
|
springAnimated:(BOOL)isSpringAnimated
|
|
inView:(UIView *)sView
|
|
displayTime:(NSTimeInterval)displayTime {
|
|
|
|
if (self.isPresenting) return;
|
|
|
|
if (!gShowingZhPopupControllers) {
|
|
gShowingZhPopupControllers = [NSMutableSet set];
|
|
}
|
|
[gShowingZhPopupControllers addObject:self];
|
|
NSMutableDictionary *parameters = [NSMutableDictionary dictionaryWithCapacity:2];
|
|
[parameters setValue:@(duration) forKey:@"zh_duration"];
|
|
[parameters setValue:@(isSpringAnimated) forKey:@"zh_springAnimated"];
|
|
objc_setAssociatedObject(self, zhPopupControllerParametersKey, parameters, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
|
|
if (nil != self.willPresent) {
|
|
self.willPresent(self);
|
|
} else {
|
|
if ([self.delegate respondsToSelector:@selector(popupControllerWillPresent:)]) {
|
|
[self.delegate popupControllerWillPresent:self];
|
|
}
|
|
}
|
|
|
|
if (nil != sView) {
|
|
_superview = sView;
|
|
_maskView.frame = _superview.frame;
|
|
}
|
|
[self addContentView:contentView];
|
|
if (![_superview.subviews containsObject:_maskView]) {
|
|
[_superview addSubview:_maskView];
|
|
}
|
|
|
|
[self prepareDropAnimated];
|
|
[self prepareBackground];
|
|
_popupView.userInteractionEnabled = NO;
|
|
_popupView.center = [self prepareCenter];
|
|
|
|
void (^presentCallback)(void) = ^() {
|
|
self->_isPresenting = YES;
|
|
self.popupView.userInteractionEnabled = YES;
|
|
if (nil != self.didPresent) {
|
|
self.didPresent(self);
|
|
} else {
|
|
if ([self.delegate respondsToSelector:@selector(popupControllerDidPresent:)]) {
|
|
[self.delegate popupControllerDidPresent:self];
|
|
}
|
|
}
|
|
|
|
if (displayTime) {
|
|
NSTimer *timer = [NSTimer timerWithTimeInterval:displayTime target:self selector:@selector(dismiss) userInfo:nil repeats:NO];
|
|
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
|
|
objc_setAssociatedObject(self, zhPopupControllerNSTimerKey, timer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
}
|
|
};
|
|
|
|
if (isSpringAnimated) {
|
|
[UIView animateWithDuration:duration delay:0.f usingSpringWithDamping:0.6 initialSpringVelocity:0.2 options:UIViewAnimationOptionCurveLinear animations:^{
|
|
|
|
[self finishedDropAnimated];
|
|
[self finishedBackground];
|
|
self.popupView.center = [self finishedCenter];
|
|
|
|
} completion:^(BOOL finished) {
|
|
|
|
if (finished) presentCallback();
|
|
|
|
}];
|
|
} else {
|
|
[UIView animateWithDuration:duration delay:0.f options:UIViewAnimationOptionCurveLinear animations:^{
|
|
|
|
[self finishedDropAnimated];
|
|
[self finishedBackground];
|
|
self.popupView.center = [self finishedCenter];
|
|
|
|
} completion:^(BOOL finished) {
|
|
|
|
if (finished) presentCallback();
|
|
|
|
}];
|
|
}
|
|
}
|
|
|
|
#pragma mark - Dismiss
|
|
|
|
- (void)fadeDismiss {
|
|
objc_setAssociatedObject(self, _cmd, @(_slideStyle), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
_slideStyle = zhPopupSlideStyleFade;
|
|
[self dismiss];
|
|
}
|
|
|
|
- (void)dismiss {
|
|
id object = objc_getAssociatedObject(self, zhPopupControllerParametersKey);
|
|
if (object && [object isKindOfClass:[NSDictionary class]]) {
|
|
NSTimeInterval duration = 0.0;
|
|
NSNumber *durationNumber = [object valueForKey:@"zh_duration"];
|
|
if (nil != durationNumber) duration = durationNumber.doubleValue;
|
|
BOOL flag = NO;
|
|
NSNumber *flagNumber = [object valueForKey:@"zh_springAnimated"];
|
|
if (nil != flagNumber) flag = flagNumber.boolValue;
|
|
[self dismissWithDuration:duration springAnimated:flag];
|
|
}
|
|
}
|
|
|
|
- (void)dismissWithDuration:(NSTimeInterval)duration springAnimated:(BOOL)isSpringAnimated {
|
|
[self destroyTimer];
|
|
[gShowingZhPopupControllers removeObject:self];
|
|
// if (!self.isPresenting) return;
|
|
|
|
if (nil != self.willDismiss) {
|
|
self.willDismiss(self);
|
|
} else {
|
|
if ([self.delegate respondsToSelector:@selector(popupControllerWillDismiss:)]) {
|
|
[self.delegate popupControllerWillDismiss:self];
|
|
}
|
|
}
|
|
|
|
void (^dismissCallback)(void) = ^() {
|
|
self->_slideStyle = [objc_getAssociatedObject(self, @selector(fadeDismiss)) integerValue];
|
|
[self removeSubviews];
|
|
self->_isPresenting = NO;
|
|
self.popupView.transform = CGAffineTransformIdentity;
|
|
if (nil != self.didDismiss) {
|
|
self.didDismiss(self);
|
|
} else {
|
|
if ([self.delegate respondsToSelector:@selector(popupControllerDidDismiss:)]) {
|
|
[self.delegate popupControllerDidDismiss:self];
|
|
}
|
|
}
|
|
};
|
|
|
|
UIViewAnimationOptions (^animOpts)(zhPopupSlideStyle) = ^(zhPopupSlideStyle slide){
|
|
if (slide != zhPopupSlideStyleShrinkInOut1) {
|
|
return UIViewAnimationOptionCurveLinear;
|
|
}
|
|
return UIViewAnimationOptionCurveEaseInOut;
|
|
};
|
|
|
|
if (isSpringAnimated) {
|
|
duration *= 0.75;
|
|
NSTimeInterval duration1 = duration * 0.25, duration2 = duration - duration1;
|
|
|
|
[UIView animateWithDuration:duration1 delay:0.f options:UIViewAnimationOptionCurveEaseInOut animations:^{
|
|
|
|
[self bufferBackground];
|
|
self.popupView.center = [self bufferCenter:30];
|
|
|
|
} completion:^(BOOL finished) {
|
|
[UIView animateWithDuration:duration2 delay:0.f options:animOpts(self.slideStyle) animations:^{
|
|
|
|
[self dismissedDropAnimated];
|
|
[self dismissedBackground];
|
|
self.popupView.center = [self dismissedCenter];
|
|
|
|
} completion:^(BOOL finished) {
|
|
if (finished) dismissCallback();
|
|
}];
|
|
|
|
}];
|
|
|
|
} else {
|
|
[UIView animateWithDuration:duration delay:0.f options:animOpts(self.slideStyle) animations:^{
|
|
|
|
[self dismissedDropAnimated];
|
|
[self dismissedBackground];
|
|
self.popupView.center = [self dismissedCenter];
|
|
|
|
} completion:^(BOOL finished) {
|
|
if (finished) dismissCallback();
|
|
}];
|
|
}
|
|
}
|
|
|
|
#pragma mark - Add contentView
|
|
|
|
- (void)addContentView:(UIView *)contentView {
|
|
if (!contentView) {
|
|
if (nil != _popupView.superview) [_popupView removeFromSuperview];
|
|
return;
|
|
}
|
|
_contentView = contentView;
|
|
if (_contentView.superview != _popupView) {
|
|
_contentView.frame = (CGRect){.origin = CGPointZero, .size = contentView.frame.size};
|
|
_popupView.frame = _contentView.frame;
|
|
_popupView.backgroundColor = _contentView.backgroundColor;
|
|
if (_contentView.layer.cornerRadius) {
|
|
_popupView.layer.cornerRadius = _contentView.layer.cornerRadius;
|
|
_popupView.clipsToBounds = NO;
|
|
}
|
|
[_popupView addSubview:_contentView];
|
|
}
|
|
}
|
|
|
|
- (void)removeSubviews {
|
|
if (_popupView.subviews.count > 0) {
|
|
[_contentView removeFromSuperview];
|
|
_contentView = nil;
|
|
}
|
|
[_maskView removeFromSuperview];
|
|
}
|
|
|
|
#pragma mark - Drop animated
|
|
|
|
- (void)dropAnimatedWithRotateAngle:(CGFloat)angle {
|
|
_dropAngle = angle;
|
|
_slideStyle = zhPopupSlideStyleFromTop;
|
|
}
|
|
|
|
- (BOOL)dropSupport {
|
|
return (_layoutType == zhPopupLayoutTypeCenter && _slideStyle == zhPopupSlideStyleFromTop);
|
|
}
|
|
|
|
static CGFloat zh_randomValue(int i, int j) {
|
|
if (arc4random() % 2) return i;
|
|
return j;
|
|
}
|
|
|
|
- (void)prepareDropAnimated {
|
|
if (_dropAngle && [self dropSupport]) {
|
|
_dismissOppositeDirection = YES;
|
|
CGFloat ty = (_maskView.bounds.size.height + _popupView.frame.size.height) / 2;
|
|
CATransform3D transform = CATransform3DMakeTranslation(0, -ty, 0);
|
|
transform = CATransform3DRotate(transform,
|
|
zh_randomValue(_dropAngle, -_dropAngle) * M_PI / 180,
|
|
0, 0, 1.0);
|
|
_popupView.layer.transform = transform;
|
|
}
|
|
}
|
|
|
|
- (void)finishedDropAnimated {
|
|
if (_dropAngle && [self dropSupport]) {
|
|
_popupView.layer.transform = CATransform3DIdentity;
|
|
}
|
|
}
|
|
|
|
- (void)dismissedDropAnimated {
|
|
if (_dropAngle && [self dropSupport]) {
|
|
CGFloat ty = _maskView.bounds.size.height;
|
|
CATransform3D transform = CATransform3DMakeTranslation(0, ty, 0);
|
|
transform = CATransform3DRotate(transform,
|
|
zh_randomValue(_dropAngle, -_dropAngle) * M_PI / 180,
|
|
0, 0, 1.0);
|
|
_popupView.layer.transform = transform;
|
|
}
|
|
}
|
|
|
|
#pragma mark - Mask view background
|
|
|
|
- (void)prepareBackground {
|
|
switch (_maskType) {
|
|
case zhPopupMaskTypeBlackBlur:
|
|
case zhPopupMaskTypeWhiteBlur:
|
|
_maskView.alpha = 1;
|
|
break;
|
|
default:
|
|
_maskView.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0];
|
|
break;
|
|
}
|
|
}
|
|
|
|
- (void)finishedBackground {
|
|
switch (_maskType) {
|
|
case zhPopupMaskTypeBlackTranslucent:
|
|
_maskView.backgroundColor = [UIColor colorWithWhite:0.0 alpha:_maskAlpha];
|
|
break;
|
|
case zhPopupMaskTypeWhite:
|
|
_maskView.backgroundColor = [UIColor whiteColor];
|
|
break;
|
|
case zhPopupMaskTypeClear:
|
|
_maskView.backgroundColor = [UIColor clearColor];
|
|
break;
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
- (void)bufferBackground {
|
|
switch (_maskType) {
|
|
case zhPopupMaskTypeBlackBlur:
|
|
case zhPopupMaskTypeWhiteBlur: break;
|
|
case zhPopupMaskTypeBlackTranslucent:
|
|
_maskView.backgroundColor = [UIColor colorWithWhite:0.0 alpha:_maskAlpha - _maskAlpha * 0.15];
|
|
break;
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
- (void)dismissedBackground {
|
|
switch (_maskType) {
|
|
case zhPopupMaskTypeBlackBlur:
|
|
case zhPopupMaskTypeWhiteBlur:
|
|
_maskView.alpha = 0;
|
|
break;
|
|
default:
|
|
_maskView.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0];
|
|
break;
|
|
}
|
|
}
|
|
|
|
#pragma mark - Center point
|
|
|
|
- (CGPoint)prepareCenterFrom:(NSInteger)type viewRef:(UIView *)viewRef{
|
|
switch (type) {
|
|
case 0: // top
|
|
return CGPointMake(viewRef.center.x,
|
|
-_popupView.bounds.size.height / 2) ;
|
|
case 1: // bottom
|
|
return CGPointMake(viewRef.center.x,
|
|
_maskView.bounds.size.height + _popupView.bounds.size.height / 2);
|
|
case 2: // left
|
|
return CGPointMake(-_popupView.bounds.size.width / 2,
|
|
viewRef.center.y);
|
|
case 3: // right
|
|
return CGPointMake(_maskView.bounds.size.width + _popupView.bounds.size.width / 2,
|
|
viewRef.center.y);
|
|
default: // center
|
|
return _maskView.center;
|
|
}
|
|
}
|
|
|
|
- (CGPoint)prepareCenter {
|
|
if (_layoutType == zhPopupLayoutTypeCenter) {
|
|
CGPoint point = _maskView.center;
|
|
if (_slideStyle == zhPopupSlideStyleShrinkInOut1) {
|
|
_popupView.transform = CGAffineTransformMakeScale(0.15, 0.15);
|
|
} else if (_slideStyle == zhPopupSlideStyleShrinkInOut2) {
|
|
_popupView.transform = CGAffineTransformMakeScale(0.8, 0.8);
|
|
} else if (_slideStyle == zhPopupSlideStyleFade) {
|
|
_maskView.alpha = 0;
|
|
} else {
|
|
point = [self prepareCenterFrom:_slideStyle viewRef:_maskView];
|
|
}
|
|
return point;
|
|
}
|
|
return [self prepareCenterFrom:_layoutType viewRef:_maskView];
|
|
}
|
|
|
|
- (CGPoint)finishedCenter {
|
|
CGPoint point = _maskView.center;
|
|
switch (_layoutType) {
|
|
case zhPopupLayoutTypeTop:
|
|
return CGPointMake(point.x,
|
|
_popupView.bounds.size.height / 2);
|
|
case zhPopupLayoutTypeBottom:
|
|
return CGPointMake(point.x,
|
|
_maskView.bounds.size.height - _popupView.bounds.size.height / 2);
|
|
case zhPopupLayoutTypeLeft:
|
|
return CGPointMake(_popupView.bounds.size.width / 2,
|
|
point.y);
|
|
case zhPopupLayoutTypeRight:
|
|
return CGPointMake(_maskView.bounds.size.width - _popupView.bounds.size.width / 2,
|
|
point.y);
|
|
default: // zhPopupLayoutTypeCenter
|
|
{
|
|
if (_slideStyle == zhPopupSlideStyleShrinkInOut1 ||
|
|
_slideStyle == zhPopupSlideStyleShrinkInOut2) {
|
|
_popupView.transform = CGAffineTransformIdentity;
|
|
} else if (_slideStyle == zhPopupSlideStyleFade) {
|
|
_maskView.alpha = 1;
|
|
}
|
|
}
|
|
return point;
|
|
}
|
|
}
|
|
|
|
- (CGPoint)dismissedCenter {
|
|
if (_layoutType != zhPopupLayoutTypeCenter) {
|
|
return [self prepareCenterFrom:_layoutType viewRef:_popupView];
|
|
}
|
|
switch (_slideStyle) {
|
|
case zhPopupSlideStyleFromTop:
|
|
return _dismissOppositeDirection ?
|
|
CGPointMake(_popupView.center.x,
|
|
_maskView.bounds.size.height + _popupView.bounds.size.height / 2) :
|
|
CGPointMake(_popupView.center.x,
|
|
-_popupView.bounds.size.height / 2);
|
|
|
|
case zhPopupSlideStyleFromBottom:
|
|
return _dismissOppositeDirection ?
|
|
CGPointMake(_popupView.center.x,
|
|
-_popupView.bounds.size.height / 2) :
|
|
CGPointMake(_popupView.center.x,
|
|
_maskView.bounds.size.height + _popupView.bounds.size.height / 2);
|
|
|
|
case zhPopupSlideStyleFromLeft:
|
|
return _dismissOppositeDirection ?
|
|
CGPointMake(_maskView.bounds.size.width + _popupView.bounds.size.width / 2,
|
|
_popupView.center.y) :
|
|
CGPointMake(-_popupView.bounds.size.width / 2,
|
|
_popupView.center.y);
|
|
|
|
case zhPopupSlideStyleFromRight:
|
|
return _dismissOppositeDirection ?
|
|
CGPointMake(-_popupView.bounds.size.width / 2,
|
|
_popupView.center.y) :
|
|
CGPointMake(_maskView.bounds.size.width + _popupView.bounds.size.width / 2,
|
|
_popupView.center.y);
|
|
|
|
case zhPopupSlideStyleShrinkInOut1:
|
|
_popupView.transform = _dismissOppositeDirection ?
|
|
CGAffineTransformMakeScale(1.75, 1.75) :
|
|
CGAffineTransformMakeScale(0.25, 0.25);
|
|
break;
|
|
|
|
case zhPopupSlideStyleShrinkInOut2:
|
|
_popupView.transform = _dismissOppositeDirection ?
|
|
CGAffineTransformMakeScale(1.2, 1.2) :
|
|
CGAffineTransformMakeScale(0.75, 0.75);
|
|
|
|
case zhPopupSlideStyleFade:
|
|
_maskView.alpha = 0;
|
|
default: break;
|
|
}
|
|
return _popupView.center;
|
|
}
|
|
|
|
#pragma mark - Buffer point
|
|
|
|
- (CGPoint)bufferCenter:(CGFloat)move {
|
|
CGPoint point = _popupView.center;
|
|
switch (_layoutType) {
|
|
case zhPopupLayoutTypeTop:
|
|
point.y += move;
|
|
break;
|
|
case zhPopupLayoutTypeBottom:
|
|
point.y -= move;
|
|
break;
|
|
case zhPopupLayoutTypeLeft:
|
|
point.x += move;
|
|
break;
|
|
case zhPopupLayoutTypeRight:
|
|
point.x -= move;
|
|
break;
|
|
case zhPopupLayoutTypeCenter: {
|
|
switch (_slideStyle) {
|
|
case zhPopupSlideStyleFromTop:
|
|
point.y += _dismissOppositeDirection ? -move : move;
|
|
break;
|
|
case zhPopupSlideStyleFromBottom:
|
|
point.y += _dismissOppositeDirection ? move : -move;
|
|
break;
|
|
case zhPopupSlideStyleFromLeft:
|
|
point.x += _dismissOppositeDirection ? -move : move;
|
|
break;
|
|
case zhPopupSlideStyleFromRight:
|
|
point.x += _dismissOppositeDirection ? move : -move;
|
|
break;
|
|
case zhPopupSlideStyleShrinkInOut1:
|
|
case zhPopupSlideStyleShrinkInOut2:
|
|
_popupView.transform = _dismissOppositeDirection ?
|
|
CGAffineTransformMakeScale(0.95, 0.95) :
|
|
CGAffineTransformMakeScale(1.05, 1.05);
|
|
break;
|
|
default: break;
|
|
}
|
|
} break;
|
|
default: break;
|
|
}
|
|
return point;
|
|
}
|
|
|
|
#pragma mark - Destroy timer
|
|
|
|
- (void)destroyTimer {
|
|
id value = objc_getAssociatedObject(self, zhPopupControllerNSTimerKey);
|
|
if (value) {
|
|
[(NSTimer *)value invalidate];
|
|
objc_setAssociatedObject(self, zhPopupControllerNSTimerKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
}
|
|
}
|
|
|
|
#pragma mark - FrontWindow
|
|
|
|
- (UIWindow *)frontWindow {
|
|
NSEnumerator *enumerator = [UIApplication.sharedApplication.windows reverseObjectEnumerator];
|
|
for (UIWindow *window in enumerator) {
|
|
BOOL windowOnMainScreen = (window.screen == [UIScreen mainScreen]);
|
|
BOOL windowIsVisible = !window.isHidden && window.alpha > 0;
|
|
if (windowOnMainScreen && windowIsVisible && window.isKeyWindow) {
|
|
return window;
|
|
}
|
|
}
|
|
UIWindow *applicationWindow = [[UIApplication sharedApplication].delegate window];
|
|
if (!applicationWindow) NSLog(@"** zhPopupController ** Window is nil!");
|
|
return applicationWindow;
|
|
}
|
|
|
|
|
|
#pragma mark - Notifications
|
|
|
|
- (void)bindNotificationEvent {
|
|
[self unbindNotificationEvent];
|
|
|
|
[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(willChangeStatusBarOrientation)
|
|
name:UIApplicationWillChangeStatusBarOrientationNotification
|
|
object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(didChangeStatusBarOrientation)
|
|
name:UIApplicationDidChangeStatusBarOrientationNotification
|
|
object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(keyboardWillChangeFrame:)
|
|
name:UIKeyboardWillChangeFrameNotification
|
|
object:nil];
|
|
}
|
|
|
|
- (void)unbindNotificationEvent {
|
|
[[UIDevice currentDevice] endGeneratingDeviceOrientationNotifications];
|
|
|
|
[[NSNotificationCenter defaultCenter]removeObserver:self
|
|
name:UIApplicationWillChangeStatusBarOrientationNotification
|
|
object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter]removeObserver:self
|
|
name:UIApplicationDidChangeStatusBarOrientationNotification
|
|
object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self
|
|
name:UIKeyboardWillChangeFrameNotification
|
|
object:nil];
|
|
}
|
|
|
|
#pragma mark - Observing
|
|
|
|
- (void)keyboardWillChangeFrame:(NSNotification*)notification {
|
|
|
|
if (!_autoAdjustOffsetWhenKeyboardFrameChanged) return;
|
|
|
|
_allowPan = NO; // The pan gesture will be invalid when the keyboard appears.
|
|
|
|
NSDictionary *userInfo = notification.userInfo;
|
|
CGRect keyboardRect = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
|
|
keyboardRect = [_maskView convertRect:keyboardRect fromView:nil];
|
|
CGFloat keyboardHeight = CGRectGetHeight(_maskView.bounds) - CGRectGetMinY(keyboardRect);
|
|
|
|
UIViewAnimationCurve curve = [userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue];
|
|
UIViewAnimationOptions options = curve << 16;
|
|
|
|
NSTimeInterval duration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
|
|
|
|
[UIView animateWithDuration:duration delay:0 options:options animations:^{
|
|
if (keyboardHeight > 0) {
|
|
|
|
CGFloat offsetSpacing = self.offsetSpacingOfKeyboard, changeHeight = 0;
|
|
|
|
switch (self.layoutType) {
|
|
case zhPopupLayoutTypeTop:
|
|
break;
|
|
case zhPopupLayoutTypeBottom:
|
|
changeHeight = keyboardHeight + offsetSpacing;
|
|
break;
|
|
default:
|
|
changeHeight = (keyboardHeight / 2) + offsetSpacing;
|
|
break;
|
|
}
|
|
|
|
if (!CGPointEqualToPoint(CGPointZero, self.markerCenter)) {
|
|
self.popupView.center = CGPointMake(self.markerCenter.x, self.markerCenter.y - changeHeight);
|
|
} else {
|
|
self.popupView.center = CGPointMake(self.popupView.center.x, self.popupView.center.y - changeHeight);
|
|
}
|
|
|
|
} else {
|
|
if (self.isPresenting) {
|
|
self.popupView.center = [self finishedCenter];
|
|
}
|
|
}
|
|
} completion:^(BOOL finished) {
|
|
self->_markerCenter = [self finishedCenter];
|
|
}];
|
|
}
|
|
|
|
- (void)willChangeStatusBarOrientation {
|
|
_maskView.frame = _superview.bounds;
|
|
_popupView.center = [self finishedCenter];
|
|
[self dismiss];
|
|
}
|
|
|
|
- (void)didChangeStatusBarOrientation {
|
|
if ([[UIDevice currentDevice].systemVersion compare:@"8.0" options:NSNumericSearch] == NSOrderedAscending) { // must manually fix orientation prior to iOS 8
|
|
CGFloat angle;
|
|
switch ([UIApplication sharedApplication].statusBarOrientation)
|
|
{
|
|
case UIInterfaceOrientationPortraitUpsideDown:
|
|
angle = M_PI;
|
|
break;
|
|
case UIInterfaceOrientationLandscapeLeft:
|
|
angle = -M_PI_2;
|
|
break;
|
|
case UIInterfaceOrientationLandscapeRight:
|
|
angle = M_PI_2;
|
|
break;
|
|
default: // as UIInterfaceOrientationPortrait
|
|
angle = 0.0;
|
|
break;
|
|
}
|
|
_popupView.transform = CGAffineTransformMakeRotation(angle);
|
|
}
|
|
_maskView.frame = _superview.bounds;
|
|
_popupView.center = [self finishedCenter];
|
|
}
|
|
|
|
#pragma mark - Gesture Recognizer
|
|
|
|
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
|
|
if ([touch.view isDescendantOfView:_popupView]) return NO;
|
|
return YES;
|
|
}
|
|
|
|
- (void)handleTap:(UITapGestureRecognizer *)g {
|
|
if (_dismissOnMaskTouched) {
|
|
if (!_dropAngle) {
|
|
id object = objc_getAssociatedObject(self, @selector(setSlideStyle:));
|
|
_slideStyle = [object integerValue];
|
|
id obj = objc_getAssociatedObject(self, @selector(setDismissOppositeDirection:));
|
|
_dismissOppositeDirection = [obj boolValue];
|
|
}
|
|
if (nil != self.maskTouched) {
|
|
self.maskTouched(self);
|
|
} else {
|
|
[self dismiss];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)handlePan:(UIPanGestureRecognizer *)g {
|
|
if (!_allowPan || !_isPresenting || _dropAngle) {
|
|
return;
|
|
}
|
|
CGPoint translation = [g translationInView:_maskView];
|
|
switch (g.state) {
|
|
case UIGestureRecognizerStateBegan:
|
|
break;
|
|
case UIGestureRecognizerStateChanged: {
|
|
switch (_layoutType) {
|
|
case zhPopupLayoutTypeCenter: {
|
|
|
|
BOOL isTransformationVertical = NO;
|
|
switch (_slideStyle) {
|
|
case zhPopupSlideStyleFromLeft:
|
|
case zhPopupSlideStyleFromRight: break;
|
|
default:
|
|
isTransformationVertical = YES;
|
|
break;
|
|
}
|
|
|
|
NSInteger factor = 4; // set screen ratio `_maskView.bounds.size.height / factor`
|
|
CGFloat changeValue;
|
|
if (isTransformationVertical) {
|
|
g.view.center = CGPointMake(g.view.center.x, g.view.center.y + translation.y);
|
|
changeValue = g.view.center.y / (_maskView.bounds.size.height / factor);
|
|
} else {
|
|
g.view.center = CGPointMake(g.view.center.x + translation.x, g.view.center.y);
|
|
changeValue = g.view.center.x / (_maskView.bounds.size.width / factor);
|
|
}
|
|
CGFloat alpha = factor / 2 - fabs(changeValue - factor / 2);
|
|
[UIView animateWithDuration:0.15 animations:^{
|
|
self.maskView.alpha = alpha;
|
|
} completion:NULL];
|
|
|
|
} break;
|
|
case zhPopupLayoutTypeBottom: {
|
|
if (g.view.frame.origin.y + translation.y > _maskView.bounds.size.height - g.view.bounds.size.height) {
|
|
g.view.center = CGPointMake(g.view.center.x, g.view.center.y + translation.y);
|
|
}
|
|
} break;
|
|
case zhPopupLayoutTypeTop: {
|
|
if (g.view.frame.origin.y + g.view.frame.size.height + translation.y < g.view.bounds.size.height) {
|
|
g.view.center = CGPointMake(g.view.center.x, g.view.center.y + translation.y);
|
|
}
|
|
} break;
|
|
case zhPopupLayoutTypeLeft: {
|
|
if (g.view.frame.origin.x + g.view.frame.size.width + translation.x < g.view.bounds.size.width) {
|
|
g.view.center = CGPointMake(g.view.center.x + translation.x, g.view.center.y);
|
|
}
|
|
} break;
|
|
case zhPopupLayoutTypeRight: {
|
|
if (g.view.frame.origin.x + translation.x > _maskView.bounds.size.width - g.view.bounds.size.width) {
|
|
g.view.center = CGPointMake(g.view.center.x + translation.x, g.view.center.y);
|
|
}
|
|
} break;
|
|
default: break;
|
|
}
|
|
[g setTranslation:CGPointZero inView:_maskView];
|
|
} break;
|
|
case UIGestureRecognizerStateEnded: {
|
|
|
|
BOOL isWillDismiss = YES, isStyleCentered = NO;
|
|
switch (_layoutType) {
|
|
case zhPopupLayoutTypeCenter: {
|
|
isStyleCentered = YES;
|
|
if (g.view.center.y != _maskView.center.y) {
|
|
if (g.view.center.y > _maskView.bounds.size.height * 0.25 &&
|
|
g.view.center.y < _maskView.bounds.size.height * 0.75) {
|
|
isWillDismiss = NO;
|
|
}
|
|
} else {
|
|
if (g.view.center.x > _maskView.bounds.size.width * 0.25 &&
|
|
g.view.center.x < _maskView.bounds.size.width * 0.75) {
|
|
isWillDismiss = NO;
|
|
}
|
|
}
|
|
} break;
|
|
case zhPopupLayoutTypeBottom:
|
|
isWillDismiss = g.view.frame.origin.y > _maskView.bounds.size.height - g.view.frame.size.height * 0.75;
|
|
break;
|
|
case zhPopupLayoutTypeTop:
|
|
isWillDismiss = g.view.frame.origin.y + g.view.frame.size.height < g.view.frame.size.height * 0.75;
|
|
break;
|
|
case zhPopupLayoutTypeLeft:
|
|
isWillDismiss = g.view.frame.origin.x + g.view.frame.size.width < g.view.frame.size.width * 0.75;
|
|
break;
|
|
case zhPopupLayoutTypeRight:
|
|
isWillDismiss = g.view.frame.origin.x > _maskView.bounds.size.width - g.view.frame.size.width * 0.75;
|
|
break;
|
|
default: break;
|
|
}
|
|
if (isWillDismiss) {
|
|
if (isStyleCentered) {
|
|
switch (_slideStyle) {
|
|
case zhPopupSlideStyleShrinkInOut1:
|
|
case zhPopupSlideStyleShrinkInOut2:
|
|
case zhPopupSlideStyleFade: {
|
|
if (g.view.center.y < _maskView.bounds.size.height * 0.25) {
|
|
_slideStyle = zhPopupSlideStyleFromTop;
|
|
} else {
|
|
if (g.view.center.y > _maskView.bounds.size.height * 0.75) {
|
|
_slideStyle = zhPopupSlideStyleFromBottom;
|
|
}
|
|
}
|
|
_dismissOppositeDirection = NO;
|
|
} break;
|
|
case zhPopupSlideStyleFromTop:
|
|
_dismissOppositeDirection = !(g.view.center.y < _maskView.bounds.size.height * 0.25);
|
|
break;
|
|
case zhPopupSlideStyleFromBottom:
|
|
_dismissOppositeDirection = g.view.center.y < _maskView.bounds.size.height * 0.25;
|
|
break;
|
|
case zhPopupSlideStyleFromLeft:
|
|
_dismissOppositeDirection = !(g.view.center.x < _maskView.bounds.size.width * 0.25);
|
|
break;
|
|
case zhPopupSlideStyleFromRight:
|
|
_dismissOppositeDirection = g.view.center.x < _maskView.bounds.size.width * 0.25;
|
|
break;
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
[self dismissWithDuration:0.25f springAnimated:NO];
|
|
|
|
} else {
|
|
// restore view location.
|
|
id object = objc_getAssociatedObject(self, zhPopupControllerParametersKey);
|
|
NSNumber *flagNumber = [object valueForKey:@"zh_springAnimated"];
|
|
BOOL flag = NO;
|
|
if (nil != flagNumber) {
|
|
flag = flagNumber.boolValue;
|
|
}
|
|
NSTimeInterval duration = 0.25;
|
|
NSNumber* durationNumber = [object valueForKey:@"zh_duration"];
|
|
if (nil != durationNumber) {
|
|
duration = durationNumber.doubleValue;
|
|
}
|
|
if (flag) {
|
|
[UIView animateWithDuration:duration delay:0.0 usingSpringWithDamping:0.6f initialSpringVelocity:0.2 options:UIViewAnimationOptionCurveLinear animations:^{
|
|
g.view.center = [self finishedCenter];
|
|
} completion:NULL];
|
|
} else {
|
|
[UIView animateWithDuration:duration delay:0.0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
|
|
g.view.center = [self finishedCenter];
|
|
} completion:NULL];
|
|
}
|
|
}
|
|
|
|
} break;
|
|
case UIGestureRecognizerStateCancelled:
|
|
break;
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
- (void)dealloc {
|
|
[self unbindNotificationEvent];
|
|
[self removeSubviews];
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation UIViewController (zhPopupController)
|
|
|
|
- (zhPopupController *)zh_popupController {
|
|
id popupController = objc_getAssociatedObject(self, _cmd);
|
|
if (nil == popupController) {
|
|
popupController = [[zhPopupController alloc] init];
|
|
self.zh_popupController = popupController;
|
|
}
|
|
return popupController;
|
|
}
|
|
|
|
- (void)setZh_popupController:(zhPopupController *)zh_popupController {
|
|
objc_setAssociatedObject(self, @selector(zh_popupController), zh_popupController, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
}
|
|
|
|
@end
|