封装了一个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