当前位置: 首页 > news >正文

封装了一个iOS评论弹窗

封装了一个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;@endtypedef 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;@endNS_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 *)contentViewmaskView:(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

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 使用js代码模拟React页面中input文本框输入
  • YOLOv8实例分割+双目相机实现物体尺寸测量
  • LSI-9361阵列卡笔记
  • 手机谷歌浏览器怎么用
  • C/C++ 多线程[1]---线程创建+线程释放+实例
  • redis的RDB快照详解
  • C学习(数据结构)-->二叉树
  • SpringBoot依赖之Spring Data Redis 实现地理坐标(Geospatial)
  • 响应式Web设计:纯HTML和CSS的实现技巧-1
  • Java 入门指南:注解(Annotation)
  • Linux系统下的容器安全:深入解析与最佳实践
  • 《AI办公类工具PPT系列之三——Gamma APP》
  • appium下载及安装
  • .NET开源纪元:穿越封闭的迷雾,拥抱开放的星辰
  • vue3-基础
  • [译] 理解数组在 PHP 内部的实现(给PHP开发者的PHP源码-第四部分)
  • 【node学习】协程
  •  D - 粉碎叛乱F - 其他起义
  • Docker 1.12实践:Docker Service、Stack与分布式应用捆绑包
  • gops —— Go 程序诊断分析工具
  • HomeBrew常规使用教程
  • idea + plantuml 画流程图
  • JAVA之继承和多态
  • Kibana配置logstash,报表一体化
  • laravel with 查询列表限制条数
  • leetcode378. Kth Smallest Element in a Sorted Matrix
  • Promise面试题2实现异步串行执行
  • react 代码优化(一) ——事件处理
  • tweak 支持第三方库
  • vue总结
  • webpack+react项目初体验——记录我的webpack环境配置
  • WePY 在小程序性能调优上做出的探究
  • 分布式事物理论与实践
  • 分享一个自己写的基于canvas的原生js图片爆炸插件
  • 构造函数(constructor)与原型链(prototype)关系
  • 批量截取pdf文件
  • 试着探索高并发下的系统架构面貌
  • 适配iPhoneX、iPhoneXs、iPhoneXs Max、iPhoneXr 屏幕尺寸及安全区域
  • 无服务器化是企业 IT 架构的未来吗?
  • gunicorn工作原理
  • Spark2.4.0源码分析之WorldCount 默认shuffling并行度为200(九) ...
  • ​TypeScript都不会用,也敢说会前端?
  • ​第20课 在Android Native开发中加入新的C++类
  • ​业务双活的数据切换思路设计(下)
  • # MySQL server 层和存储引擎层是怎么交互数据的?
  • (01)ORB-SLAM2源码无死角解析-(66) BA优化(g2o)→闭环线程:Optimizer::GlobalBundleAdjustemnt→全局优化
  • (12)目标检测_SSD基于pytorch搭建代码
  • (libusb) usb口自动刷新
  • (已解决)报错:Could not load the Qt platform plugin “xcb“
  • (原創) 如何動態建立二維陣列(多維陣列)? (.NET) (C#)
  • (转)Java socket中关闭IO流后,发生什么事?(以关闭输出流为例) .
  • .h头文件 .lib动态链接库文件 .dll 动态链接库
  • .NET CF命令行调试器MDbg入门(二) 设备模拟器
  • .net mvc 获取url中controller和action
  • .NET 设计模式—适配器模式(Adapter Pattern)