HWPanModalContainerView.m 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546
  1. //
  2. // HWPanModalContainerView.m
  3. // Pods
  4. //
  5. // Created by heath wang on 2019/10/17.
  6. //
  7. #import "HWPanModalContainerView.h"
  8. #import "HWPanModalContentView.h"
  9. #import "HWPanModalPresentableHandler.h"
  10. #import "HWDimmedView.h"
  11. #import "HWPanContainerView.h"
  12. #import "UIView+HW_Frame.h"
  13. #import "HWPanIndicatorView.h"
  14. #import "HWPanModalAnimator.h"
  15. @interface HWPanModalContainerView () <HWPanModalPresentableHandlerDelegate, HWPanModalPresentableHandlerDataSource>
  16. @property (nonatomic, strong) HWPanModalContentView<HWPanModalPresentable> *contentView;
  17. @property (nonatomic, weak) UIView *presentingView;
  18. @property (nonatomic, strong) HWPanModalPresentableHandler *handler;
  19. // 判断弹出的view是否在做动画
  20. @property (nonatomic, assign) BOOL isPresentedViewAnimating;
  21. @property (nonatomic, assign) PresentationState currentPresentationState;
  22. @property (nonatomic, assign) BOOL isPresenting;
  23. @property (nonatomic, assign) BOOL isDismissing;
  24. // view
  25. @property (nonatomic, strong) HWDimmedView *backgroundView;
  26. @property (nonatomic, strong) HWPanContainerView *panContainerView;
  27. @property (nonatomic, strong) UIView<HWPanModalIndicatorProtocol> *dragIndicatorView;
  28. @property (nonatomic, copy) void(^animationBlock)(void);
  29. @property (nullable, nonatomic, strong) UISelectionFeedbackGenerator *feedbackGenerator API_AVAILABLE(ios(10.0));
  30. @end
  31. @implementation HWPanModalContainerView
  32. - (instancetype)initWithPresentingView:(UIView *)presentingView contentView:(HWPanModalContentView<HWPanModalPresentable> *)contentView {
  33. self = [super init];
  34. if (self) {
  35. _presentingView = presentingView;
  36. _contentView = contentView;
  37. }
  38. return self;
  39. }
  40. - (void)show {
  41. [self prepare];
  42. [self presentAnimationWillBegin];
  43. [self beginPresentAnimation];
  44. }
  45. - (void)dismissAnimated:(BOOL)flag completion:(void (^)(void))completion {
  46. if (flag) {
  47. self.animationBlock = completion;
  48. [self dismiss:NO mode:PanModalInteractiveModeNone];
  49. } else {
  50. self.isDismissing = YES;
  51. [[self presentable] panModalWillDismiss];
  52. [self removeFromSuperview];
  53. [[self presentable] panModalDidDismissed];
  54. completion ? completion() : nil;
  55. self.isDismissing = NO;
  56. }
  57. }
  58. - (void)prepare {
  59. [self.presentingView addSubview:self];
  60. self.frame = self.presentingView.bounds;
  61. _handler = [[HWPanModalPresentableHandler alloc] initWithPresentable:self.contentView];
  62. _handler.delegate = self;
  63. _handler.dataSource = self;
  64. if (@available(iOS 10.0, *)) {
  65. _feedbackGenerator = [UISelectionFeedbackGenerator new];
  66. [_feedbackGenerator prepare];
  67. } else {
  68. // Fallback on earlier versions
  69. }
  70. }
  71. - (void)willMoveToSuperview:(UIView *)newSuperview {
  72. [super willMoveToSuperview:newSuperview];
  73. if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) {
  74. [self.superview removeObserver:self forKeyPath:@"frame"];
  75. [newSuperview addObserver:self forKeyPath:@"frame" options:NSKeyValueObservingOptionNew context:nil];
  76. }
  77. }
  78. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
  79. if (object == self.presentingView && [keyPath isEqualToString:@"frame"]) {
  80. self.frame = self.presentingView.bounds;
  81. [self setNeedsLayoutUpdate];
  82. [self updateDragIndicatorViewFrame];
  83. [self.contentView hw_panModalTransitionTo:self.contentView.hw_presentationState animated:NO];
  84. } else {
  85. [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
  86. }
  87. }
  88. - (void)presentAnimationWillBegin {
  89. [[self presentable] panModalTransitionWillBegin];
  90. [self layoutBackgroundView];
  91. if ([[self presentable] originPresentationState] == PresentationStateLong) {
  92. self.currentPresentationState = PresentationStateLong;
  93. } else if ([[self presentable] originPresentationState] == PresentationStateMedium) {
  94. self.currentPresentationState = PresentationStateMedium;
  95. }
  96. [self addSubview:self.panContainerView];
  97. [self layoutPresentedView];
  98. [self.handler configureScrollViewInsets];
  99. [[self presentable] presentedViewDidMoveToSuperView];
  100. }
  101. - (void)beginPresentAnimation {
  102. self.isPresenting = YES;
  103. CGFloat yPos = self.contentView.shortFormYPos;
  104. if ([[self presentable] originPresentationState] == PresentationStateLong) {
  105. yPos = self.contentView.longFormYPos;
  106. } else if ([[self presentable] originPresentationState] == PresentationStateMedium) {
  107. yPos = self.contentView.mediumFormYPos;
  108. }
  109. // refresh layout
  110. [self configureViewLayout];
  111. [self adjustPresentedViewFrame];
  112. self.panContainerView.hw_top = self.hw_height;
  113. if ([[self presentable] isHapticFeedbackEnabled]) {
  114. if (@available(iOS 10.0, *)) {
  115. [self.feedbackGenerator selectionChanged];
  116. }
  117. }
  118. [HWPanModalAnimator animate:^{
  119. self.panContainerView.hw_top = yPos;
  120. self.backgroundView.dimState = DimStateMax;
  121. } config:[self presentable] completion:^(BOOL completion) {
  122. self.isPresenting = NO;
  123. [[self presentable] panModalTransitionDidFinish];
  124. if (@available(iOS 10.0, *)) {
  125. self.feedbackGenerator = nil;
  126. }
  127. }];
  128. }
  129. - (void)layoutSubviews {
  130. [super layoutSubviews];
  131. [self configureViewLayout];
  132. }
  133. #pragma mark - public method
  134. - (void)setNeedsLayoutUpdate {
  135. [self configureViewLayout];
  136. [self updateBackgroundColor];
  137. [self.handler observeScrollable];
  138. [self adjustPresentedViewFrame];
  139. [self.handler configureScrollViewInsets];
  140. [self updateContainerViewShadow];
  141. [self updateDragIndicatorView];
  142. [self updateRoundedCorners];
  143. }
  144. - (void)updateUserHitBehavior {
  145. [self checkBackgroundViewEventPass];
  146. [self checkPanGestureRecognizer];
  147. }
  148. - (void)transitionToState:(PresentationState)state animated:(BOOL)animated {
  149. if (![self.presentable shouldTransitionToState:state]) return;
  150. [self.dragIndicatorView didChangeToState:HWIndicatorStateNormal];
  151. [self.presentable willTransitionToState:state];
  152. switch (state) {
  153. case PresentationStateLong: {
  154. [self snapToYPos:self.handler.longFormYPosition animated:animated];
  155. }
  156. break;
  157. case PresentationStateMedium: {
  158. [self snapToYPos:self.handler.mediumFormYPosition animated:animated];
  159. }
  160. break;
  161. case PresentationStateShort: {
  162. [self snapToYPos:self.handler.shortFormYPosition animated:animated];
  163. }
  164. break;
  165. default:
  166. break;
  167. }
  168. self.currentPresentationState = state;
  169. [[self presentable] didChangeTransitionToState:state];
  170. }
  171. - (void)setScrollableContentOffset:(CGPoint)offset animated:(BOOL)animated {
  172. [self.handler setScrollableContentOffset:offset animated:animated];
  173. }
  174. #pragma mark - layout
  175. - (void)adjustPresentedViewFrame {
  176. CGRect frame = self.frame;
  177. CGSize size = CGSizeMake(CGRectGetWidth(frame), CGRectGetHeight(frame) - self.handler.anchoredYPosition);
  178. self.panContainerView.hw_size = frame.size;
  179. self.panContainerView.contentView.frame = CGRectMake(0, 0, size.width, size.height);
  180. self.contentView.frame = self.panContainerView.contentView.bounds;
  181. [self.contentView setNeedsLayout];
  182. [self.contentView layoutIfNeeded];
  183. }
  184. - (void)configureViewLayout {
  185. [self.handler configureViewLayout];
  186. self.userInteractionEnabled = [[self presentable] isUserInteractionEnabled];
  187. }
  188. - (void)layoutBackgroundView {
  189. [self addSubview:self.backgroundView];
  190. [self updateBackgroundColor];
  191. self.backgroundView.translatesAutoresizingMaskIntoConstraints = NO;
  192. NSArray *hCons = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[backgroundView]|" options:0 metrics:nil views:@{@"backgroundView": self.backgroundView}];
  193. NSArray *vCons = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[backgroundView]|" options:0 metrics:nil views:@{@"backgroundView": self.backgroundView}];
  194. [NSLayoutConstraint activateConstraints:hCons];
  195. [NSLayoutConstraint activateConstraints:vCons];
  196. }
  197. - (void)updateBackgroundColor {
  198. self.backgroundView.blurTintColor = [self.presentable backgroundConfig].blurTintColor;
  199. }
  200. - (void)layoutPresentedView {
  201. if (!self.presentable)
  202. return;
  203. self.handler.presentedView = self.panContainerView;
  204. if ([[self presentable] allowsTouchEventsPassingThroughTransitionView]) {
  205. [self.panContainerView addGestureRecognizer:self.handler.panGestureRecognizer];
  206. } else {
  207. [self addGestureRecognizer:self.handler.panGestureRecognizer];
  208. }
  209. [self setNeedsLayoutUpdate];
  210. [self adjustPanContainerBackgroundColor];
  211. }
  212. - (void)adjustPanContainerBackgroundColor {
  213. self.panContainerView.contentView.backgroundColor = self.contentView.backgroundColor ? : [self.presentable panScrollable].backgroundColor;
  214. }
  215. - (void)updateDragIndicatorView {
  216. if ([self.presentable showDragIndicator]) {
  217. [self addDragIndicatorView];
  218. } else {
  219. self.dragIndicatorView.hidden = YES;
  220. }
  221. }
  222. - (void)addDragIndicatorView {
  223. // if has been add, won't update it.
  224. self.dragIndicatorView.hidden = NO;
  225. if (self.dragIndicatorView.superview == self.panContainerView) {
  226. [self updateDragIndicatorViewFrame];
  227. [self.dragIndicatorView didChangeToState:HWIndicatorStateNormal];
  228. return;
  229. }
  230. self.handler.dragIndicatorView = self.dragIndicatorView;
  231. [self.panContainerView addSubview:self.dragIndicatorView];
  232. [self updateDragIndicatorViewFrame];
  233. [self.dragIndicatorView setupSubviews];
  234. [self.dragIndicatorView didChangeToState:HWIndicatorStateNormal];
  235. }
  236. - (void)updateDragIndicatorViewFrame {
  237. CGSize indicatorSize = [self.dragIndicatorView indicatorSize];
  238. self.dragIndicatorView.frame = CGRectMake((self.panContainerView.hw_width - indicatorSize.width) / 2, -kIndicatorYOffset - indicatorSize.height, indicatorSize.width, indicatorSize.height);
  239. }
  240. - (void)updateContainerViewShadow {
  241. HWPanModalShadow *shadow = [[self presentable] contentShadow];
  242. if (shadow.shadowColor) {
  243. [self.panContainerView updateShadow:shadow.shadowColor shadowRadius:shadow.shadowRadius shadowOffset:shadow.shadowOffset shadowOpacity:shadow.shadowOpacity];
  244. } else {
  245. [self.panContainerView clearShadow];
  246. }
  247. }
  248. - (void)updateRoundedCorners {
  249. if ([self.presentable shouldRoundTopCorners]) {
  250. [self addRoundedCornersToView:self.panContainerView.contentView];
  251. } else {
  252. [self resetRoundedCornersToView:self.panContainerView.contentView];
  253. }
  254. }
  255. - (void)addRoundedCornersToView:(UIView *)view {
  256. CGFloat radius = [self.presentable cornerRadius];
  257. UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRoundedRect:view.bounds byRoundingCorners:UIRectCornerTopRight | UIRectCornerTopLeft cornerRadii:CGSizeMake(radius, radius)];
  258. CAShapeLayer *mask = [CAShapeLayer new];
  259. mask.path = bezierPath.CGPath;
  260. view.layer.mask = mask;
  261. // 提高性能
  262. view.layer.shouldRasterize = YES;
  263. view.layer.rasterizationScale = [UIScreen mainScreen].scale;
  264. }
  265. - (void)resetRoundedCornersToView:(UIView *)view {
  266. view.layer.mask = nil;
  267. view.layer.shouldRasterize = NO;
  268. }
  269. - (void)snapToYPos:(CGFloat)yPos animated:(BOOL)animated {
  270. if (animated) {
  271. [HWPanModalAnimator animate:^{
  272. self.isPresentedViewAnimating = YES;
  273. [self adjustToYPos:yPos];
  274. } config:self.presentable completion:^(BOOL completion) {
  275. self.isPresentedViewAnimating = NO;
  276. }];
  277. } else {
  278. [self adjustToYPos:yPos];
  279. }
  280. }
  281. - (void)adjustToYPos:(CGFloat)yPos {
  282. self.panContainerView.hw_top = MAX(yPos, self.handler.anchoredYPosition);
  283. // change dim background starting from shortFormYPosition.
  284. if (self.panContainerView.frame.origin.y >= self.handler.shortFormYPosition) {
  285. CGFloat yDistanceFromShortForm = self.panContainerView.frame.origin.y - self.handler.shortFormYPosition;
  286. CGFloat bottomHeight = self.hw_height - self.handler.shortFormYPosition;
  287. CGFloat percent = yDistanceFromShortForm / bottomHeight;
  288. self.backgroundView.dimState = DimStatePercent;
  289. self.backgroundView.percent = 1 - percent;
  290. [self.presentable panModalGestureRecognizer:self.handler.panGestureRecognizer dismissPercent:MIN(percent, 1)];
  291. } else {
  292. self.backgroundView.dimState = DimStateMax;
  293. }
  294. }
  295. #pragma mark - HWPanModalPresentableHandlerDelegate
  296. - (void)adjustPresentableYPos:(CGFloat)yPos {
  297. [self adjustToYPos:yPos];
  298. }
  299. - (void)presentableTransitionToState:(PresentationState)state {
  300. [self transitionToState:state animated:YES];
  301. }
  302. - (PresentationState)getCurrentPresentationState {
  303. return self.currentPresentationState;
  304. }
  305. - (void)dismiss:(BOOL)isInteractive mode:(PanModalInteractiveMode)mode {
  306. self.handler.panGestureRecognizer.enabled = NO;
  307. self.isDismissing = YES;
  308. [[self presentable] panModalWillDismiss];
  309. [HWPanModalAnimator animate:^{
  310. self.panContainerView.hw_top = CGRectGetHeight(self.bounds);
  311. self.backgroundView.dimState = DimStateOff;
  312. self.dragIndicatorView.alpha = 0;
  313. } config:[self presentable] completion:^(BOOL completion) {
  314. [self removeFromSuperview];
  315. [[self presentable] panModalDidDismissed];
  316. self.animationBlock ? self.animationBlock() : nil;
  317. self.isDismissing = NO;
  318. }];
  319. }
  320. #pragma mark - HWPanModalPresentableHandlerDataSource
  321. - (CGSize)containerSize {
  322. return self.presentingView.bounds.size;
  323. }
  324. - (BOOL)isBeingDismissed {
  325. return self.isDismissing;
  326. }
  327. - (BOOL)isBeingPresented {
  328. return self.isPresenting;
  329. }
  330. - (BOOL)isFormPositionAnimating {
  331. return self.isPresentedViewAnimating;
  332. }
  333. - (BOOL)isPresentedViewAnchored {
  334. if (![[self presentable] shouldRespondToPanModalGestureRecognizer:self.handler.panGestureRecognizer]) {
  335. return YES;
  336. }
  337. if (!self.isPresentedViewAnimating && self.handler.extendsPanScrolling && (CGRectGetMinY(self.panContainerView.frame) <= self.handler.anchoredYPosition || HW_TWO_FLOAT_IS_EQUAL(CGRectGetMinY(self.panContainerView.frame), self.handler.anchoredYPosition))) {
  338. return YES;
  339. }
  340. return NO;
  341. }
  342. #pragma mark - event handle
  343. - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
  344. if (!self.userInteractionEnabled || self.hidden || self.alpha < 0.01) {
  345. return nil;
  346. }
  347. if (![self pointInside:point withEvent:event]) {
  348. return nil;
  349. }
  350. BOOL eventThrough = [[self presentable] allowsTouchEventsPassingThroughTransitionView];
  351. if (eventThrough) {
  352. CGPoint convertedPoint = [self.panContainerView convertPoint:point fromView:self];
  353. if (CGRectGetWidth(self.panContainerView.frame) >= convertedPoint.x &&
  354. convertedPoint.x > 0 &&
  355. CGRectGetHeight(self.panContainerView.frame) >= convertedPoint.y &&
  356. convertedPoint.y > 0) {
  357. return [super hitTest:point withEvent:event];
  358. } else {
  359. return nil;
  360. }
  361. } else {
  362. return [super hitTest:point withEvent:event];
  363. }
  364. }
  365. - (void)checkBackgroundViewEventPass {
  366. if ([[self presentable] allowsTouchEventsPassingThroughTransitionView]) {
  367. self.backgroundView.userInteractionEnabled = NO;
  368. self.backgroundView.tapBlock = nil;
  369. } else {
  370. self.backgroundView.userInteractionEnabled = YES;
  371. __weak typeof(self) wkSelf = self;
  372. self.backgroundView.tapBlock = ^(UITapGestureRecognizer *recognizer) {
  373. if ([[wkSelf presentable] allowsTapBackgroundToDismiss]) {
  374. [wkSelf dismiss:NO mode:PanModalInteractiveModeNone];
  375. }
  376. };
  377. }
  378. }
  379. - (void)checkPanGestureRecognizer {
  380. if ([[self presentable] allowsTouchEventsPassingThroughTransitionView]) {
  381. [self removeGestureRecognizer:self.handler.panGestureRecognizer];
  382. [self.panContainerView addGestureRecognizer:self.handler.panGestureRecognizer];
  383. } else {
  384. [self.panContainerView removeGestureRecognizer:self.handler.panGestureRecognizer];
  385. [self addGestureRecognizer:self.handler.panGestureRecognizer];
  386. }
  387. }
  388. #pragma mark - getter
  389. - (id<HWPanModalPresentable>)presentable {
  390. if ([self.contentView conformsToProtocol:@protocol(HWPanModalPresentable)]) {
  391. return self.contentView;
  392. }
  393. return nil;
  394. }
  395. - (HWDimmedView *)backgroundView {
  396. if (!_backgroundView) {
  397. if (self.presentable) {
  398. _backgroundView = [[HWDimmedView alloc] initWithBackgroundConfig:[self.presentable backgroundConfig]];
  399. } else {
  400. _backgroundView = [[HWDimmedView alloc] init];
  401. }
  402. if ([[self presentable] allowsTouchEventsPassingThroughTransitionView]) {
  403. _backgroundView.userInteractionEnabled = NO;
  404. } else {
  405. __weak typeof(self) wkSelf = self;
  406. _backgroundView.tapBlock = ^(UITapGestureRecognizer *recognizer) {
  407. if ([[wkSelf presentable] allowsTapBackgroundToDismiss]) {
  408. [wkSelf dismiss:NO mode:PanModalInteractiveModeNone];
  409. }
  410. };
  411. }
  412. }
  413. return _backgroundView;
  414. }
  415. - (HWPanContainerView *)panContainerView {
  416. if (!_panContainerView) {
  417. _panContainerView = [[HWPanContainerView alloc] initWithPresentedView:self.contentView frame:self.bounds];
  418. }
  419. return _panContainerView;
  420. }
  421. - (UIView<HWPanModalIndicatorProtocol> *)dragIndicatorView {
  422. if (!_dragIndicatorView) {
  423. if ([self presentable] &&
  424. [[self presentable] respondsToSelector:@selector(customIndicatorView)] &&
  425. [[self presentable] customIndicatorView] != nil) {
  426. _dragIndicatorView = [[self presentable] customIndicatorView];
  427. // set the indicator size first in case `setupSubviews` can Not get the right size.
  428. _dragIndicatorView.hw_size = [[[self presentable] customIndicatorView] indicatorSize];
  429. } else {
  430. _dragIndicatorView = [HWPanIndicatorView new];
  431. }
  432. }
  433. return _dragIndicatorView;
  434. }
  435. @end