HWPanModalContainerView.m 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  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)presentAnimationWillBegin {
  72. [[self presentable] panModalTransitionWillBegin];
  73. [self layoutBackgroundView];
  74. if ([[self presentable] originPresentationState] == PresentationStateLong) {
  75. self.currentPresentationState = PresentationStateLong;
  76. } else if ([[self presentable] originPresentationState] == PresentationStateMedium) {
  77. self.currentPresentationState = PresentationStateMedium;
  78. }
  79. [self addSubview:self.panContainerView];
  80. [self layoutPresentedView];
  81. [self.handler configureScrollViewInsets];
  82. [[self presentable] presentedViewDidMoveToSuperView];
  83. }
  84. - (void)beginPresentAnimation {
  85. self.isPresenting = YES;
  86. CGFloat yPos = self.contentView.shortFormYPos;
  87. if ([[self presentable] originPresentationState] == PresentationStateLong) {
  88. yPos = self.contentView.longFormYPos;
  89. } else if ([[self presentable] originPresentationState] == PresentationStateMedium) {
  90. yPos = self.contentView.mediumFormYPos;
  91. }
  92. // refresh layout
  93. [self configureViewLayout];
  94. [self adjustPresentedViewFrame];
  95. self.panContainerView.hw_top = self.hw_height;
  96. if ([[self presentable] isHapticFeedbackEnabled]) {
  97. if (@available(iOS 10.0, *)) {
  98. [self.feedbackGenerator selectionChanged];
  99. }
  100. }
  101. [HWPanModalAnimator animate:^{
  102. self.panContainerView.hw_top = yPos;
  103. self.backgroundView.dimState = DimStateMax;
  104. } config:[self presentable] completion:^(BOOL completion) {
  105. self.isPresenting = NO;
  106. [[self presentable] panModalTransitionDidFinish];
  107. if (@available(iOS 10.0, *)) {
  108. self.feedbackGenerator = nil;
  109. }
  110. }];
  111. }
  112. - (void)layoutSubviews {
  113. [super layoutSubviews];
  114. [self configureViewLayout];
  115. }
  116. #pragma mark - public method
  117. - (void)setNeedsLayoutUpdate {
  118. [self configureViewLayout];
  119. [self updateBackgroundColor];
  120. [self.handler observeScrollable];
  121. [self adjustPresentedViewFrame];
  122. [self.handler configureScrollViewInsets];
  123. [self updateContainerViewShadow];
  124. [self updateDragIndicatorView];
  125. [self updateRoundedCorners];
  126. }
  127. - (void)updateUserHitBehavior {
  128. [self checkBackgroundViewEventPass];
  129. [self checkPanGestureRecognizer];
  130. }
  131. - (void)transitionToState:(PresentationState)state animated:(BOOL)animated {
  132. if (![self.presentable shouldTransitionToState:state]) return;
  133. [self.dragIndicatorView didChangeToState:HWIndicatorStateNormal];
  134. [self.presentable willTransitionToState:state];
  135. switch (state) {
  136. case PresentationStateLong: {
  137. [self snapToYPos:self.handler.longFormYPosition animated:animated];
  138. }
  139. break;
  140. case PresentationStateMedium: {
  141. [self snapToYPos:self.handler.mediumFormYPosition animated:animated];
  142. }
  143. break;
  144. case PresentationStateShort: {
  145. [self snapToYPos:self.handler.shortFormYPosition animated:animated];
  146. }
  147. break;
  148. default:
  149. break;
  150. }
  151. self.currentPresentationState = state;
  152. [[self presentable] didChangeTransitionToState:state];
  153. }
  154. - (void)setScrollableContentOffset:(CGPoint)offset animated:(BOOL)animated {
  155. [self.handler setScrollableContentOffset:offset animated:animated];
  156. }
  157. #pragma mark - layout
  158. - (void)adjustPresentedViewFrame {
  159. CGRect frame = self.frame;
  160. CGSize size = CGSizeMake(CGRectGetWidth(frame), CGRectGetHeight(frame) - self.handler.anchoredYPosition);
  161. self.panContainerView.hw_size = frame.size;
  162. self.panContainerView.contentView.frame = CGRectMake(0, 0, size.width, size.height);
  163. self.contentView.frame = self.panContainerView.contentView.bounds;
  164. [self.contentView setNeedsLayout];
  165. [self.contentView layoutIfNeeded];
  166. }
  167. - (void)configureViewLayout {
  168. [self.handler configureViewLayout];
  169. self.userInteractionEnabled = [[self presentable] isUserInteractionEnabled];
  170. }
  171. - (void)layoutBackgroundView {
  172. [self addSubview:self.backgroundView];
  173. [self updateBackgroundColor];
  174. self.backgroundView.translatesAutoresizingMaskIntoConstraints = NO;
  175. NSArray *hCons = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[backgroundView]|" options:0 metrics:nil views:@{@"backgroundView": self.backgroundView}];
  176. NSArray *vCons = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[backgroundView]|" options:0 metrics:nil views:@{@"backgroundView": self.backgroundView}];
  177. [NSLayoutConstraint activateConstraints:hCons];
  178. [NSLayoutConstraint activateConstraints:vCons];
  179. }
  180. - (void)updateBackgroundColor {
  181. self.backgroundView.blurTintColor = [self.presentable backgroundConfig].blurTintColor;
  182. }
  183. - (void)layoutPresentedView {
  184. if (!self.presentable)
  185. return;
  186. self.handler.presentedView = self.panContainerView;
  187. if ([[self presentable] allowsTouchEventsPassingThroughTransitionView]) {
  188. [self.panContainerView addGestureRecognizer:self.handler.panGestureRecognizer];
  189. } else {
  190. [self addGestureRecognizer:self.handler.panGestureRecognizer];
  191. }
  192. [self setNeedsLayoutUpdate];
  193. [self adjustPanContainerBackgroundColor];
  194. }
  195. - (void)adjustPanContainerBackgroundColor {
  196. self.panContainerView.contentView.backgroundColor = self.contentView.backgroundColor ? : [self.presentable panScrollable].backgroundColor;
  197. }
  198. - (void)updateDragIndicatorView {
  199. if ([self.presentable showDragIndicator]) {
  200. [self addDragIndicatorViewToView:self.panContainerView];
  201. } else {
  202. self.dragIndicatorView.hidden = YES;
  203. }
  204. }
  205. - (void)addDragIndicatorViewToView:(UIView *)view {
  206. // if has been add, won't update it.
  207. self.dragIndicatorView.hidden = NO;
  208. if (self.dragIndicatorView.superview == view) {
  209. [self.dragIndicatorView didChangeToState:HWIndicatorStateNormal];
  210. return;
  211. }
  212. self.handler.dragIndicatorView = self.dragIndicatorView;
  213. [view addSubview:self.dragIndicatorView];
  214. CGSize indicatorSize = [self.dragIndicatorView indicatorSize];
  215. self.dragIndicatorView.frame = CGRectMake((view.hw_width - indicatorSize.width) / 2, -kIndicatorYOffset - indicatorSize.height, indicatorSize.width, indicatorSize.height);
  216. [self.dragIndicatorView setupSubviews];
  217. [self.dragIndicatorView didChangeToState:HWIndicatorStateNormal];
  218. }
  219. - (void)updateContainerViewShadow {
  220. HWPanModalShadow shadow = [[self presentable] contentShadow];
  221. if (shadow.shadowColor) {
  222. [self.panContainerView updateShadow:shadow.shadowColor shadowRadius:shadow.shadowRadius shadowOffset:shadow.shadowOffset shadowOpacity:shadow.shadowOpacity];
  223. } else {
  224. [self.panContainerView clearShadow];
  225. }
  226. }
  227. - (void)updateRoundedCorners {
  228. if ([self.presentable shouldRoundTopCorners]) {
  229. [self addRoundedCornersToView:self.panContainerView.contentView];
  230. } else {
  231. [self resetRoundedCornersToView:self.panContainerView.contentView];
  232. }
  233. }
  234. - (void)addRoundedCornersToView:(UIView *)view {
  235. CGFloat radius = [self.presentable cornerRadius];
  236. UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRoundedRect:view.bounds byRoundingCorners:UIRectCornerTopRight | UIRectCornerTopLeft cornerRadii:CGSizeMake(radius, radius)];
  237. CAShapeLayer *mask = [CAShapeLayer new];
  238. mask.path = bezierPath.CGPath;
  239. view.layer.mask = mask;
  240. // 提高性能
  241. view.layer.shouldRasterize = YES;
  242. view.layer.rasterizationScale = [UIScreen mainScreen].scale;
  243. }
  244. - (void)resetRoundedCornersToView:(UIView *)view {
  245. view.layer.mask = nil;
  246. view.layer.shouldRasterize = NO;
  247. }
  248. - (void)snapToYPos:(CGFloat)yPos animated:(BOOL)animated {
  249. if (animated) {
  250. [HWPanModalAnimator animate:^{
  251. self.isPresentedViewAnimating = YES;
  252. [self adjustToYPos:yPos];
  253. } config:self.presentable completion:^(BOOL completion) {
  254. self.isPresentedViewAnimating = NO;
  255. }];
  256. } else {
  257. [self adjustToYPos:yPos];
  258. }
  259. }
  260. - (void)adjustToYPos:(CGFloat)yPos {
  261. self.panContainerView.hw_top = MAX(yPos, self.handler.anchoredYPosition);
  262. // change dim background starting from shortFormYPosition.
  263. if (self.panContainerView.frame.origin.y >= self.handler.shortFormYPosition) {
  264. CGFloat yDistanceFromShortForm = self.panContainerView.frame.origin.y - self.handler.shortFormYPosition;
  265. CGFloat bottomHeight = self.hw_height - self.handler.shortFormYPosition;
  266. CGFloat percent = yDistanceFromShortForm / bottomHeight;
  267. self.backgroundView.dimState = DimStatePercent;
  268. self.backgroundView.percent = 1 - percent;
  269. [self.presentable panModalGestureRecognizer:self.handler.panGestureRecognizer dismissPercent:MIN(percent, 1)];
  270. } else {
  271. self.backgroundView.dimState = DimStateMax;
  272. }
  273. }
  274. #pragma mark - HWPanModalPresentableHandlerDelegate
  275. - (void)adjustPresentableYPos:(CGFloat)yPos {
  276. [self adjustToYPos:yPos];
  277. }
  278. - (void)presentableTransitionToState:(PresentationState)state {
  279. [self transitionToState:state animated:YES];
  280. }
  281. - (PresentationState)getCurrentPresentationState {
  282. return self.currentPresentationState;
  283. }
  284. - (void)dismiss:(BOOL)isInteractive mode:(PanModalInteractiveMode)mode {
  285. self.handler.panGestureRecognizer.enabled = NO;
  286. self.isDismissing = YES;
  287. [[self presentable] panModalWillDismiss];
  288. [HWPanModalAnimator animate:^{
  289. self.panContainerView.hw_top = CGRectGetHeight(self.bounds);
  290. self.backgroundView.dimState = DimStateOff;
  291. self.dragIndicatorView.alpha = 0;
  292. } config:[self presentable] completion:^(BOOL completion) {
  293. [self removeFromSuperview];
  294. [[self presentable] panModalDidDismissed];
  295. self.animationBlock ? self.animationBlock() : nil;
  296. self.isDismissing = NO;
  297. }];
  298. }
  299. #pragma mark - HWPanModalPresentableHandlerDataSource
  300. - (CGSize)containerSize {
  301. return self.presentingView.bounds.size;
  302. }
  303. - (BOOL)isBeingDismissed {
  304. return self.isDismissing;
  305. }
  306. - (BOOL)isBeingPresented {
  307. return self.isPresenting;
  308. }
  309. - (BOOL)isFormPositionAnimating {
  310. return self.isPresentedViewAnimating;
  311. }
  312. - (BOOL)isPresentedViewAnchored {
  313. if (![[self presentable] shouldRespondToPanModalGestureRecognizer:self.handler.panGestureRecognizer]) {
  314. return YES;
  315. }
  316. 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))) {
  317. return YES;
  318. }
  319. return NO;
  320. }
  321. #pragma mark - event handle
  322. - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
  323. if (!self.userInteractionEnabled || self.hidden || self.alpha < 0.01) {
  324. return nil;
  325. }
  326. if (![self pointInside:point withEvent:event]) {
  327. return nil;
  328. }
  329. BOOL eventThrough = [[self presentable] allowsTouchEventsPassingThroughTransitionView];
  330. if (eventThrough) {
  331. CGPoint convertedPoint = [self.panContainerView convertPoint:point fromView:self];
  332. if (CGRectGetWidth(self.panContainerView.frame) >= convertedPoint.x &&
  333. convertedPoint.x > 0 &&
  334. CGRectGetHeight(self.panContainerView.frame) >= convertedPoint.y &&
  335. convertedPoint.y > 0) {
  336. return [super hitTest:point withEvent:event];
  337. } else {
  338. return nil;
  339. }
  340. } else {
  341. return [super hitTest:point withEvent:event];
  342. }
  343. }
  344. - (void)checkBackgroundViewEventPass {
  345. if ([[self presentable] allowsTouchEventsPassingThroughTransitionView]) {
  346. self.backgroundView.userInteractionEnabled = NO;
  347. self.backgroundView.tapBlock = nil;
  348. } else {
  349. self.backgroundView.userInteractionEnabled = YES;
  350. __weak typeof(self) wkSelf = self;
  351. self.backgroundView.tapBlock = ^(UITapGestureRecognizer *recognizer) {
  352. if ([[wkSelf presentable] allowsTapBackgroundToDismiss]) {
  353. [wkSelf dismiss:NO mode:PanModalInteractiveModeNone];
  354. }
  355. };
  356. }
  357. }
  358. - (void)checkPanGestureRecognizer {
  359. if ([[self presentable] allowsTouchEventsPassingThroughTransitionView]) {
  360. [self removeGestureRecognizer:self.handler.panGestureRecognizer];
  361. [self.panContainerView addGestureRecognizer:self.handler.panGestureRecognizer];
  362. } else {
  363. [self.panContainerView removeGestureRecognizer:self.handler.panGestureRecognizer];
  364. [self addGestureRecognizer:self.handler.panGestureRecognizer];
  365. }
  366. }
  367. #pragma mark - getter
  368. - (id<HWPanModalPresentable>)presentable {
  369. if ([self.contentView conformsToProtocol:@protocol(HWPanModalPresentable)]) {
  370. return self.contentView;
  371. }
  372. return nil;
  373. }
  374. - (HWDimmedView *)backgroundView {
  375. if (!_backgroundView) {
  376. if (self.presentable) {
  377. _backgroundView = [[HWDimmedView alloc] initWithBackgroundConfig:[self.presentable backgroundConfig]];
  378. } else {
  379. _backgroundView = [[HWDimmedView alloc] init];
  380. }
  381. if ([[self presentable] allowsTouchEventsPassingThroughTransitionView]) {
  382. _backgroundView.userInteractionEnabled = NO;
  383. } else {
  384. __weak typeof(self) wkSelf = self;
  385. _backgroundView.tapBlock = ^(UITapGestureRecognizer *recognizer) {
  386. if ([[wkSelf presentable] allowsTapBackgroundToDismiss]) {
  387. [wkSelf dismiss:NO mode:PanModalInteractiveModeNone];
  388. }
  389. };
  390. }
  391. }
  392. return _backgroundView;
  393. }
  394. - (HWPanContainerView *)panContainerView {
  395. if (!_panContainerView) {
  396. _panContainerView = [[HWPanContainerView alloc] initWithPresentedView:self.contentView frame:self.bounds];
  397. }
  398. return _panContainerView;
  399. }
  400. - (UIView<HWPanModalIndicatorProtocol> *)dragIndicatorView {
  401. if (!_dragIndicatorView) {
  402. if ([self presentable] &&
  403. [[self presentable] respondsToSelector:@selector(customIndicatorView)] &&
  404. [[self presentable] customIndicatorView] != nil) {
  405. _dragIndicatorView = [[self presentable] customIndicatorView];
  406. // set the indicator size first in case `setupSubviews` can Not get the right size.
  407. _dragIndicatorView.hw_size = [[[self presentable] customIndicatorView] indicatorSize];
  408. } else {
  409. _dragIndicatorView = [HWPanIndicatorView new];
  410. }
  411. }
  412. return _dragIndicatorView;
  413. }
  414. @end