赞
踩
封装了一个iOS类似抖音效果的评论弹窗,可以跟手滑动的效果
主要有下面两需要注意的点
因为我们的弹窗既要支持拖动整体上下滑动,还要支持内容列表的滑动
,所以,我们需要在内容视图中添加一个滑动的手势,以此来滑动弹窗
并且要支持同时响应,同时要注意,支持同时响应并不是同时滑动,
而是支持手指在列表中的时候,可以响应弹窗的手势
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
NSLog(@"shouldRecognizeSimultaneouslyWithGestureRecognizer: %@", gestureRecognizer);
if (gestureRecognizer == self.panGesture) {
if ([otherGestureRecognizer isEqual:self.scrollView.panGestureRecognizer]) {
return YES;
}
}
return NO;
}
这个很好理解,因为如果我们滑动弹窗的时候,仍然滑动列表,那样的用户体验就会很差,所以我们要处理的就是在我们滚动弹窗的时候,禁止列表的滚动
.h
// // LBSlidePopView.h // TEXT // // Created by mac on 2024/7/28. // Copyright © 2024 刘博. All rights reserved. // #import <UIKit/UIKit.h> NS_ASSUME_NONNULL_BEGIN @protocol LBSlidePopViewDelegate <NSObject> - (void)slideDismiss:(NSString *)source; @optional - (void)didPanGesture:(UIPanGestureRecognizer *)panGesture; @end typedef NS_ENUM(NSInteger, LBSlideViewExpand) { LBSlideViewExpandNone = 0, // 无 LBSlideViewExpandMin = 1, LBSlideViewExpandMid = 2, LBSlideViewExpandMax = 3 }; @protocol LBPopSlideFrameDelegate <NSObject> - (void)frameChanged:(CGRect)frame percent:(CGFloat)percent isMax:(BOOL)isMax inAnimation:(BOOL)inAnimation finished:(BOOL)finished; - (void)frameChangedInAnimationBlock:(CGRect)frame percent:(CGFloat)percent isMax:(BOOL)isMax; - (void)expandStatusChanged:(LBSlideViewExpand)status lastStatus:(LBSlideViewExpand)lastStatus; // Offer区块 - (BOOL)shouldCollopseOfferView; - (BOOL)shouldExpandOfferView; - (void)updateOfferViewFrame:(CGFloat)offset; - (void)collopseOrExpandOfferView; @end @interface LBSlidePopView : UIView @property (nonatomic, weak) id<LBPopSlideFrameDelegate> frameDelegate; @property (nonatomic, weak) UIView *contentView; // 主内容 @property (nonatomic, strong) UITapGestureRecognizer *tapGesture; @property (nonatomic, strong) UIPanGestureRecognizer *panGesture; @property (nonatomic, strong) UILongPressGestureRecognizer *longPressGesture; @property (nonatomic, weak) UIScrollView *scrollView; @property (nonatomic, assign) CGFloat beginContentOffsetY; @property (nonatomic, assign) BOOL isDragScrollView; @property (nonatomic, assign) CGFloat lastTransitionY; @property (nonatomic, assign) CGFloat beginY; @property (nonatomic, assign) BOOL hasImpactFeedback; // 是否已经震动过 @property (nonatomic, assign) CGFloat slideHPercent; // 横向滚动的阈值:用来判断是否横向滚动还是竖向滚动 ///允许横向滑动消失,默认NO @property (nonatomic, assign) BOOL horizontalPanDismiss; @property (nonatomic, weak) id<LBSlidePopViewDelegate> delegate; @property (nonatomic, assign) BOOL disableGesture; // 是否禁用手势 //原来的内容的高度 @property (nonatomic, assign) CGFloat contentOriginHeight; - (instancetype)initWithFrame:(CGRect)frame contentView:(UIView *)contentView maskView:(nullable UIView *)maskView delegate:(id<LBSlidePopViewDelegate>)delegate; - (void)updateContentFrame:(CGFloat)offset; - (void)dismiss:(NSString *)source; - (void)updateForSingleView; @end NS_ASSUME_NONNULL_END
.m
// // LBSlidePopView.m // TEXT // // Created by mac on 2024/7/28. // #import "LBSlidePopView.h" #import "LBFunctionTestHeader.h" @interface LBSlidePopView ()<UIGestureRecognizerDelegate> @property (nonatomic, weak) UIView *maskView; @end @implementation LBSlidePopView - (instancetype)initWithFrame:(CGRect)frame contentView:(UIView *)contentView maskView:(UIView *)maskView delegate:(id<LBSlidePopViewDelegate>)delegate { if (self = [super initWithFrame:frame]) { self.backgroundColor = [UIColor clearColor]; self.delegate = delegate; self.maskView = maskView; self.contentView = contentView; [self addSubview:self.maskView]; [self addSubview:self.contentView]; // 横滑幅度较大,幅度可开关控制,默认是60°夹角 self.slideHPercent = 0.58; // 添加手势 [self addGestureRecognizer:self.tapGesture]; [self addGestureRecognizer:self.panGesture]; } return self; } - (void)updateForSingleView { self.zf_height = self.contentView.zf_height; self.contentView.zf_top = 0; [self.maskView removeFromSuperview]; } - (void)dealloc { NSLog(@"##** LIVSlidePopupView dealloc"); } - (void)setDisableGesture:(BOOL)disableGesture { if (_disableGesture != disableGesture) { _disableGesture = disableGesture; if (disableGesture) { [self removeGestureRecognizer:self.tapGesture]; [self removeGestureRecognizer:self.panGesture]; } else { [self addGestureRecognizer:self.tapGesture]; [self addGestureRecognizer:self.panGesture]; } } } - (void)showWithCompletion:(void (^)(void))completion { [UIView animateWithDuration:0.25f animations:^{ self.contentView.zf_bottom = self.zf_height; } completion:^(BOOL finished) { !completion ? : completion(); [self frameChanged:NO]; }]; } - (void)dismiss:(NSString *)source { NSLog(@""); if (self.delegate && [self.delegate respondsToSelector:@selector(slideDismiss:)]) { [self.delegate slideDismiss:source]; } [self frameChanged:YES]; } #pragma mark - UIGestureRecognizerDelegate - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { NSLog(@"gestureRecognizer shouldReceiveTouch: %@", gestureRecognizer); if (gestureRecognizer == self.panGesture) { UIView *touchView = touch.view; while (touchView != nil) { NSLog(@"%@", touchView); if ([touchView isKindOfClass:NSClassFromString(@"EmotionBoardScrollView")] || [touchView isKindOfClass:[UITextView class]]) { // 滑动Emoji键盘、输入框时不处理 self.isDragScrollView = NO; return NO; } if (touchView == self.scrollView) { self.isDragScrollView = YES; break; } else if (touchView == self.contentView) { self.isDragScrollView = NO; break; } touchView = (UIView *)[touchView nextResponder]; } } return YES; } - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { NSLog(@"gestureRecognizerShouldBegin: %@", gestureRecognizer); if (gestureRecognizer == self.tapGesture) { CGPoint point = [gestureRecognizer locationInView:self.contentView]; if ([self.contentView.layer containsPoint:point] && gestureRecognizer.view == self) { return NO; } } else if (gestureRecognizer == self.panGesture) { CGPoint translation = [self.panGesture translationInView:self.contentView]; if (!self.horizontalPanDismiss) { // 不处理禁用横滑的手势 if (ABS(translation.y) == 0) { // 完全横滑 return NO; } else if ((ABS(translation.x) / ABS(translation.y)) > self.slideHPercent) { // 横滑幅度较大,幅度可开关控制,默认是60°夹角 return NO; } } self.beginY = self.zf_top; } self.hasImpactFeedback = NO; return YES; } //是否支持多手势触发,返回YES,则可以多个手势一起触发方法,返回NO则为互斥 //是否允许多个手势识别器共同识别,一个控件的手势识别后是否阻断手势识别继续向下传播,默认返回NO; //如果为YES,响应者链上层对象触发手势识别后,如果下层对象也添加了手势并成功识别也会继续执行,否则上层对象识别后则不再继续传播 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { NSLog(@"shouldRecognizeSimultaneouslyWithGestureRecognizer: %@", gestureRecognizer); if (gestureRecognizer == self.panGesture) { if ([otherGestureRecognizer isEqual:self.scrollView.panGestureRecognizer]) { return YES; } } return NO; } 这个方法返回YES,第一个手势和第二个互斥时,第一个会失效 //- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { // return NO; //} // 这个方法返回YES,第一个和第二个互斥时,第二个会失效 //- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { // return NO; //} #pragma mark - HandleGesture - (void)handleTapGesture:(UITapGestureRecognizer *)tapGesture { CGPoint point = [tapGesture locationInView:self.contentView]; if (![self.contentView.layer containsPoint:point] && tapGesture.view == self) { [self dismiss:@"blankArea"]; } } - (void)handlePanGesture:(UIPanGestureRecognizer *)panGesture { CGPoint translationPoint = [panGesture translationInView:self.contentView]; CGFloat translationOffset = translationPoint.y; BOOL isHorizontal = NO; if ((ABS(translationPoint.y) == 0 || (ABS(translationPoint.x) / ABS(translationPoint.y)) > self.slideHPercent) && self.horizontalPanDismiss) { translationOffset = translationPoint.x; // 根据contentOffset变化判断scrollView是否有滚动过,scrollView没有滚动过才能支持横向滑动关闭面板 isHorizontal = ABS(self.beginContentOffsetY - self.scrollView.contentOffset.y) < 0.5; } if (self.isDragScrollView) { // 当UIScrollView在最顶部时,处理视图的滑动 if (self.scrollView.contentOffset.y <= 0 || isHorizontal) { if (translationOffset > 0) { // 向右、向下拖拽 // self.scrollView.contentOffset = CGPointZero; self.scrollView.panGestureRecognizer.enabled = NO; self.isDragScrollView = NO; [self updateContentFrame:translationOffset]; } } } else { [self updateContentFrame:translationOffset]; } [panGesture setTranslation:CGPointZero inView:self.contentView]; if (panGesture.state == UIGestureRecognizerStateBegan) { // 手势开始时记录contentOffset初始位置 self.beginContentOffsetY = self.scrollView.contentOffset.y; } else if (panGesture.state == UIGestureRecognizerStateEnded) { CGPoint velocityPoint = [panGesture velocityInView:self.contentView]; self.scrollView.panGestureRecognizer.enabled = YES; // (结束时的速度>0 滑动距离> 5) 或 绝对移动距离超过150 且UIScrollView滑动到最顶部 CGFloat velocityOffset = velocityPoint.y; if ((ABS(velocityPoint.y) == 0 || (ABS(velocityPoint.x) / ABS(velocityPoint.y)) > self.slideHPercent) && self.horizontalPanDismiss) { velocityOffset = velocityPoint.x; } if (((velocityOffset > 0 && self.lastTransitionY > 5) || (self.contentView.zf_bottom - self.zf_height) > 150) && !self.isDragScrollView) { [self dismiss:@"swipeDown"]; } else { [self showWithCompletion:nil]; } } self.lastTransitionY = translationOffset; if (self.delegate && [self.delegate respondsToSelector:@selector(didPanGesture:)]) { [self.delegate didPanGesture:panGesture]; } } // 更新内容区域的frame - (void)updateContentFrame:(CGFloat)offset { CGFloat topY = (self.zf_height - self.contentView.zf_height); self.contentView.zf_top = MAX(topY, self.contentView.zf_top + offset); // 下拉时蒙层透明度变化 CGFloat rate = 1.f; if (self.contentView.zf_height > 0 && self.contentView.zf_bottom > self.zf_height) { rate = MIN(1.f, (self.zf_height - self.contentView.zf_top) / self.contentView.zf_height); rate = MAX(0.f, rate); } self.maskView.alpha = 0.55 * rate; [self frameChanged:NO]; } - (void)frameChanged:(BOOL)dismiss { if (self.frameDelegate && [self.frameDelegate respondsToSelector:@selector(frameChanged:percent:isMax:inAnimation:finished:)]) { CGRect frame = CGRectZero; if (!dismiss) { frame.size.height = self.contentOriginHeight - self.contentView.zf_top; } [self.frameDelegate frameChanged:frame percent:!dismiss isMax:!dismiss inAnimation:YES finished:dismiss]; } } #pragma mark - 懒加载 - (UITapGestureRecognizer *)tapGesture { if (!_tapGesture) { _tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)]; _tapGesture.delegate = self; } return _tapGesture; } - (UIPanGestureRecognizer *)panGesture { if (!_panGesture) { _panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanGesture:)]; _panGesture.delegate = self; } return _panGesture; } @end
弹窗中的代码
// // LBSlidePopViewController.m // TEXT // // Created by mac on 2024/8/18. // Copyright © 2024 刘博. All rights reserved. // #import "LBSlidePopViewController.h" #import "LBSlidePopView.h" #import "LBFunctionTestHeader.h" #import "LBview.h" @interface LBSlidePopViewController () <UITableViewDelegate, UITableViewDataSource, LBSlidePopViewDelegate> @property (nonatomic, strong) UITableView *tableView; @property (nonatomic, strong) LBSlidePopView *slideView; @end @implementation LBSlidePopViewController - (void)viewDidLoad { [super viewDidLoad]; [self setUpUI]; [self.tableView reloadData]; // Do any additional setup after loading the view. } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; [self.navigationController setNavigationBarHidden:true animated:animated]; } - (void)setUpUI { self.view.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.4]; CGFloat top = SCREEN_HEIGHT - 400; self.viewContent = [[LBview alloc] initWithFrame:CGRectMake(0, top, SCREEN_WIDTH, 400)]; self.viewContent.backgroundColor = [UIColor purpleColor]; self.view.backgroundColor = [UIColor whiteColor]; //半透明背景 self.viewDismiss = [[UIView alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)]; self.viewDismiss.alpha = 0.0; self.slideView = [[LBSlidePopView alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT) contentView:self.viewContent maskView:self.viewDismiss delegate:self]; self.view = self.slideView; [self.slideView addGestureRecognizer:self.slideView.tapGesture]; self.view.backgroundColor = [UIColor clearColor]; self.view.window.backgroundColor = [UIColor clearColor]; [self.viewContent addSubview:self.tableView]; self.slideView.scrollView = self.tableView; } #pragma mark - UITableViewDelegate, UITableViewDataSource - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return 100; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { return 40; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"kkk"]; return cell; } #pragma mark - LBSlidePopViewDelegate - (void)slideDismiss:(NSString *)source { [self dismissViewControllerAnimated:YES completion:nil]; } #pragma mark - lazy load - (UITableView *)tableView { if (!_tableView) { _tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, 400) style:UITableViewStylePlain]; [_tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"kkk"]; _tableView.delegate = self; _tableView.dataSource = self; } return _tableView; } /* #pragma mark - Navigation // In a storyboard-based application, you will often want to do a little preparation before navigation - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { // Get the new view controller using [segue destinationViewController]. // Pass the selected object to the new view controller. } */ @end
代码
链接: link
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。