HWPanModalPresentationController.m 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923
  1. //
  2. // HWPanModalPresentationController.m
  3. // HWPanModal
  4. //
  5. // Created by heath wang on 2019/4/26.
  6. //
  7. #import "HWPanModalPresentationController.h"
  8. #import "HWDimmedView.h"
  9. #import "HWPanContainerView.h"
  10. #import "UIViewController+LayoutHelper.h"
  11. #import "HWPanModalAnimator.h"
  12. #import "KVOController.h"
  13. #import "HWPanModalInteractiveAnimator.h"
  14. #import "HWPanModalPresentationDelegate.h"
  15. #import "UIViewController+PanModalPresenter.h"
  16. #import "HWPanIndicatorView.h"
  17. #import "UIView+HW_Frame.h"
  18. static CGFloat const kIndicatorYOffset = 0;
  19. static CGFloat const kSnapMovementSensitivity = 0.7;
  20. static NSString *const kScrollViewKVOContentOffsetKey = @"contentOffset";
  21. @interface UIScrollView (Helper)
  22. @property (nonatomic, assign, readonly) BOOL isScrolling;
  23. @end
  24. @interface HWPanModalPresentationController () <UIGestureRecognizerDelegate>
  25. // 判断弹出的view是否在做动画
  26. @property (nonatomic, assign) BOOL isPresentedViewAnimating;
  27. // HWPanModalPresentable config
  28. @property (nonatomic, assign) BOOL extendsPanScrolling;
  29. @property (nonatomic, assign) BOOL anchorModalToLongForm;
  30. @property (nonatomic, assign) BOOL originalScrollableShowsVerticalScrollIndicator;
  31. @property (nonatomic, assign) CGFloat scrollViewYOffset;
  32. @property (nonatomic, assign) CGFloat shortFormYPosition;
  33. @property (nonatomic, assign) CGFloat longFormYPosition;
  34. @property (nonatomic, assign) CGFloat anchoredYPosition;
  35. @property (nonatomic, assign) PresentationState currentPresentationState;
  36. @property (nonatomic, strong) id<HWPanModalPresentable> presentable;
  37. // view
  38. @property (nonatomic, strong) HWDimmedView *backgroundView;
  39. @property (nonatomic, strong) HWPanContainerView *panContainerView;
  40. @property (nonatomic, strong) UIView<HWPanModalIndicatorProtocol> *dragIndicatorView;
  41. // gesture
  42. @property (nonatomic, strong) UIPanGestureRecognizer *panGestureRecognizer;
  43. @property (nonatomic, strong) UIScreenEdgePanGestureRecognizer *screenGestureRecognizer;
  44. // keyboard handle
  45. @property (nonatomic, copy) NSDictionary *keyboardInfo;
  46. @end
  47. @implementation HWPanModalPresentationController
  48. - (instancetype)initWithPresentedViewController:(UIViewController *)presentedViewController presentingViewController:(nullable UIViewController *)presentingViewController {
  49. self = [super initWithPresentedViewController:presentedViewController presentingViewController:presentingViewController];
  50. if (self) {
  51. // make props as default
  52. _extendsPanScrolling = YES;
  53. _anchorModalToLongForm = YES;
  54. [self addKeyboardObserver];
  55. }
  56. return self;
  57. }
  58. #pragma mark - overridden
  59. - (UIView *)presentedView {
  60. return self.panContainerView;
  61. }
  62. - (void)containerViewWillLayoutSubviews {
  63. [super containerViewWillLayoutSubviews];
  64. [self configureViewLayout];
  65. }
  66. - (void)presentationTransitionWillBegin {
  67. if (!self.containerView)
  68. return;
  69. [self layoutBackgroundView:self.containerView];
  70. [self layoutPresentedView:self.containerView];
  71. [self configureScrollViewInsets];
  72. if (!self.presentedViewController.transitionCoordinator) {
  73. self.backgroundView.dimState = DimStateMax;
  74. return;
  75. }
  76. __weak typeof(self) wkSelf = self;
  77. [self.presentedViewController.transitionCoordinator animateAlongsideTransition:^(id <UIViewControllerTransitionCoordinatorContext> context) {
  78. wkSelf.backgroundView.dimState = DimStateMax;
  79. [self.presentedViewController setNeedsStatusBarAppearanceUpdate];
  80. } completion:nil];
  81. }
  82. - (void)presentationTransitionDidEnd:(BOOL)completed {
  83. if (completed)
  84. return;
  85. [self.backgroundView removeFromSuperview];
  86. [self.panContainerView endEditing:YES];
  87. }
  88. - (void)dismissalTransitionWillBegin {
  89. id <UIViewControllerTransitionCoordinator> transitionCoordinator = self.presentedViewController.transitionCoordinator;
  90. if (!transitionCoordinator) {
  91. self.backgroundView.dimState = DimStateOff;
  92. return;
  93. }
  94. __weak typeof(self) wkSelf = self;
  95. [transitionCoordinator animateAlongsideTransition:^(id <UIViewControllerTransitionCoordinatorContext> context) {
  96. wkSelf.dragIndicatorView.alpha = 0;
  97. wkSelf.backgroundView.dimState = DimStateOff;
  98. [wkSelf.presentedViewController setNeedsStatusBarAppearanceUpdate];
  99. } completion:^(id <UIViewControllerTransitionCoordinatorContext> context) {
  100. }];
  101. }
  102. - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator {
  103. [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
  104. [coordinator animateAlongsideTransition:^(id <UIViewControllerTransitionCoordinatorContext> context) {
  105. if (self && [self presentable]) {
  106. [self adjustPresentedViewFrame];
  107. if ([self.presentable shouldRoundTopCorners]) {
  108. [self addRoundedCornersToView:self.panContainerView.contentView];
  109. }
  110. }
  111. } completion:^(id <UIViewControllerTransitionCoordinatorContext> context) {
  112. [self transitionToState:self.currentPresentationState];
  113. }];
  114. }
  115. #pragma mark - public method
  116. - (void)setNeedsLayoutUpdate {
  117. [self configureViewLayout];
  118. [self updateBackgroundColor];
  119. [self adjustPresentedViewFrame];
  120. [self checkEdgeInteractive];
  121. [self observe:[self.presentable panScrollable]];
  122. [self configureScrollViewInsets];
  123. }
  124. - (void)transitionToState:(PresentationState)state {
  125. [self.dragIndicatorView didChangeToState:HWIndicatorStateNormal];
  126. if (![self.presentable shouldTransitionToState:state])
  127. return;
  128. [self.presentable willTransitionToState:state];
  129. switch (state) {
  130. case PresentationStateLong: {
  131. [self snapToYPos:self.longFormYPosition];
  132. }
  133. break;
  134. case PresentationStateShort:{
  135. [self snapToYPos:self.shortFormYPosition];
  136. }
  137. break;
  138. default:
  139. break;
  140. }
  141. self.currentPresentationState = state;
  142. }
  143. - (void)setContentOffset:(CGPoint)offset {
  144. if (![self.presentable panScrollable])
  145. return;
  146. UIScrollView *scrollView = [self.presentable panScrollable];
  147. [self.KVOController unobserve:scrollView];
  148. [scrollView setContentOffset:offset animated:YES];
  149. // wait for animation finished.
  150. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.30 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
  151. [self trackScrolling:scrollView];
  152. [self observe:scrollView];
  153. });
  154. }
  155. #pragma mark - layout
  156. - (BOOL)isPresentedViewAnchored {
  157. if (!self.isPresentedViewAnimating && self.extendsPanScrolling && CGRectGetMinY(self.presentedView.frame) <= self.anchoredYPosition) {
  158. return YES;
  159. }
  160. return NO;
  161. }
  162. - (void)adjustPresentedViewFrame {
  163. if (!self.containerView)
  164. return;
  165. CGRect frame = self.containerView.frame;
  166. CGSize size = CGSizeMake(CGRectGetWidth(frame), CGRectGetHeight(frame) - self.anchoredYPosition);
  167. self.presentedView.hw_size = frame.size;
  168. self.panContainerView.contentView.frame = CGRectMake(0, 0, size.width, size.height);
  169. self.presentedViewController.view.frame = self.panContainerView.contentView.bounds;
  170. [self.presentedViewController.view setNeedsLayout];
  171. [self.presentedViewController.view layoutIfNeeded];
  172. }
  173. - (void)configureScrollViewInsets {
  174. // when scrolling, return
  175. if ([self.presentable panScrollable] && ![self.presentable panScrollable].isScrolling) {
  176. UIScrollView *scrollView = [self.presentable panScrollable];
  177. // 禁用scrollView indicator除非用户开始滑动scrollView
  178. scrollView.showsVerticalScrollIndicator = NO;
  179. scrollView.scrollEnabled = [self.presentable isPanScrollEnabled];
  180. scrollView.scrollIndicatorInsets = [self.presentable scrollIndicatorInsets];
  181. UIEdgeInsets insets1 = scrollView.contentInset;
  182. /*
  183. * If scrollView has been set contentInset, and bottom is NOT zero, we won't change it.
  184. * If contentInset.bottom is zero, set bottom = bottomLayoutOffset
  185. * If scrollView has been set contentInset, BUT the bottom < bottomLayoutOffset, set bottom = bottomLayoutOffset
  186. */
  187. if (HW_FLOAT_IS_ZERO(insets1.bottom) || insets1.bottom < self.presentedViewController.bottomLayoutOffset) {
  188. insets1.bottom = self.presentedViewController.bottomLayoutOffset;
  189. scrollView.contentInset = insets1;
  190. }
  191. if (@available(iOS 11.0, *)) {
  192. scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
  193. } else {
  194. // Fallback on earlier versions
  195. }
  196. }
  197. }
  198. /**
  199. * add backGroundView并设置约束
  200. */
  201. - (void)layoutBackgroundView:(UIView *)containerView {
  202. [containerView addSubview:self.backgroundView];
  203. [self updateBackgroundColor];
  204. self.backgroundView.translatesAutoresizingMaskIntoConstraints = NO;
  205. NSArray *hCons = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[backgroundView]|" options:0 metrics:nil views:@{@"backgroundView": self.backgroundView}];
  206. NSArray *vCons = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[backgroundView]|" options:0 metrics:nil views:@{@"backgroundView": self.backgroundView}];
  207. [NSLayoutConstraint activateConstraints:hCons];
  208. [NSLayoutConstraint activateConstraints:vCons];
  209. }
  210. - (void)updateBackgroundColor {
  211. self.backgroundView.blurTintColor = [self.presentable backgroundBlurColor];
  212. }
  213. - (void)layoutPresentedView:(UIView *)containerView {
  214. if (!self.presentable)
  215. return;
  216. [containerView addSubview:self.presentedView];
  217. [containerView addGestureRecognizer:self.panGestureRecognizer];
  218. if ([self.presentable allowScreenEdgeInteractive]) {
  219. [containerView addGestureRecognizer:self.screenGestureRecognizer];
  220. }
  221. if ([self.presentable shouldRoundTopCorners]) {
  222. [self addRoundedCornersToView:self.panContainerView.contentView];
  223. }
  224. if ([self.presentable showDragIndicator]) {
  225. [self addDragIndicatorViewToView:self.panContainerView];
  226. }
  227. [self setNeedsLayoutUpdate];
  228. [self adjustPanContainerBackgroundColor];
  229. }
  230. - (void)adjustPanContainerBackgroundColor {
  231. self.panContainerView.contentView.backgroundColor = self.presentedViewController.view.backgroundColor ? : [self.presentable panScrollable].backgroundColor;
  232. }
  233. - (void)addDragIndicatorViewToView:(UIView *)view {
  234. [view addSubview:self.dragIndicatorView];
  235. CGSize indicatorSize = [self.dragIndicatorView indicatorSize];
  236. self.dragIndicatorView.translatesAutoresizingMaskIntoConstraints = NO;
  237. // layout
  238. NSLayoutConstraint *bottomCons = [NSLayoutConstraint constraintWithItem:self.dragIndicatorView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.presentedView attribute:NSLayoutAttributeTop multiplier:1 constant:-kIndicatorYOffset];
  239. NSLayoutConstraint *centerXCons = [NSLayoutConstraint constraintWithItem:self.dragIndicatorView attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self.presentedView attribute:NSLayoutAttributeCenterX multiplier:1 constant:0];
  240. NSLayoutConstraint *widthCons = [NSLayoutConstraint constraintWithItem:self.dragIndicatorView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:indicatorSize.width];
  241. NSLayoutConstraint *heightCons = [NSLayoutConstraint constraintWithItem:self.dragIndicatorView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:indicatorSize.height];
  242. [view addConstraints:@[bottomCons, centerXCons, widthCons, heightCons]];
  243. [self.dragIndicatorView setupSubviews];
  244. [self.dragIndicatorView didChangeToState:HWIndicatorStateNormal];
  245. }
  246. - (void)addRoundedCornersToView:(UIView *)view {
  247. CGFloat radius = [self.presentable cornerRadius];
  248. UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRoundedRect:view.bounds byRoundingCorners:UIRectCornerTopRight | UIRectCornerTopLeft cornerRadii:CGSizeMake(radius, radius)];
  249. CAShapeLayer *mask = [CAShapeLayer new];
  250. mask.path = bezierPath.CGPath;
  251. view.layer.mask = mask;
  252. // 提高性能
  253. view.layer.shouldRasterize = YES;
  254. view.layer.rasterizationScale = [UIScreen mainScreen].scale;
  255. }
  256. - (void)snapToYPos:(CGFloat)yPos {
  257. [HWPanModalAnimator animate:^{
  258. self.isPresentedViewAnimating = YES;
  259. [self adjustToYPos:yPos];
  260. } config:self.presentable completion:^(BOOL completion) {
  261. self.isPresentedViewAnimating = NO;
  262. }];
  263. }
  264. - (void)adjustToYPos:(CGFloat)yPos {
  265. self.presentedView.hw_top = MAX(yPos, self.anchoredYPosition);
  266. // change dim background starting from shortFormYPosition.
  267. if (self.presentedView.frame.origin.y >= self.shortFormYPosition) {
  268. CGFloat yDistanceFromShortForm = self.presentedView.frame.origin.y - self.shortFormYPosition;
  269. CGFloat bottomHeight = self.containerView.hw_height - self.shortFormYPosition;
  270. CGFloat percent = yDistanceFromShortForm / bottomHeight;
  271. self.backgroundView.dimState = DimStatePercent;
  272. self.backgroundView.percent = 1 - percent;
  273. [self.presentable panModalGestureRecognizer:self.panGestureRecognizer dismissPercent:MIN(percent, 1)];
  274. } else {
  275. self.backgroundView.dimState = DimStateMax;
  276. }
  277. }
  278. /**
  279. * Caluclates & stores the layout anchor points & options
  280. */
  281. - (void)configureViewLayout {
  282. if ([self.presentedViewController conformsToProtocol:@protocol(HWPanModalPresentable)]) {
  283. UIViewController<HWPanModalPresentable> *layoutPresentable = (UIViewController <HWPanModalPresentable> *) self.presentedViewController;
  284. self.shortFormYPosition = layoutPresentable.shortFormYPos;
  285. self.longFormYPosition = layoutPresentable.longFormYPos;
  286. self.anchorModalToLongForm = [layoutPresentable anchorModalToLongForm];
  287. self.extendsPanScrolling = [layoutPresentable allowsExtendedPanScrolling];
  288. self.originalScrollableShowsVerticalScrollIndicator = [layoutPresentable panScrollable].showsVerticalScrollIndicator;
  289. self.containerView.userInteractionEnabled = [layoutPresentable isUserInteractionEnabled];
  290. }
  291. }
  292. #pragma mark - UIScrollView handle
  293. - (void)observe:(UIScrollView *)scrollView {
  294. if (!scrollView) {
  295. return;
  296. }
  297. self.scrollViewYOffset = MAX(scrollView.contentOffset.y, -(MAX(scrollView.contentInset.top, 0)));
  298. __weak typeof(self) wkSelf = self;
  299. [self.KVOController observe:scrollView keyPath:kScrollViewKVOContentOffsetKey options:NSKeyValueObservingOptionOld block:^(id observer, id object, NSDictionary<NSString *, id > *change) {
  300. if (wkSelf.containerView != nil) {
  301. [wkSelf didPanOnScrollView:object change:change];
  302. }
  303. }];
  304. }
  305. /**
  306. As the user scrolls, track & save the scroll view y offset.
  307. This helps halt scrolling when we want to hold the scroll view in place.
  308. */
  309. - (void)trackScrolling:(UIScrollView *)scrollView {
  310. self.scrollViewYOffset = MAX(scrollView.contentOffset.y, -(MAX(scrollView.contentInset.top, 0)));
  311. scrollView.showsVerticalScrollIndicator = self.originalScrollableShowsVerticalScrollIndicator ? YES : NO;
  312. }
  313. /**
  314. * Halts the scroll of a given scroll view & anchors it at the `scrollViewYOffset`
  315. */
  316. - (void)haltScrolling:(UIScrollView *)scrollView {
  317. [scrollView setContentOffset:CGPointMake(0, self.scrollViewYOffset) animated:NO];
  318. scrollView.showsVerticalScrollIndicator = NO;
  319. }
  320. - (void)didPanOnScrollView:(UIScrollView *)scrollView change:(NSDictionary<NSKeyValueChangeKey, id> *)change {
  321. if (!self.presentedViewController.isBeingDismissed && !self.presentedViewController.isBeingPresented) {
  322. if (!self.isPresentedViewAnchored && scrollView.contentOffset.y > 0) {
  323. [self haltScrolling:scrollView];
  324. } else if ([scrollView isScrolling] || self.isPresentedViewAnimating) {
  325. /**
  326. While we're scrolling upwards on the scrollView,
  327. store the last content offset position
  328. */
  329. if (self.isPresentedViewAnchored) {
  330. [self trackScrolling:scrollView];
  331. } else {
  332. /**
  333. * Keep scroll view in place while we're panning on main view
  334. */
  335. [self haltScrolling:scrollView];
  336. }
  337. }
  338. // else if ([self.presentedViewController.view isKindOfClass:UIScrollView.class] && !self.isPresentedViewAnimating && scrollView.contentOffset.y <= 0) {
  339. // /**
  340. // * In the case where we drag down quickly on the scroll view and let go,
  341. // `handleScrollViewTopBounce` adds a nice elegant touch.
  342. // */
  343. // [self handleScrollViewTopBounce:scrollView change:change];
  344. // }
  345. else {
  346. [self trackScrolling:scrollView];
  347. }
  348. } else {
  349. /**
  350. * 当present Controller,而且动画没有结束的时候,用户可能会对scrollView设置contentOffset
  351. * 首次用户滑动scrollView时,会因为scrollViewYOffset = 0而出现错位
  352. */
  353. [self setContentOffset:scrollView.contentOffset];
  354. }
  355. }
  356. /**
  357. To ensure that the scroll transition between the scrollView & the modal
  358. is completely seamless, we need to handle the case where content offset is negative.
  359. In this case, we follow the curve of the decelerating scroll view.
  360. This gives the effect that the modal view and the scroll view are one view entirely.
  361. - Note: This works best where the view behind view controller is a UIScrollView.
  362. So, for example, a UITableViewController.
  363. */
  364. - (void)handleScrollViewTopBounce:(UIScrollView *)scrollView change:(NSDictionary<NSKeyValueChangeKey, id> *)change {
  365. NSValue *value = change[NSKeyValueChangeOldKey];
  366. if (value) {
  367. CGPoint offset = [value CGPointValue];
  368. CGFloat yOffset = scrollView.contentOffset.y;
  369. CGSize presentedSize = self.containerView.frame.size;
  370. self.presentedView.hw_size = CGSizeMake(presentedSize.width, presentedSize.height + yOffset);
  371. self.panContainerView.contentView.hw_size = CGSizeMake(self.presentedView.hw_width, self.presentedView.hw_height - self.anchoredYPosition);
  372. self.presentedViewController.view.frame = self.panContainerView.contentView.bounds;
  373. if (offset.y > yOffset) {
  374. self.presentedView.hw_top = self.longFormYPosition - yOffset;
  375. } else {
  376. self.scrollViewYOffset = 0;
  377. [self snapToYPos:self.longFormYPosition];
  378. }
  379. scrollView.showsVerticalScrollIndicator = NO;
  380. }
  381. }
  382. #pragma mark - Pan Gesture Event Handler
  383. - (void)didPanOnView:(UIPanGestureRecognizer *)panGestureRecognizer {
  384. if ([self shouldResponseToPanGestureRecognizer:panGestureRecognizer] && self.containerView && !self.keyboardInfo) {
  385. CGPoint velocity = [panGestureRecognizer velocityInView:self.presentedView];
  386. switch (panGestureRecognizer.state) {
  387. case UIGestureRecognizerStateBegan:
  388. case UIGestureRecognizerStateChanged: {
  389. [self respondToPanGestureRecognizer:panGestureRecognizer];
  390. if (self.presentedView.frame.origin.y == self.anchoredYPosition && self.extendsPanScrolling) {
  391. [self.presentable willTransitionToState:PresentationStateLong];
  392. }
  393. if (panGestureRecognizer.state == UIGestureRecognizerStateChanged) {
  394. if (velocity.y > 0) {
  395. [self.dragIndicatorView didChangeToState:HWIndicatorStatePullDown];
  396. } else if (velocity.y < 0 && self.presentedView.frame.origin.y <= self.anchoredYPosition && !self.extendsPanScrolling) {
  397. [self.dragIndicatorView didChangeToState:HWIndicatorStateNormal];
  398. }
  399. }
  400. }
  401. break;
  402. default: {
  403. /**
  404. * pan recognizer结束
  405. * 根据velocity(速度),当velocity.y < 0,说明用户在向上拖拽view;当velocity.y > 0,向下拖拽
  406. * 根据拖拽的速度,处理不同的情况:
  407. * 1.超过拖拽速度阈值时并且向下拖拽,dismiss controller
  408. * 2.向上拖拽永远不会dismiss,回弹至相应的状态
  409. */
  410. if ([self isVelocityWithinSensitivityRange:velocity.y]) {
  411. if (velocity.y < 0) {
  412. [self transitionToState:PresentationStateLong];
  413. } 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) ||
  414. ![self.presentable allowsDragToDismiss]) {
  415. [self transitionToState:PresentationStateShort];
  416. } else {
  417. [self dismissPresentedViewController];
  418. }
  419. } else {
  420. CGFloat position = [self nearestDistance:CGRectGetMinY(self.presentedView.frame) inDistances:@[@(self.containerView.frame.size.height), @(self.shortFormYPosition), @(self.longFormYPosition)]];
  421. if (HW_TWO_FLOAT_IS_EQUAL(position, self.longFormYPosition)) {
  422. [self transitionToState:PresentationStateLong];
  423. } else if (HW_TWO_FLOAT_IS_EQUAL(position, self.shortFormYPosition) || ![self.presentable allowsDragToDismiss]) {
  424. [self transitionToState:PresentationStateShort];
  425. } else {
  426. [self dismissPresentedViewController];
  427. }
  428. }
  429. }
  430. break;
  431. }
  432. } else {
  433. switch (panGestureRecognizer.state) {
  434. case UIGestureRecognizerStateEnded:
  435. case UIGestureRecognizerStateCancelled:
  436. case UIGestureRecognizerStateFailed: {
  437. [self.dragIndicatorView didChangeToState:HWIndicatorStateNormal];
  438. }
  439. break;
  440. default:
  441. break;
  442. }
  443. [panGestureRecognizer setTranslation:CGPointZero inView:panGestureRecognizer.view];
  444. }
  445. }
  446. - (BOOL)shouldResponseToPanGestureRecognizer:(UIPanGestureRecognizer *)panGestureRecognizer {
  447. if ([self.presentable shouldRespondToPanModalGestureRecognizer:panGestureRecognizer] ||
  448. !(panGestureRecognizer.state == UIGestureRecognizerStateBegan || panGestureRecognizer.state == UIGestureRecognizerStateCancelled)) {
  449. return ![self shouldFailPanGestureRecognizer:panGestureRecognizer];
  450. } else {
  451. panGestureRecognizer.enabled = NO;
  452. panGestureRecognizer.enabled = YES;
  453. return NO;
  454. }
  455. }
  456. - (BOOL)shouldFailPanGestureRecognizer:(UIPanGestureRecognizer *)panGestureRecognizer {
  457. if ([self shouldPrioritizePanGestureRecognizer:panGestureRecognizer]) {
  458. // high priority than scroll view gesture, disable scrollView gesture.
  459. [self.presentable panScrollable].panGestureRecognizer.enabled = NO;
  460. [self.presentable panScrollable].panGestureRecognizer.enabled = YES;
  461. return NO;
  462. }
  463. BOOL shouldFail = NO;
  464. UIScrollView *scrollView = [self.presentable panScrollable];
  465. if (scrollView) {
  466. shouldFail = scrollView.contentOffset.y > -MAX(scrollView.contentInset.top, 0);
  467. if (self.isPresentedViewAnchored && shouldFail) {
  468. CGPoint location = [panGestureRecognizer locationInView:self.presentedView];
  469. BOOL flag = CGRectContainsPoint(scrollView.frame, location) || scrollView.isScrolling;
  470. if (flag) {
  471. [self.dragIndicatorView didChangeToState:HWIndicatorStateNormal];
  472. }
  473. return flag;
  474. } else {
  475. return NO;
  476. }
  477. } else {
  478. return NO;
  479. }
  480. }
  481. - (BOOL)shouldPrioritizePanGestureRecognizer:(UIPanGestureRecognizer *)recognizer {
  482. return recognizer.state == UIGestureRecognizerStateBegan && [[self presentable] shouldPrioritizePanModalGestureRecognizer:recognizer];
  483. }
  484. - (void)respondToPanGestureRecognizer:(UIPanGestureRecognizer *)panGestureRecognizer {
  485. [self.presentable willRespondToPanModalGestureRecognizer:panGestureRecognizer];
  486. CGFloat yDisplacement = [panGestureRecognizer translationInView:self.presentedView].y;
  487. if (self.presentedView.frame.origin.y < self.longFormYPosition) {
  488. yDisplacement = yDisplacement / 2;
  489. }
  490. [self adjustToYPos:self.presentedView.frame.origin.y + yDisplacement];
  491. [panGestureRecognizer setTranslation:CGPointZero inView:self.presentedView];
  492. }
  493. - (BOOL)isVelocityWithinSensitivityRange:(CGFloat)velocity {
  494. return (ABS(velocity) - (1000 * (1 - kSnapMovementSensitivity))) > 0;
  495. }
  496. - (CGFloat)nearestDistance:(CGFloat)position inDistances:(NSArray *)distances {
  497. if (distances.count <= 0) {
  498. return position;
  499. }
  500. // TODO: need refine this sort code.
  501. NSMutableArray *tmpArr = [NSMutableArray arrayWithCapacity:distances.count];
  502. NSMutableDictionary *tmpDict = [NSMutableDictionary dictionaryWithCapacity:distances.count];
  503. [distances enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
  504. NSNumber *number = obj;
  505. NSNumber *absValue = @(fabs(number.floatValue - position));
  506. [tmpArr addObject:absValue];
  507. [tmpDict setObject:number forKey:absValue];
  508. }];
  509. [tmpArr sortUsingSelector:@selector(compare:)];
  510. NSNumber *result = tmpDict[tmpArr.firstObject];
  511. return result.floatValue;
  512. }
  513. - (void)dismissPresentedViewController {
  514. [self.presentable panModalWillDismiss];
  515. [self.presentedViewController dismissViewControllerAnimated:YES completion:^{
  516. [self.presentable panModalDidDismissed];
  517. }];
  518. }
  519. #pragma mark - Screen Gesture enevt
  520. - (void)screenEdgeInteractiveAction:(UIScreenEdgePanGestureRecognizer *)recognizer {
  521. CGPoint translation = [recognizer translationInView:recognizer.view];
  522. CGFloat percent = translation.x / CGRectGetWidth(recognizer.view.bounds);
  523. switch (recognizer.state) {
  524. case UIGestureRecognizerStateBegan:
  525. {
  526. self.presentedViewController.presentationDelegate.interactive = YES;
  527. [self.presentedViewController dismissViewControllerAnimated:YES completion:NULL];
  528. }
  529. break;
  530. case UIGestureRecognizerStateCancelled:
  531. case UIGestureRecognizerStateEnded:
  532. {
  533. if (percent > 0.5) {
  534. [[self interactiveAnimator] finishInteractiveTransition];
  535. } else {
  536. [[self interactiveAnimator] cancelInteractiveTransition];
  537. }
  538. self.presentedViewController.presentationDelegate.interactive = NO;
  539. }
  540. break;
  541. case UIGestureRecognizerStateChanged:
  542. {
  543. [[self interactiveAnimator] updateInteractiveTransition:percent];
  544. }
  545. break;
  546. default:
  547. break;
  548. }
  549. }
  550. - (void)checkEdgeInteractive {
  551. if ([self.presentedViewController isKindOfClass:UINavigationController.class]) {
  552. UINavigationController *navigationController = (UINavigationController *) self.presentedViewController;
  553. if ((navigationController.topViewController != navigationController.viewControllers.firstObject) &&
  554. [[self presentable] allowScreenEdgeInteractive] &&
  555. navigationController.viewControllers.count > 0) {
  556. self.screenGestureRecognizer.enabled = NO;
  557. } else if ([[self presentable] allowScreenEdgeInteractive]) {
  558. self.screenGestureRecognizer.enabled = YES;
  559. }
  560. }
  561. }
  562. #pragma mark - UIGestureRecognizerDelegate
  563. /**
  564. * 只有当其他的UIGestureRecognizer为pan recognizer时才能同时存在
  565. */
  566. - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
  567. if ([gestureRecognizer isKindOfClass:UIPanGestureRecognizer.class]) {
  568. return [otherGestureRecognizer isKindOfClass:UIPanGestureRecognizer.class];
  569. }
  570. return NO;
  571. }
  572. /**
  573. * 当当前手势为screenGestureRecognizer时,其他pan recognizer都应该fail
  574. */
  575. - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
  576. if (gestureRecognizer == self.screenGestureRecognizer && [otherGestureRecognizer isKindOfClass:UIPanGestureRecognizer.class]) {
  577. return YES;
  578. }
  579. return NO;
  580. }
  581. #pragma mark - UIKeyboard Handle
  582. - (void)addKeyboardObserver {
  583. if ([self.presentable isAutoHandleKeyboardEnabled]) {
  584. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
  585. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
  586. }
  587. }
  588. - (void)removeKeyboardObserver {
  589. [[NSNotificationCenter defaultCenter] removeObserver:self];
  590. }
  591. - (void)keyboardWillShow:(NSNotification *)notification {
  592. UIView<UIKeyInput> *currentInput = [self findCurrentTextInputInView:self.panContainerView];
  593. if (!currentInput)
  594. return;
  595. self.keyboardInfo = notification.userInfo;
  596. [self updatePanContainerFrameForKeyboard];
  597. }
  598. - (void)keyboardWillHide:(NSNotification *)notification {
  599. self.keyboardInfo = nil;
  600. NSTimeInterval duration = [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
  601. UIViewAnimationCurve curve = (UIViewAnimationCurve) [notification.userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue];
  602. [UIView beginAnimations:nil context:nil];
  603. [UIView setAnimationBeginsFromCurrentState:YES];
  604. [UIView setAnimationCurve:curve];
  605. [UIView setAnimationDuration:duration];
  606. self.panContainerView.transform = CGAffineTransformIdentity;
  607. [UIView commitAnimations];
  608. }
  609. - (void)updatePanContainerFrameForKeyboard {
  610. if (!self.keyboardInfo)
  611. return;
  612. UIView<UIKeyInput> *textInput = [self findCurrentTextInputInView:self.panContainerView];
  613. if (!textInput)
  614. return;
  615. CGAffineTransform lastTransform = self.panContainerView.transform;
  616. self.panContainerView.transform = CGAffineTransformIdentity;
  617. CGFloat textViewBottomY = [textInput convertRect:textInput.bounds toView:self.panContainerView].origin.y + textInput.hw_height;
  618. CGFloat keyboardHeight = [self.keyboardInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue].size.height;
  619. CGFloat offsetY = 0;
  620. CGFloat top = [self.presentable keyboardOffsetFromInputView];
  621. offsetY = self.panContainerView.hw_height - (keyboardHeight + top + textViewBottomY + self.panContainerView.hw_top);
  622. NSTimeInterval duration = [self.keyboardInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
  623. UIViewAnimationCurve curve = (UIViewAnimationCurve) [self.keyboardInfo[UIKeyboardAnimationCurveUserInfoKey] intValue];
  624. self.panContainerView.transform = lastTransform;
  625. [UIView beginAnimations:nil context:NULL];
  626. [UIView setAnimationBeginsFromCurrentState:YES];
  627. [UIView setAnimationCurve:curve];
  628. [UIView setAnimationDuration:duration];
  629. self.panContainerView.transform = CGAffineTransformMakeTranslation(0, offsetY);
  630. [UIView commitAnimations];
  631. }
  632. - (UIView <UIKeyInput> *)findCurrentTextInputInView:(UIView *)view {
  633. if ([view conformsToProtocol:@protocol(UIKeyInput)] && view.isFirstResponder) {
  634. // Quick fix for web view issue
  635. if ([view isKindOfClass:NSClassFromString(@"UIWebBrowserView")] || [view isKindOfClass:NSClassFromString(@"WKContentView")]) {
  636. return nil;
  637. }
  638. return (UIView <UIKeyInput> *) view;
  639. }
  640. for (UIView *subview in view.subviews) {
  641. UIView <UIKeyInput> *inputInView = [self findCurrentTextInputInView:subview];
  642. if (inputInView) {
  643. return inputInView;
  644. }
  645. }
  646. return nil;
  647. }
  648. #pragma mark - Getter
  649. - (CGFloat)anchoredYPosition {
  650. CGFloat defaultTopOffset = [self.presentable topOffset];
  651. return self.anchorModalToLongForm ? self.longFormYPosition : defaultTopOffset;
  652. }
  653. - (id <HWPanModalPresentable>)presentable {
  654. if ([self.presentedViewController conformsToProtocol:@protocol(HWPanModalPresentable)]) {
  655. return (id <HWPanModalPresentable>) self.presentedViewController;
  656. }
  657. return nil;
  658. }
  659. - (HWPanModalInteractiveAnimator *)interactiveAnimator {
  660. HWPanModalPresentationDelegate *presentationDelegate = self.presentedViewController.presentationDelegate;
  661. return presentationDelegate.interactiveDismissalAnimator;
  662. }
  663. - (HWDimmedView *)backgroundView {
  664. if (!_backgroundView) {
  665. if (self.presentable) {
  666. _backgroundView = [[HWDimmedView alloc] initWithDimAlpha:[self.presentable backgroundAlpha] blurRadius:[self.presentable backgroundBlurRadius]];
  667. } else {
  668. _backgroundView = [[HWDimmedView alloc] init];
  669. }
  670. __weak typeof(self) wkSelf = self;
  671. _backgroundView.tapBlock = ^(UITapGestureRecognizer *recognizer) {
  672. if ([[wkSelf presentable] allowsTapBackgroundToDismiss]) {
  673. [wkSelf dismissPresentedViewController];
  674. }
  675. };
  676. }
  677. return _backgroundView;
  678. }
  679. - (HWPanContainerView *)panContainerView {
  680. if (!_panContainerView) {
  681. _panContainerView = [[HWPanContainerView alloc] initWithPresentedView:self.presentedViewController.view frame:self.containerView.frame];
  682. }
  683. return _panContainerView;
  684. }
  685. - (UIView<HWPanModalIndicatorProtocol> *)dragIndicatorView {
  686. if (!_dragIndicatorView) {
  687. if ([self presentable] &&
  688. [[self presentable] respondsToSelector:@selector(customIndicatorView)] &&
  689. [[self presentable] customIndicatorView] != nil) {
  690. _dragIndicatorView = [[self presentable] customIndicatorView];
  691. // set the indicator size first in case `setupSubviews` can Not get the right size.
  692. _dragIndicatorView.hw_size = [[[self presentable] customIndicatorView] indicatorSize];
  693. } else {
  694. _dragIndicatorView = [HWPanIndicatorView new];
  695. }
  696. }
  697. return _dragIndicatorView;
  698. }
  699. - (UIPanGestureRecognizer *)panGestureRecognizer {
  700. if (!_panGestureRecognizer) {
  701. _panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(didPanOnView:)];
  702. _panGestureRecognizer.minimumNumberOfTouches = 1;
  703. _panGestureRecognizer.maximumNumberOfTouches = 1;
  704. _panGestureRecognizer.delegate = self;
  705. }
  706. return _panGestureRecognizer;
  707. }
  708. - (UIScreenEdgePanGestureRecognizer *)screenGestureRecognizer {
  709. if (!_screenGestureRecognizer) {
  710. _screenGestureRecognizer = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(screenEdgeInteractiveAction:)];
  711. _screenGestureRecognizer.minimumNumberOfTouches = 1;
  712. _screenGestureRecognizer.maximumNumberOfTouches = 1;
  713. _screenGestureRecognizer.delegate = self;
  714. _screenGestureRecognizer.edges = UIRectEdgeLeft;
  715. }
  716. return _screenGestureRecognizer;
  717. }
  718. #pragma mark - dealloc
  719. - (void)dealloc {
  720. [self removeKeyboardObserver];
  721. }
  722. @end
  723. @implementation UIScrollView (Helper)
  724. - (BOOL)isScrolling {
  725. return (self.isDragging && !self.isDecelerating) || self.isTracking;
  726. }
  727. @end