// // JYPulleyViewController.m // JYPulleyController // // Created by JunpuChen on 2018/7/30. // Copyright © 2018年 earnestLin. All rights reserved. // #import "JYPulleyViewController.h" #import "JYPulleyScrollView.h" @interface JYPulleyViewController () /// 主内容视图容器 @property (nonatomic, strong) UIView *contentContainerView; /// 抽屉内容视图容器 @property (nonatomic, strong) UIView *drawerContainerView; /// 用于承载 drawerContainerView 和手势传递的 scrollView @property (nonatomic, strong) JYPulleyScrollView *scrollView; /// scrollView 滑动手势 @property (nonatomic, strong) UIPanGestureRecognizer *scrollViewPanGestureRecognizer; /// 背景遮罩点击手势 @property (nonatomic, strong, readwrite) UITapGestureRecognizer *dimmingViewTapGestureRecognizer; /// 记录最后一次滑动位置 @property (nonatomic, assign) CGPoint lastContentOffSet; /// 抽屉视图是否可以滚动 @property (nonatomic, assign) BOOL drawerShouldScroll; /// 当前状态 @property (nonatomic, assign, readwrite) JYPulleyStatus currentStatus; @end @implementation JYPulleyViewController #pragma mark - life cycle - (instancetype)initWithContentDataSource:(id)contentDataSource drawerDataSource:(id)drawerDataSource { if (self = [self initWithNibName:nil bundle:nil]) { NSParameterAssert(contentDataSource); NSParameterAssert(drawerDataSource); _contentDataSource = contentDataSource; _drawerDataSource = drawerDataSource; } return self; } - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) { _drawerClosedHeight = 68.0f; _drawerPartiallyExpandHeight = 264.0f; _drawerExpandTopInset = 20.0f; _dimmingView = [UIView new]; _dimmingView.backgroundColor = [UIColor redColor]; _dimmingOpacity = 0.5f; _currentStatus = JYPulleyStatusClosed; _supportedStatus = JYPulleyStatusNone | JYPulleyStatusClosed | JYPulleyStatusPartiallyExpand | JYPulleyStatusExpand; } return self; } - (void)viewDidLoad { [super viewDidLoad]; [PBNoteCenter addObserver:self selector:@selector(needChangeStatus:) name:@"LinPulBottomViewControllerNeedChangeStatus" object:nil]; [self setupUI]; } - (void)needChangeStatus:(NSNotification *)noti { JYPulleyStatus status = (JYPulleyStatus)[noti.object integerValue]; [self updateStatus:status animated:YES]; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } #pragma mark - UIScrollViewDelegate - (void)scrollViewDidScroll:(UIScrollView *)scrollView { if (scrollView != self.scrollView) { return; } [self updateDrawerDraggingProgress:scrollView]; [self updateDimmingViewAlpha:scrollView]; } - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { if (scrollView != self.scrollView) { return; } JYPulleyStatus newStatus = [self newStatusFromCurrentStatus:self.currentStatus lastContentOffSet:self.lastContentOffSet scrollView:self.scrollView supportedStatus:self.supportedStatus]; [self updateStatus:newStatus animated:YES]; } - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset { if (scrollView != self.scrollView) { return; } self.lastContentOffSet = CGPointMake(targetContentOffset->x, targetContentOffset->y); *targetContentOffset = scrollView.contentOffset; } #pragma mark - JYPulleyScrollViewDelegate - (BOOL)shouldTouchPulleyScrollView:(JYPulleyScrollView *)scrollView point:(CGPoint)point { CGPoint convertPoint = [self.drawerContainerView convertPoint:point fromView:scrollView]; return !CGRectContainsPoint(self.drawerContainerView.bounds, convertPoint); } - (UIView *)viewToReceiveTouch:(JYPulleyScrollView *)scrollView point:(CGPoint)point { if (self.currentStatus == JYPulleyStatusExpand && self.dimmingView) { return self.dimmingView; } return self.contentContainerView; } #pragma mark - JYPulleyDrawerScrollViewDelegate - (void)drawerScrollViewDidScroll:(UIScrollView *)scrollView { if (scrollView.contentOffset.y <= 0) { self.drawerShouldScroll = YES; scrollView.scrollEnabled = NO; } else { self.drawerShouldScroll = NO; scrollView.scrollEnabled = YES; } } #pragma mark - UIGestureRecognizerDelegate - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer: (UIGestureRecognizer *)otherGestureRecognizer { return YES; } #pragma mark - public method /** 根据伸缩状态处理悬停动作 @param status 伸缩状态 */ - (void)updateStatus:(JYPulleyStatus)status animated:(BOOL)animated { if (!(self.supportedStatus & status)) { return; } CGFloat stopToMoveTo; CGFloat minimumHeight = [self drawerClosedHeight]; if (status == JYPulleyStatusClosed) { stopToMoveTo = minimumHeight; } else if (status == JYPulleyStatusPartiallyExpand) { stopToMoveTo = [self drawerPartiallyExpandHeight]; } else if (status == JYPulleyStatusExpand) { if ([self drawerExpandHeight] > 0) { stopToMoveTo = [self drawerExpandHeight]; } else { stopToMoveTo = self.scrollView.frame.size.height; } } else { stopToMoveTo = 0.0f; } self.currentStatus = status; if (animated) { [UIView animateWithDuration:0.3 delay:0.0 usingSpringWithDamping:0.75 initialSpringVelocity:0.0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ [self.scrollView setContentOffset:CGPointMake(0, stopToMoveTo - minimumHeight) animated:NO]; if (self.dimmingView) { self.dimmingView.frame = [self dimmingViewFrameForDrawerPosition:stopToMoveTo]; } } completion:nil]; } else { [self.scrollView setContentOffset:CGPointMake(0, stopToMoveTo - minimumHeight) animated:NO]; if (self.dimmingView) { self.dimmingView.frame = [self dimmingViewFrameForDrawerPosition:stopToMoveTo]; } } } #pragma mark - event response - (void)didRecognizedScrollViewPanGestureRecognizer:(UIPanGestureRecognizer *)gestureRecognizer { if (!self.drawerShouldScroll) { return; } if (gestureRecognizer.state == UIGestureRecognizerStateChanged) { CGPoint old = [gestureRecognizer translationInView:self.scrollView]; if (old.y < 0) { return; } CGPoint offSet = CGPointMake(0, self.scrollView.frame.size.height - old.y - [self drawerClosedHeight]); self.lastContentOffSet = offSet; self.scrollView.contentOffset = offSet; } else if (gestureRecognizer.state == UIGestureRecognizerStateEnded) { self.drawerShouldScroll = NO; JYPulleyStatus newStatus = [self newStatusFromCurrentStatus:self.currentStatus lastContentOffSet:self.lastContentOffSet scrollView:self.scrollView supportedStatus:self.supportedStatus]; [self updateStatus:newStatus animated:YES]; } } - (void)didRecognizedDimmingViewTapGestureRecognizer:(UITapGestureRecognizer *)gestureRecognizer { if (gestureRecognizer.state == UIGestureRecognizerStateEnded) { [self updateStatus:JYPulleyStatusClosed animated:YES]; } } #pragma mark - private method - (void)updateDrawerDraggingProgress:(UIScrollView *)scrollView { CGFloat drawerClosedHeight = [self drawerClosedHeight]; if ([self.drawerDelegate respondsToSelector:@selector(pulleyViewController:drawerDraggingProgress:)]) { CGFloat safeAreaTopInset; if (@available(iOS 11.0, *)) { safeAreaTopInset = self.view.safeAreaInsets.top; } else { safeAreaTopInset = self.topLayoutGuide.length; } CGFloat spaceToDrag = self.scrollView.bounds.size.height - safeAreaTopInset - drawerClosedHeight; CGFloat dragProgress = fabs(scrollView.contentOffset.y) / spaceToDrag; if (dragProgress - 1 > FLT_EPSILON) { //in case greater than 1 dragProgress = 1.0f; } // 保留两位小数 NSString *progress = [NSString stringWithFormat:@"%.2f", dragProgress]; [self.drawerDelegate pulleyViewController:self drawerDraggingProgress:progress.floatValue]; } } - (void)updateDimmingViewAlpha:(UIScrollView *)scrollView { CGFloat safeAreaBottomInset; CGFloat drawerClosedHeight = [self drawerClosedHeight]; if (@available(iOS 11.0, *)) { safeAreaBottomInset = self.view.safeAreaInsets.bottom; } else { safeAreaBottomInset = self.bottomLayoutGuide.length; } // 背景遮罩颜色变化 self.dimmingView.backgroundColor = PBColor(40, 58, 79); if ((scrollView.contentOffset.y - safeAreaBottomInset) > ([self drawerPartiallyExpandHeight] - drawerClosedHeight)) { CGFloat progress; CGFloat fullRevealHeight = self.scrollView.bounds.size.height; if (fullRevealHeight == [self drawerPartiallyExpandHeight]) { progress = 1.0; } else { progress = (scrollView.contentOffset.y - ([self drawerPartiallyExpandHeight] - drawerClosedHeight)) / (fullRevealHeight - [self drawerPartiallyExpandHeight]); } self.dimmingView.alpha = progress * self.dimmingOpacity * 0.5; self.dimmingView.userInteractionEnabled = YES; } else { if (self.dimmingView.alpha >= 0.01) { self.dimmingView.alpha = 0.0; self.dimmingView.userInteractionEnabled = NO; } } self.dimmingView.frame = [self dimmingViewFrameForDrawerPosition:scrollView.contentOffset.y + drawerClosedHeight]; } - (CGRect)dimmingViewFrameForDrawerPosition:(CGFloat)position { CGRect dimmingViewFrame = self.dimmingView.frame; dimmingViewFrame.origin.y = 0 - position; return dimmingViewFrame; } - (JYPulleyStatus)newStatusFromCurrentStatus:(JYPulleyStatus)currentStatus lastContentOffSet:(CGPoint)lastContentOffSet scrollView:(UIScrollView *)scrollView supportedStatus:(JYPulleyStatus)supportedStatus { NSMutableArray *drawerStops = [NSMutableArray array]; CGFloat currentDrawerStatusStop = 0.0f; if (supportedStatus & JYPulleyStatusClosed) { CGFloat collapsedHeight = [self drawerClosedHeight]; [drawerStops addObject:@(collapsedHeight)]; if (currentStatus == JYPulleyStatusClosed) { currentDrawerStatusStop = collapsedHeight; } } if (supportedStatus & JYPulleyStatusPartiallyExpand) { CGFloat partialHeight = [self drawerPartiallyExpandHeight]; [drawerStops addObject:@(partialHeight)]; if (currentStatus == JYPulleyStatusPartiallyExpand) { currentDrawerStatusStop = partialHeight; } } if (supportedStatus & JYPulleyStatusExpand) { CGFloat openHeight = scrollView.bounds.size.height; [drawerStops addObject:@(openHeight)]; if (currentStatus == JYPulleyStatusExpand) { currentDrawerStatusStop = openHeight; } } // 取最小值 CGFloat lowestStop = [[drawerStops valueForKeyPath:@"@min.floatValue"] floatValue]; CGFloat distanceFromBottomOfView = lowestStop + lastContentOffSet.y; CGFloat currentClosestStop = lowestStop; JYPulleyStatus cloestValidDrawerStatus = currentStatus; for (NSNumber *currentStop in drawerStops) { if (fabs(currentStop.floatValue - distanceFromBottomOfView) < fabs(currentClosestStop - distanceFromBottomOfView)) { currentClosestStop = currentStop.integerValue; } } if (fabs(currentClosestStop - (scrollView.frame.size.height)) <= FLT_EPSILON && supportedStatus & JYPulleyStatusExpand) { cloestValidDrawerStatus = JYPulleyStatusExpand; } else if (fabs(currentClosestStop - [self drawerClosedHeight]) <= FLT_EPSILON && supportedStatus & JYPulleyStatusClosed) { cloestValidDrawerStatus = JYPulleyStatusClosed; } else if (supportedStatus & JYPulleyStatusPartiallyExpand){ cloestValidDrawerStatus = JYPulleyStatusPartiallyExpand; } return cloestValidDrawerStatus; } #pragma mark - set up UI - (void)setupUI { self.view.backgroundColor = [UIColor whiteColor]; self.lastContentOffSet = CGPointZero; [self.scrollView addSubview:self.drawerContainerView]; [self.view addSubview:self.contentContainerView]; [self.view addSubview:self.scrollView]; [self setupDimmingView]; } - (void)setupDimmingView { self.dimmingView.alpha = 0.0; [self addTapGestureRecognizerToDimmingViewIfNeeded]; [self.view insertSubview:self.dimmingView aboveSubview:self.contentContainerView]; } // 用户可能创建了自己的 dimmingView,则 tap 手势也要重新添加上去 - (void)addTapGestureRecognizerToDimmingViewIfNeeded { if (!self.dimmingView) { return; } if (self.dimmingViewTapGestureRecognizer.view == self.dimmingView) { return; } if (!self.dimmingViewTapGestureRecognizer) { self.dimmingViewTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didRecognizedDimmingViewTapGestureRecognizer:)]; } [self.dimmingView addGestureRecognizer:self.dimmingViewTapGestureRecognizer]; // UIImageView 默认 userInteractionEnabled 为NO,为了兼容 UIImageView,这里必须主动设置为 YES // self.dimmingView.userInteractionEnabled = YES; } - (void)viewWillLayoutSubviews { [super viewWillLayoutSubviews]; [self.contentContainerView addSubview:self.contentDataSource.view]; [self.contentContainerView sendSubviewToBack:self.contentDataSource.view]; [self.drawerContainerView addSubview:self.drawerDataSource.view]; [self.drawerContainerView sendSubviewToBack:self.drawerDataSource.view]; self.contentContainerView.frame = self.view.bounds; CGFloat safeAreaTopInset; CGFloat safeAreaBottomInset; if (@available(iOS 11.0, *)) { safeAreaTopInset = self.view.safeAreaInsets.top; safeAreaBottomInset = self.view.safeAreaInsets.bottom; } else { safeAreaTopInset = self.topLayoutGuide.length; safeAreaBottomInset = self.bottomLayoutGuide.length; } if (@available(iOS 11.0, *)) { self.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentAlways; } else { self.automaticallyAdjustsScrollViewInsets = NO; self.scrollView.contentInset = UIEdgeInsetsMake(0, 0, self.bottomLayoutGuide.length, 0); } CGFloat minimumHeight = [self drawerClosedHeight]; if (self.supportedStatus & JYPulleyStatusExpand) { self.scrollView.frame = CGRectMake(0, self.drawerExpandTopInset + safeAreaTopInset, self.view.bounds.size.width, self.view.bounds.size.height - self.drawerExpandTopInset - safeAreaTopInset); } else { CGFloat adjustedTopInset = self.supportedStatus & JYPulleyStatusPartiallyExpand ? [self drawerPartiallyExpandHeight] : [self drawerClosedHeight]; self.scrollView.frame = CGRectMake(0, self.view.bounds.size.height - adjustedTopInset, self.view.bounds.size.width, adjustedTopInset); } self.drawerContainerView.frame = CGRectMake(0, self.scrollView.bounds.size.height - minimumHeight, self.scrollView.bounds.size.width, self.scrollView.bounds.size.height); self.scrollView.contentSize = CGSizeMake(self.scrollView.bounds.size.width, (self.scrollView.bounds.size.height - minimumHeight) + self.scrollView.bounds.size.height - safeAreaBottomInset); self.dimmingView.frame = CGRectMake(0.0, 0.0, self.view.bounds.size.width, self.view.bounds.size.height + self.scrollView.contentSize.height); [self updateStatus:self.currentStatus animated:NO]; } #pragma mark - setter - (void)setCurrentStatus:(JYPulleyStatus)currentStatus { _currentStatus = currentStatus; // 通知外部状态变化 if ([self.drawerDelegate respondsToSelector:@selector(pulleyViewController:didChangeStatus:)]) { [self.drawerDelegate pulleyViewController:self didChangeStatus:currentStatus]; } } - (void)setDimmingView:(UIView *)dimmingView { if (![self isViewLoaded]) { _dimmingView = dimmingView; } else { [self.view insertSubview:dimmingView belowSubview:_dimmingView]; [_dimmingView removeFromSuperview]; _dimmingView = dimmingView; [self.view setNeedsLayout]; } [self addTapGestureRecognizerToDimmingViewIfNeeded]; } #pragma mark - getter - (UIView *)contentContainerView { if (!_contentContainerView) { _contentContainerView = [[UIView alloc] initWithFrame:self.view.bounds]; _contentContainerView.backgroundColor = [UIColor clearColor]; } return _contentContainerView; } - (UIView *)drawerContainerView { if (!_drawerContainerView) { _drawerContainerView = [[UIView alloc] initWithFrame:self.view.bounds]; _drawerContainerView.backgroundColor = [UIColor clearColor]; } return _drawerContainerView; } - (JYPulleyScrollView *)scrollView { if (!_scrollView) { _scrollView = [[JYPulleyScrollView alloc] initWithFrame:self.drawerContainerView.bounds]; _scrollView.backgroundColor = [UIColor clearColor]; _scrollView.delegate = self; _scrollView.showsVerticalScrollIndicator = NO; _scrollView.showsHorizontalScrollIndicator = NO; _scrollView.bounces = NO; _scrollView.canCancelContentTouches = YES; _scrollView.decelerationRate = UIScrollViewDecelerationRateFast; _scrollView.touchDelegate = self; [_scrollView addGestureRecognizer:self.scrollViewPanGestureRecognizer]; } return _scrollView; } - (UIPanGestureRecognizer *)scrollViewPanGestureRecognizer { if (!_scrollViewPanGestureRecognizer) { _scrollViewPanGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(didRecognizedScrollViewPanGestureRecognizer:)]; _scrollViewPanGestureRecognizer.delegate = self; } return _scrollViewPanGestureRecognizer; } - (CGFloat)drawerClosedHeight { if ([self.drawerDataSource respondsToSelector:@selector(closedHeightInPulleyViewController:)]) { return [self.drawerDataSource closedHeightInPulleyViewController:self]; } return _drawerClosedHeight; } - (CGFloat)drawerPartiallyExpandHeight { if ([self.drawerDataSource respondsToSelector:@selector(partiallyExpandHeightInPulleyViewController:)]) { return [self.drawerDataSource partiallyExpandHeightInPulleyViewController:self]; } return _drawerPartiallyExpandHeight; } - (CGFloat)drawerExpandHeight { if ([self.drawerDataSource respondsToSelector:@selector(expandHeightInPulleyViewController:)]) { return [self.drawerDataSource expandHeightInPulleyViewController:self]; } return _drawerExpandHeight; } @end