123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923 |
- //
- // HWPanModalPresentationController.m
- // HWPanModal
- //
- // Created by heath wang on 2019/4/26.
- //
- #import "HWPanModalPresentationController.h"
- #import "HWDimmedView.h"
- #import "HWPanContainerView.h"
- #import "UIViewController+LayoutHelper.h"
- #import "HWPanModalAnimator.h"
- #import "KVOController.h"
- #import "HWPanModalInteractiveAnimator.h"
- #import "HWPanModalPresentationDelegate.h"
- #import "UIViewController+PanModalPresenter.h"
- #import "HWPanIndicatorView.h"
- #import "UIView+HW_Frame.h"
- static CGFloat const kIndicatorYOffset = 0;
- static CGFloat const kSnapMovementSensitivity = 0.7;
- static NSString *const kScrollViewKVOContentOffsetKey = @"contentOffset";
- @interface UIScrollView (Helper)
- @property (nonatomic, assign, readonly) BOOL isScrolling;
- @end
- @interface HWPanModalPresentationController () <UIGestureRecognizerDelegate>
- // 判断弹出的view是否在做动画
- @property (nonatomic, assign) BOOL isPresentedViewAnimating;
- // HWPanModalPresentable config
- @property (nonatomic, assign) BOOL extendsPanScrolling;
- @property (nonatomic, assign) BOOL anchorModalToLongForm;
- @property (nonatomic, assign) BOOL originalScrollableShowsVerticalScrollIndicator;
- @property (nonatomic, assign) CGFloat scrollViewYOffset;
- @property (nonatomic, assign) CGFloat shortFormYPosition;
- @property (nonatomic, assign) CGFloat longFormYPosition;
- @property (nonatomic, assign) CGFloat anchoredYPosition;
- @property (nonatomic, assign) PresentationState currentPresentationState;
- @property (nonatomic, strong) id<HWPanModalPresentable> presentable;
- // view
- @property (nonatomic, strong) HWDimmedView *backgroundView;
- @property (nonatomic, strong) HWPanContainerView *panContainerView;
- @property (nonatomic, strong) UIView<HWPanModalIndicatorProtocol> *dragIndicatorView;
- // gesture
- @property (nonatomic, strong) UIPanGestureRecognizer *panGestureRecognizer;
- @property (nonatomic, strong) UIScreenEdgePanGestureRecognizer *screenGestureRecognizer;
- // keyboard handle
- @property (nonatomic, copy) NSDictionary *keyboardInfo;
- @end
- @implementation HWPanModalPresentationController
- - (instancetype)initWithPresentedViewController:(UIViewController *)presentedViewController presentingViewController:(nullable UIViewController *)presentingViewController {
- self = [super initWithPresentedViewController:presentedViewController presentingViewController:presentingViewController];
- if (self) {
- // make props as default
- _extendsPanScrolling = YES;
- _anchorModalToLongForm = YES;
- [self addKeyboardObserver];
- }
- return self;
- }
- #pragma mark - overridden
- - (UIView *)presentedView {
- return self.panContainerView;
- }
- - (void)containerViewWillLayoutSubviews {
- [super containerViewWillLayoutSubviews];
- [self configureViewLayout];
- }
- - (void)presentationTransitionWillBegin {
- if (!self.containerView)
- return;
- [self layoutBackgroundView:self.containerView];
- [self layoutPresentedView:self.containerView];
- [self configureScrollViewInsets];
- if (!self.presentedViewController.transitionCoordinator) {
- self.backgroundView.dimState = DimStateMax;
- return;
- }
- __weak typeof(self) wkSelf = self;
- [self.presentedViewController.transitionCoordinator animateAlongsideTransition:^(id <UIViewControllerTransitionCoordinatorContext> context) {
- wkSelf.backgroundView.dimState = DimStateMax;
- [self.presentedViewController setNeedsStatusBarAppearanceUpdate];
- } completion:nil];
- }
- - (void)presentationTransitionDidEnd:(BOOL)completed {
- if (completed)
- return;
- [self.backgroundView removeFromSuperview];
- [self.panContainerView endEditing:YES];
- }
- - (void)dismissalTransitionWillBegin {
- id <UIViewControllerTransitionCoordinator> transitionCoordinator = self.presentedViewController.transitionCoordinator;
- if (!transitionCoordinator) {
- self.backgroundView.dimState = DimStateOff;
- return;
- }
- __weak typeof(self) wkSelf = self;
- [transitionCoordinator animateAlongsideTransition:^(id <UIViewControllerTransitionCoordinatorContext> context) {
- wkSelf.dragIndicatorView.alpha = 0;
- wkSelf.backgroundView.dimState = DimStateOff;
- [wkSelf.presentedViewController setNeedsStatusBarAppearanceUpdate];
- } completion:^(id <UIViewControllerTransitionCoordinatorContext> context) {
- }];
- }
- - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator {
- [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
- [coordinator animateAlongsideTransition:^(id <UIViewControllerTransitionCoordinatorContext> context) {
- if (self && [self presentable]) {
- [self adjustPresentedViewFrame];
- if ([self.presentable shouldRoundTopCorners]) {
- [self addRoundedCornersToView:self.panContainerView.contentView];
- }
- }
- } completion:^(id <UIViewControllerTransitionCoordinatorContext> context) {
- [self transitionToState:self.currentPresentationState];
- }];
- }
- #pragma mark - public method
- - (void)setNeedsLayoutUpdate {
- [self configureViewLayout];
- [self updateBackgroundColor];
- [self adjustPresentedViewFrame];
- [self checkEdgeInteractive];
- [self observe:[self.presentable panScrollable]];
- [self configureScrollViewInsets];
- }
- - (void)transitionToState:(PresentationState)state {
- [self.dragIndicatorView didChangeToState:HWIndicatorStateNormal];
- if (![self.presentable shouldTransitionToState:state])
- return;
- [self.presentable willTransitionToState:state];
- switch (state) {
- case PresentationStateLong: {
- [self snapToYPos:self.longFormYPosition];
- }
- break;
- case PresentationStateShort:{
- [self snapToYPos:self.shortFormYPosition];
- }
- break;
- default:
- break;
- }
- self.currentPresentationState = state;
- }
- - (void)setContentOffset:(CGPoint)offset {
- if (![self.presentable panScrollable])
- return;
- UIScrollView *scrollView = [self.presentable panScrollable];
- [self.KVOController unobserve:scrollView];
- [scrollView setContentOffset:offset animated:YES];
- // wait for animation finished.
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.30 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
- [self trackScrolling:scrollView];
-
- [self observe:scrollView];
- });
-
- }
- #pragma mark - layout
- - (BOOL)isPresentedViewAnchored {
- if (!self.isPresentedViewAnimating && self.extendsPanScrolling && CGRectGetMinY(self.presentedView.frame) <= self.anchoredYPosition) {
- return YES;
- }
- return NO;
- }
- - (void)adjustPresentedViewFrame {
- if (!self.containerView)
- return;
- CGRect frame = self.containerView.frame;
- CGSize size = CGSizeMake(CGRectGetWidth(frame), CGRectGetHeight(frame) - self.anchoredYPosition);
- self.presentedView.hw_size = frame.size;
- self.panContainerView.contentView.frame = CGRectMake(0, 0, size.width, size.height);
- self.presentedViewController.view.frame = self.panContainerView.contentView.bounds;
- [self.presentedViewController.view setNeedsLayout];
- [self.presentedViewController.view layoutIfNeeded];
- }
- - (void)configureScrollViewInsets {
- // when scrolling, return
- if ([self.presentable panScrollable] && ![self.presentable panScrollable].isScrolling) {
- UIScrollView *scrollView = [self.presentable panScrollable];
- // 禁用scrollView indicator除非用户开始滑动scrollView
- scrollView.showsVerticalScrollIndicator = NO;
- scrollView.scrollEnabled = [self.presentable isPanScrollEnabled];
- scrollView.scrollIndicatorInsets = [self.presentable scrollIndicatorInsets];
- UIEdgeInsets insets1 = scrollView.contentInset;
- /*
- * If scrollView has been set contentInset, and bottom is NOT zero, we won't change it.
- * If contentInset.bottom is zero, set bottom = bottomLayoutOffset
- * If scrollView has been set contentInset, BUT the bottom < bottomLayoutOffset, set bottom = bottomLayoutOffset
- */
- if (HW_FLOAT_IS_ZERO(insets1.bottom) || insets1.bottom < self.presentedViewController.bottomLayoutOffset) {
-
- insets1.bottom = self.presentedViewController.bottomLayoutOffset;
- scrollView.contentInset = insets1;
- }
-
- if (@available(iOS 11.0, *)) {
- scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
- } else {
- // Fallback on earlier versions
- }
- }
- }
- /**
- * add backGroundView并设置约束
- */
- - (void)layoutBackgroundView:(UIView *)containerView {
- [containerView addSubview:self.backgroundView];
- [self updateBackgroundColor];
- self.backgroundView.translatesAutoresizingMaskIntoConstraints = NO;
- NSArray *hCons = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[backgroundView]|" options:0 metrics:nil views:@{@"backgroundView": self.backgroundView}];
- NSArray *vCons = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[backgroundView]|" options:0 metrics:nil views:@{@"backgroundView": self.backgroundView}];
- [NSLayoutConstraint activateConstraints:hCons];
- [NSLayoutConstraint activateConstraints:vCons];
- }
- - (void)updateBackgroundColor {
- self.backgroundView.blurTintColor = [self.presentable backgroundBlurColor];
- }
- - (void)layoutPresentedView:(UIView *)containerView {
- if (!self.presentable)
- return;
- [containerView addSubview:self.presentedView];
- [containerView addGestureRecognizer:self.panGestureRecognizer];
- if ([self.presentable allowScreenEdgeInteractive]) {
- [containerView addGestureRecognizer:self.screenGestureRecognizer];
- }
- if ([self.presentable shouldRoundTopCorners]) {
- [self addRoundedCornersToView:self.panContainerView.contentView];
- }
- if ([self.presentable showDragIndicator]) {
- [self addDragIndicatorViewToView:self.panContainerView];
- }
- [self setNeedsLayoutUpdate];
- [self adjustPanContainerBackgroundColor];
- }
- - (void)adjustPanContainerBackgroundColor {
- self.panContainerView.contentView.backgroundColor = self.presentedViewController.view.backgroundColor ? : [self.presentable panScrollable].backgroundColor;
- }
- - (void)addDragIndicatorViewToView:(UIView *)view {
- [view addSubview:self.dragIndicatorView];
- CGSize indicatorSize = [self.dragIndicatorView indicatorSize];
- self.dragIndicatorView.translatesAutoresizingMaskIntoConstraints = NO;
- // layout
- NSLayoutConstraint *bottomCons = [NSLayoutConstraint constraintWithItem:self.dragIndicatorView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.presentedView attribute:NSLayoutAttributeTop multiplier:1 constant:-kIndicatorYOffset];
- NSLayoutConstraint *centerXCons = [NSLayoutConstraint constraintWithItem:self.dragIndicatorView attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self.presentedView attribute:NSLayoutAttributeCenterX multiplier:1 constant:0];
- NSLayoutConstraint *widthCons = [NSLayoutConstraint constraintWithItem:self.dragIndicatorView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:indicatorSize.width];
- NSLayoutConstraint *heightCons = [NSLayoutConstraint constraintWithItem:self.dragIndicatorView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:indicatorSize.height];
- [view addConstraints:@[bottomCons, centerXCons, widthCons, heightCons]];
- [self.dragIndicatorView setupSubviews];
- [self.dragIndicatorView didChangeToState:HWIndicatorStateNormal];
- }
- - (void)addRoundedCornersToView:(UIView *)view {
- CGFloat radius = [self.presentable cornerRadius];
- UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRoundedRect:view.bounds byRoundingCorners:UIRectCornerTopRight | UIRectCornerTopLeft cornerRadii:CGSizeMake(radius, radius)];
-
- CAShapeLayer *mask = [CAShapeLayer new];
- mask.path = bezierPath.CGPath;
- view.layer.mask = mask;
- // 提高性能
- view.layer.shouldRasterize = YES;
- view.layer.rasterizationScale = [UIScreen mainScreen].scale;
- }
- - (void)snapToYPos:(CGFloat)yPos {
- [HWPanModalAnimator animate:^{
- self.isPresentedViewAnimating = YES;
- [self adjustToYPos:yPos];
- } config:self.presentable completion:^(BOOL completion) {
- self.isPresentedViewAnimating = NO;
- }];
- }
- - (void)adjustToYPos:(CGFloat)yPos {
- self.presentedView.hw_top = MAX(yPos, self.anchoredYPosition);
- // change dim background starting from shortFormYPosition.
- if (self.presentedView.frame.origin.y >= self.shortFormYPosition) {
-
- CGFloat yDistanceFromShortForm = self.presentedView.frame.origin.y - self.shortFormYPosition;
- CGFloat bottomHeight = self.containerView.hw_height - self.shortFormYPosition;
- CGFloat percent = yDistanceFromShortForm / bottomHeight;
- self.backgroundView.dimState = DimStatePercent;
- self.backgroundView.percent = 1 - percent;
- [self.presentable panModalGestureRecognizer:self.panGestureRecognizer dismissPercent:MIN(percent, 1)];
- } else {
- self.backgroundView.dimState = DimStateMax;
- }
- }
- /**
- * Caluclates & stores the layout anchor points & options
- */
- - (void)configureViewLayout {
-
- if ([self.presentedViewController conformsToProtocol:@protocol(HWPanModalPresentable)]) {
- UIViewController<HWPanModalPresentable> *layoutPresentable = (UIViewController <HWPanModalPresentable> *) self.presentedViewController;
- self.shortFormYPosition = layoutPresentable.shortFormYPos;
- self.longFormYPosition = layoutPresentable.longFormYPos;
- self.anchorModalToLongForm = [layoutPresentable anchorModalToLongForm];
- self.extendsPanScrolling = [layoutPresentable allowsExtendedPanScrolling];
- self.originalScrollableShowsVerticalScrollIndicator = [layoutPresentable panScrollable].showsVerticalScrollIndicator;
- self.containerView.userInteractionEnabled = [layoutPresentable isUserInteractionEnabled];
- }
- }
- #pragma mark - UIScrollView handle
- - (void)observe:(UIScrollView *)scrollView {
-
- if (!scrollView) {
- return;
- }
-
- self.scrollViewYOffset = MAX(scrollView.contentOffset.y, -(MAX(scrollView.contentInset.top, 0)));
- __weak typeof(self) wkSelf = self;
- [self.KVOController observe:scrollView keyPath:kScrollViewKVOContentOffsetKey options:NSKeyValueObservingOptionOld block:^(id observer, id object, NSDictionary<NSString *, id > *change) {
- if (wkSelf.containerView != nil) {
- [wkSelf didPanOnScrollView:object change:change];
- }
- }];
- }
- /**
- As the user scrolls, track & save the scroll view y offset.
- This helps halt scrolling when we want to hold the scroll view in place.
- */
- - (void)trackScrolling:(UIScrollView *)scrollView {
- self.scrollViewYOffset = MAX(scrollView.contentOffset.y, -(MAX(scrollView.contentInset.top, 0)));
-
- scrollView.showsVerticalScrollIndicator = self.originalScrollableShowsVerticalScrollIndicator ? YES : NO;
- }
- /**
- * Halts the scroll of a given scroll view & anchors it at the `scrollViewYOffset`
- */
- - (void)haltScrolling:(UIScrollView *)scrollView {
- [scrollView setContentOffset:CGPointMake(0, self.scrollViewYOffset) animated:NO];
- scrollView.showsVerticalScrollIndicator = NO;
- }
- - (void)didPanOnScrollView:(UIScrollView *)scrollView change:(NSDictionary<NSKeyValueChangeKey, id> *)change {
- if (!self.presentedViewController.isBeingDismissed && !self.presentedViewController.isBeingPresented) {
- if (!self.isPresentedViewAnchored && scrollView.contentOffset.y > 0) {
- [self haltScrolling:scrollView];
- } else if ([scrollView isScrolling] || self.isPresentedViewAnimating) {
- /**
- While we're scrolling upwards on the scrollView,
- store the last content offset position
- */
- if (self.isPresentedViewAnchored) {
- [self trackScrolling:scrollView];
- } else {
- /**
- * Keep scroll view in place while we're panning on main view
- */
- [self haltScrolling:scrollView];
- }
- }
- // else if ([self.presentedViewController.view isKindOfClass:UIScrollView.class] && !self.isPresentedViewAnimating && scrollView.contentOffset.y <= 0) {
- // /**
- // * In the case where we drag down quickly on the scroll view and let go,
- // `handleScrollViewTopBounce` adds a nice elegant touch.
- // */
- // [self handleScrollViewTopBounce:scrollView change:change];
- // }
- else {
- [self trackScrolling:scrollView];
- }
- } else {
- /**
- * 当present Controller,而且动画没有结束的时候,用户可能会对scrollView设置contentOffset
- * 首次用户滑动scrollView时,会因为scrollViewYOffset = 0而出现错位
- */
- [self setContentOffset:scrollView.contentOffset];
- }
- }
- /**
- To ensure that the scroll transition between the scrollView & the modal
- is completely seamless, we need to handle the case where content offset is negative.
- In this case, we follow the curve of the decelerating scroll view.
- This gives the effect that the modal view and the scroll view are one view entirely.
- - Note: This works best where the view behind view controller is a UIScrollView.
- So, for example, a UITableViewController.
- */
- - (void)handleScrollViewTopBounce:(UIScrollView *)scrollView change:(NSDictionary<NSKeyValueChangeKey, id> *)change {
- NSValue *value = change[NSKeyValueChangeOldKey];
- if (value) {
- CGPoint offset = [value CGPointValue];
- CGFloat yOffset = scrollView.contentOffset.y;
-
- CGSize presentedSize = self.containerView.frame.size;
- self.presentedView.hw_size = CGSizeMake(presentedSize.width, presentedSize.height + yOffset);
- self.panContainerView.contentView.hw_size = CGSizeMake(self.presentedView.hw_width, self.presentedView.hw_height - self.anchoredYPosition);
- self.presentedViewController.view.frame = self.panContainerView.contentView.bounds;
- if (offset.y > yOffset) {
- self.presentedView.hw_top = self.longFormYPosition - yOffset;
- } else {
- self.scrollViewYOffset = 0;
- [self snapToYPos:self.longFormYPosition];
- }
- scrollView.showsVerticalScrollIndicator = NO;
- }
- }
- #pragma mark - Pan Gesture Event Handler
- - (void)didPanOnView:(UIPanGestureRecognizer *)panGestureRecognizer {
- if ([self shouldResponseToPanGestureRecognizer:panGestureRecognizer] && self.containerView && !self.keyboardInfo) {
- CGPoint velocity = [panGestureRecognizer velocityInView:self.presentedView];
- switch (panGestureRecognizer.state) {
- case UIGestureRecognizerStateBegan:
- case UIGestureRecognizerStateChanged: {
- [self respondToPanGestureRecognizer:panGestureRecognizer];
- if (self.presentedView.frame.origin.y == self.anchoredYPosition && self.extendsPanScrolling) {
- [self.presentable willTransitionToState:PresentationStateLong];
- }
- if (panGestureRecognizer.state == UIGestureRecognizerStateChanged) {
- if (velocity.y > 0) {
- [self.dragIndicatorView didChangeToState:HWIndicatorStatePullDown];
- } else if (velocity.y < 0 && self.presentedView.frame.origin.y <= self.anchoredYPosition && !self.extendsPanScrolling) {
- [self.dragIndicatorView didChangeToState:HWIndicatorStateNormal];
- }
- }
- }
- break;
- default: {
- /**
- * pan recognizer结束
- * 根据velocity(速度),当velocity.y < 0,说明用户在向上拖拽view;当velocity.y > 0,向下拖拽
- * 根据拖拽的速度,处理不同的情况:
- * 1.超过拖拽速度阈值时并且向下拖拽,dismiss controller
- * 2.向上拖拽永远不会dismiss,回弹至相应的状态
- */
- if ([self isVelocityWithinSensitivityRange:velocity.y]) {
- if (velocity.y < 0) {
- [self transitionToState:PresentationStateLong];
- } else if ((HW_TWO_FLOAT_IS_EQUAL([self nearestDistance:CGRectGetMinY(self.presentedView.frame) inDistances:@[@(self.longFormYPosition), @(self.containerView.frame.size.height)]], self.longFormYPosition) && CGRectGetMinY(self.presentedView.frame) < self.shortFormYPosition) ||
- ![self.presentable allowsDragToDismiss]) {
- [self transitionToState:PresentationStateShort];
- } else {
- [self dismissPresentedViewController];
- }
- } else {
- CGFloat position = [self nearestDistance:CGRectGetMinY(self.presentedView.frame) inDistances:@[@(self.containerView.frame.size.height), @(self.shortFormYPosition), @(self.longFormYPosition)]];
- if (HW_TWO_FLOAT_IS_EQUAL(position, self.longFormYPosition)) {
- [self transitionToState:PresentationStateLong];
- } else if (HW_TWO_FLOAT_IS_EQUAL(position, self.shortFormYPosition) || ![self.presentable allowsDragToDismiss]) {
- [self transitionToState:PresentationStateShort];
- } else {
- [self dismissPresentedViewController];
- }
- }
- }
- break;
- }
- } else {
- switch (panGestureRecognizer.state) {
- case UIGestureRecognizerStateEnded:
- case UIGestureRecognizerStateCancelled:
- case UIGestureRecognizerStateFailed: {
- [self.dragIndicatorView didChangeToState:HWIndicatorStateNormal];
- }
- break;
- default:
- break;
- }
- [panGestureRecognizer setTranslation:CGPointZero inView:panGestureRecognizer.view];
- }
- }
- - (BOOL)shouldResponseToPanGestureRecognizer:(UIPanGestureRecognizer *)panGestureRecognizer {
- if ([self.presentable shouldRespondToPanModalGestureRecognizer:panGestureRecognizer] ||
- !(panGestureRecognizer.state == UIGestureRecognizerStateBegan || panGestureRecognizer.state == UIGestureRecognizerStateCancelled)) {
- return ![self shouldFailPanGestureRecognizer:panGestureRecognizer];
- } else {
- panGestureRecognizer.enabled = NO;
- panGestureRecognizer.enabled = YES;
- return NO;
- }
- }
- - (BOOL)shouldFailPanGestureRecognizer:(UIPanGestureRecognizer *)panGestureRecognizer {
-
- if ([self shouldPrioritizePanGestureRecognizer:panGestureRecognizer]) {
- // high priority than scroll view gesture, disable scrollView gesture.
- [self.presentable panScrollable].panGestureRecognizer.enabled = NO;
- [self.presentable panScrollable].panGestureRecognizer.enabled = YES;
- return NO;
- }
- BOOL shouldFail = NO;
- UIScrollView *scrollView = [self.presentable panScrollable];
- if (scrollView) {
- shouldFail = scrollView.contentOffset.y > -MAX(scrollView.contentInset.top, 0);
- if (self.isPresentedViewAnchored && shouldFail) {
- CGPoint location = [panGestureRecognizer locationInView:self.presentedView];
- BOOL flag = CGRectContainsPoint(scrollView.frame, location) || scrollView.isScrolling;
- if (flag) {
- [self.dragIndicatorView didChangeToState:HWIndicatorStateNormal];
- }
- return flag;
- } else {
- return NO;
- }
- } else {
- return NO;
- }
-
- }
- - (BOOL)shouldPrioritizePanGestureRecognizer:(UIPanGestureRecognizer *)recognizer {
- return recognizer.state == UIGestureRecognizerStateBegan && [[self presentable] shouldPrioritizePanModalGestureRecognizer:recognizer];
- }
- - (void)respondToPanGestureRecognizer:(UIPanGestureRecognizer *)panGestureRecognizer {
- [self.presentable willRespondToPanModalGestureRecognizer:panGestureRecognizer];
- CGFloat yDisplacement = [panGestureRecognizer translationInView:self.presentedView].y;
- if (self.presentedView.frame.origin.y < self.longFormYPosition) {
- yDisplacement = yDisplacement / 2;
- }
- [self adjustToYPos:self.presentedView.frame.origin.y + yDisplacement];
- [panGestureRecognizer setTranslation:CGPointZero inView:self.presentedView];
- }
- - (BOOL)isVelocityWithinSensitivityRange:(CGFloat)velocity {
- return (ABS(velocity) - (1000 * (1 - kSnapMovementSensitivity))) > 0;
- }
- - (CGFloat)nearestDistance:(CGFloat)position inDistances:(NSArray *)distances {
- if (distances.count <= 0) {
- return position;
- }
-
- // TODO: need refine this sort code.
- NSMutableArray *tmpArr = [NSMutableArray arrayWithCapacity:distances.count];
- NSMutableDictionary *tmpDict = [NSMutableDictionary dictionaryWithCapacity:distances.count];
- [distances enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
- NSNumber *number = obj;
- NSNumber *absValue = @(fabs(number.floatValue - position));
- [tmpArr addObject:absValue];
- [tmpDict setObject:number forKey:absValue];
-
- }];
- [tmpArr sortUsingSelector:@selector(compare:)];
- NSNumber *result = tmpDict[tmpArr.firstObject];
- return result.floatValue;
- }
- - (void)dismissPresentedViewController {
- [self.presentable panModalWillDismiss];
- [self.presentedViewController dismissViewControllerAnimated:YES completion:^{
- [self.presentable panModalDidDismissed];
- }];
- }
- #pragma mark - Screen Gesture enevt
- - (void)screenEdgeInteractiveAction:(UIScreenEdgePanGestureRecognizer *)recognizer {
- CGPoint translation = [recognizer translationInView:recognizer.view];
- CGFloat percent = translation.x / CGRectGetWidth(recognizer.view.bounds);
- switch (recognizer.state) {
- case UIGestureRecognizerStateBegan:
- {
- self.presentedViewController.presentationDelegate.interactive = YES;
- [self.presentedViewController dismissViewControllerAnimated:YES completion:NULL];
- }
- break;
- case UIGestureRecognizerStateCancelled:
- case UIGestureRecognizerStateEnded:
- {
- if (percent > 0.5) {
- [[self interactiveAnimator] finishInteractiveTransition];
- } else {
- [[self interactiveAnimator] cancelInteractiveTransition];
- }
- self.presentedViewController.presentationDelegate.interactive = NO;
- }
- break;
- case UIGestureRecognizerStateChanged:
- {
- [[self interactiveAnimator] updateInteractiveTransition:percent];
- }
- break;
- default:
- break;
- }
- }
- - (void)checkEdgeInteractive {
- if ([self.presentedViewController isKindOfClass:UINavigationController.class]) {
- UINavigationController *navigationController = (UINavigationController *) self.presentedViewController;
- if ((navigationController.topViewController != navigationController.viewControllers.firstObject) &&
- [[self presentable] allowScreenEdgeInteractive] &&
- navigationController.viewControllers.count > 0) {
- self.screenGestureRecognizer.enabled = NO;
- } else if ([[self presentable] allowScreenEdgeInteractive]) {
- self.screenGestureRecognizer.enabled = YES;
- }
- }
- }
- #pragma mark - UIGestureRecognizerDelegate
- /**
- * 只有当其他的UIGestureRecognizer为pan recognizer时才能同时存在
- */
- - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
- if ([gestureRecognizer isKindOfClass:UIPanGestureRecognizer.class]) {
- return [otherGestureRecognizer isKindOfClass:UIPanGestureRecognizer.class];
- }
- return NO;
- }
- /**
- * 当当前手势为screenGestureRecognizer时,其他pan recognizer都应该fail
- */
- - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
- if (gestureRecognizer == self.screenGestureRecognizer && [otherGestureRecognizer isKindOfClass:UIPanGestureRecognizer.class]) {
- return YES;
- }
- return NO;
- }
- #pragma mark - UIKeyboard Handle
- - (void)addKeyboardObserver {
- if ([self.presentable isAutoHandleKeyboardEnabled]) {
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
- }
- }
- - (void)removeKeyboardObserver {
- [[NSNotificationCenter defaultCenter] removeObserver:self];
- }
- - (void)keyboardWillShow:(NSNotification *)notification {
- UIView<UIKeyInput> *currentInput = [self findCurrentTextInputInView:self.panContainerView];
- if (!currentInput)
- return;
- self.keyboardInfo = notification.userInfo;
- [self updatePanContainerFrameForKeyboard];
- }
- - (void)keyboardWillHide:(NSNotification *)notification {
- self.keyboardInfo = nil;
-
- NSTimeInterval duration = [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
- UIViewAnimationCurve curve = (UIViewAnimationCurve) [notification.userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue];
- [UIView beginAnimations:nil context:nil];
- [UIView setAnimationBeginsFromCurrentState:YES];
- [UIView setAnimationCurve:curve];
- [UIView setAnimationDuration:duration];
- self.panContainerView.transform = CGAffineTransformIdentity;
- [UIView commitAnimations];
- }
- - (void)updatePanContainerFrameForKeyboard {
- if (!self.keyboardInfo)
- return;
- UIView<UIKeyInput> *textInput = [self findCurrentTextInputInView:self.panContainerView];
- if (!textInput)
- return;
-
- CGAffineTransform lastTransform = self.panContainerView.transform;
- self.panContainerView.transform = CGAffineTransformIdentity;
-
- CGFloat textViewBottomY = [textInput convertRect:textInput.bounds toView:self.panContainerView].origin.y + textInput.hw_height;
- CGFloat keyboardHeight = [self.keyboardInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue].size.height;
- CGFloat offsetY = 0;
- CGFloat top = [self.presentable keyboardOffsetFromInputView];
- offsetY = self.panContainerView.hw_height - (keyboardHeight + top + textViewBottomY + self.panContainerView.hw_top);
- NSTimeInterval duration = [self.keyboardInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
- UIViewAnimationCurve curve = (UIViewAnimationCurve) [self.keyboardInfo[UIKeyboardAnimationCurveUserInfoKey] intValue];
- self.panContainerView.transform = lastTransform;
- [UIView beginAnimations:nil context:NULL];
- [UIView setAnimationBeginsFromCurrentState:YES];
- [UIView setAnimationCurve:curve];
- [UIView setAnimationDuration:duration];
- self.panContainerView.transform = CGAffineTransformMakeTranslation(0, offsetY);
- [UIView commitAnimations];
- }
- - (UIView <UIKeyInput> *)findCurrentTextInputInView:(UIView *)view {
- if ([view conformsToProtocol:@protocol(UIKeyInput)] && view.isFirstResponder) {
- // Quick fix for web view issue
- if ([view isKindOfClass:NSClassFromString(@"UIWebBrowserView")] || [view isKindOfClass:NSClassFromString(@"WKContentView")]) {
- return nil;
- }
- return (UIView <UIKeyInput> *) view;
- }
- for (UIView *subview in view.subviews) {
- UIView <UIKeyInput> *inputInView = [self findCurrentTextInputInView:subview];
- if (inputInView) {
- return inputInView;
- }
- }
- return nil;
- }
- #pragma mark - Getter
- - (CGFloat)anchoredYPosition {
- CGFloat defaultTopOffset = [self.presentable topOffset];
- return self.anchorModalToLongForm ? self.longFormYPosition : defaultTopOffset;
- }
- - (id <HWPanModalPresentable>)presentable {
- if ([self.presentedViewController conformsToProtocol:@protocol(HWPanModalPresentable)]) {
- return (id <HWPanModalPresentable>) self.presentedViewController;
- }
- return nil;
- }
- - (HWPanModalInteractiveAnimator *)interactiveAnimator {
- HWPanModalPresentationDelegate *presentationDelegate = self.presentedViewController.presentationDelegate;
- return presentationDelegate.interactiveDismissalAnimator;
- }
- - (HWDimmedView *)backgroundView {
- if (!_backgroundView) {
- if (self.presentable) {
- _backgroundView = [[HWDimmedView alloc] initWithDimAlpha:[self.presentable backgroundAlpha] blurRadius:[self.presentable backgroundBlurRadius]];
- } else {
- _backgroundView = [[HWDimmedView alloc] init];
- }
- __weak typeof(self) wkSelf = self;
- _backgroundView.tapBlock = ^(UITapGestureRecognizer *recognizer) {
- if ([[wkSelf presentable] allowsTapBackgroundToDismiss]) {
- [wkSelf dismissPresentedViewController];
- }
- };
- }
-
- return _backgroundView;
- }
- - (HWPanContainerView *)panContainerView {
- if (!_panContainerView) {
- _panContainerView = [[HWPanContainerView alloc] initWithPresentedView:self.presentedViewController.view frame:self.containerView.frame];
- }
- return _panContainerView;
- }
- - (UIView<HWPanModalIndicatorProtocol> *)dragIndicatorView {
-
- if (!_dragIndicatorView) {
- if ([self presentable] &&
- [[self presentable] respondsToSelector:@selector(customIndicatorView)] &&
- [[self presentable] customIndicatorView] != nil) {
- _dragIndicatorView = [[self presentable] customIndicatorView];
- // set the indicator size first in case `setupSubviews` can Not get the right size.
- _dragIndicatorView.hw_size = [[[self presentable] customIndicatorView] indicatorSize];
- } else {
- _dragIndicatorView = [HWPanIndicatorView new];
- }
- }
-
- return _dragIndicatorView;
- }
- - (UIPanGestureRecognizer *)panGestureRecognizer {
- if (!_panGestureRecognizer) {
- _panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(didPanOnView:)];
- _panGestureRecognizer.minimumNumberOfTouches = 1;
- _panGestureRecognizer.maximumNumberOfTouches = 1;
- _panGestureRecognizer.delegate = self;
- }
- return _panGestureRecognizer;
- }
- - (UIScreenEdgePanGestureRecognizer *)screenGestureRecognizer {
- if (!_screenGestureRecognizer) {
- _screenGestureRecognizer = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(screenEdgeInteractiveAction:)];
- _screenGestureRecognizer.minimumNumberOfTouches = 1;
- _screenGestureRecognizer.maximumNumberOfTouches = 1;
- _screenGestureRecognizer.delegate = self;
- _screenGestureRecognizer.edges = UIRectEdgeLeft;
- }
- return _screenGestureRecognizer;
- }
- #pragma mark - dealloc
- - (void)dealloc {
- [self removeKeyboardObserver];
- }
- @end
- @implementation UIScrollView (Helper)
- - (BOOL)isScrolling {
- return (self.isDragging && !self.isDecelerating) || self.isTracking;
- }
- @end
|