UIScrollView和UITableView的混合使用

数字dIP属地: 海南
0.45字数 1,085

需求效果:

2j.gif

demo拉取地址:demo

最简单的实现方式是,放一个tableview,这个tableview有一个headView,这个headview就是上图所示的蓝色头部的View,但是这样做的结果是,在tableView下拉刷新数据的时候,刷新动画会出现在headView的上方,这样看着就异常令人难受了,当时为了节省时间,就是这么实现的.现在有充足的时间的情况下,是不允许有瑕疵的.

于是就有了UIScrollView + UITableView的组合实现方案;

组合的方案实现遇到的问题

UIScrollView和UITableView的组合使用问题整理:
1.手势冲突
2.tableview和scrollview一起滑动
3.scrollview滑动到底部之后,tableview上拉没反应
4.scrollview滑出了指定的头部区域之后下拉没反应
5.tableview的下拉刷新无效

a.手势事件的穿透

为解决手势冲突问题,自定义一个ScrollView,ArtScrollView,并将滑动手势的响应传递到最下层的scrollview,
返回YES,则可以多个手势一起触发方法,返回NO则为互斥(比如外层UIScrollView名为mainScroll内嵌的UIScrollView名为subScroll,当我们拖动subScroll时,mainScroll是不会响应手势的(多个手势默认是互斥的),当下面这个代理返回YES时,subScroll和mainScroll就能同时响应手势,同时滚动,这符合我们这里的需求)

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
    return YES;
}

viewController中的所有代码,这里可以忽略,demo中有全部的实现

#define SCREEN_WIDTH ([[UIScreen mainScreen] bounds].size.width)
#define SCREEN_HEIGHT ([[UIScreen mainScreen] bounds].size.height)

#import "ViewController.h"

//#import "RCDraggableButton.h"

//#import "YZDraggeMoveView.h"

//#import "YZClearUIView.h"

#import "Masonry.h"

//#import "SDWebImage.h"

#import "Toast.h"

#import "ArtScrollView.h"

#import "MJRefresh.h"



@interface ViewController ()<UIScrollViewDelegate,UITableViewDataSource,UITableViewDelegate>

@property(nonatomic,assign)CGFloat redHeight;
@property(nonatomic,assign)CGFloat blueHeight;
@property(nonatomic,strong)UIView * redView;
@property(nonatomic,strong)UIView * blueView;
@property(nonatomic,strong)ArtScrollView * scrollView;
@property(nonatomic,strong)UIScrollView * scrollInnerView;
@property(nonatomic,strong)NSMutableArray * array;
@property(nonatomic,strong)UITableView * tableView;

@property (nonatomic, assign) BOOL vccanScroll;   // 这里的布尔值类似一个锁,初始化的默认值是YES,当用户拖拽了tableview背后的scrollview并且拖拽到了scrollview的偏移距离大于blueview的时候vccanScroll值为NO,锁住了scrollview,不让scrollview进行偏移,不管往上滑动还是往下滑动,并将scrollview的偏移量改为blueview.height.当且仅当tableView.offset.y < 0的时候,也就是tableView被进行了下拉操作的时候,这种情况下说明tableview已经进入到了最顶端的位置,这时候,可以对scrollview进行滑动解锁,也就是把vccanScroll的值再改为YES,这种情况下可以将scrollview可以正常上下滑动了

@end



@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _vccanScroll = YES;
    self.view.backgroundColor = [UIColor whiteColor];
    self.redHeight = 180;
    self.blueHeight = 200;
    self.array = [NSMutableArray arrayWithCapacity:0];
    
    for (int i = 0 ; i < 30; i ++) {
        [self.array addObject:[NSString stringWithFormat:@"need + %d",i]];
    }
    
    [self.view addSubview:self.redView];
    
    [self.view addSubview:self.scrollView];
    self.scrollView.backgroundColor = [UIColor purpleColor];

}


-(UIView *)redView {
    if (_redView == nil) {
        _redView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH  , _redHeight)];
        _redView.backgroundColor = [UIColor redColor];
    }
    return _redView;
}

-(UIView *)blueView {
    if (_blueView == nil) {
        _blueView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, _blueHeight)];
        _blueView.backgroundColor = [UIColor blueColor];
        UIButton * button = [UIButton buttonWithType:(UIButtonTypeCustom)];
        button.backgroundColor = [UIColor whiteColor];
        [button setTitle:@"change blueHeight" forState:(UIControlStateNormal)];
        [button setTitleColor:[UIColor redColor] forState:(UIControlStateNormal)];
        [button addTarget:self action:@selector(changeBlueValue) forControlEvents:(UIControlEventTouchUpInside)];
     //   [button mas_makeConstraints:^(MASConstraintMaker *make) {
       //     make.centerX.equalTo(_blueView.mas_centerX);
         //   make.centerY.equalTo(_blueView.mas_centerY);
           // make.width.equalTo(@200);
           // make.height.equalTo(@50);
        //}];
          button.frame = CGRectMake(0, 0, 200, 50);
        [_blueView addSubview:button];
    }
    return _blueView;
}

-(void)changeBlueValue {

    if (self.blueHeight == 240) {
        self.blueHeight = 200;
    }else
    {
        self.blueHeight = 240;
    }
    
    [self changeBindingFrame];
}

-(void)changeBindingFrame {
    _scrollView.contentSize = CGSizeMake(SCREEN_WIDTH, (SCREEN_HEIGHT - _redHeight) + _blueHeight);
    _scrollInnerView.frame = CGRectMake(0, 0, SCREEN_WIDTH, (SCREEN_HEIGHT - _redHeight) + _blueHeight);
    _scrollInnerView.contentSize = CGSizeMake(SCREEN_WIDTH, (SCREEN_HEIGHT - _redHeight) + _blueHeight);
    self.blueView.frame = CGRectMake(0, 0, SCREEN_WIDTH, _blueHeight);
    self.tableView.frame = CGRectMake(0, _blueHeight, SCREEN_WIDTH, SCREEN_HEIGHT - _redHeight);
}


-(ArtScrollView *)scrollView {
    if(_scrollView == nil){
        _scrollView = [[ArtScrollView alloc] initWithFrame:CGRectMake(0, _redHeight, SCREEN_WIDTH, (SCREEN_HEIGHT - _redHeight))];
        _scrollView.contentSize = CGSizeMake(SCREEN_WIDTH, (SCREEN_HEIGHT - _redHeight) + _blueHeight);
        _scrollView.backgroundColor = [UIColor grayColor];
        _scrollView.showsVerticalScrollIndicator = NO;
        _scrollView.delegate = self;
        _scrollView.bounces = NO;
        
        [_scrollView addSubview:self.scrollInnerView];
        
    }
    return _scrollView;
}

-(UIScrollView *)scrollInnerView {
    if (_scrollInnerView == nil) {
        _scrollInnerView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, (SCREEN_HEIGHT - _redHeight) + _blueHeight)];
        _scrollInnerView.contentSize = CGSizeMake(SCREEN_WIDTH, (SCREEN_HEIGHT - _redHeight) + _blueHeight);
        _scrollInnerView.showsVerticalScrollIndicator = NO;
        _scrollInnerView.delegate = self;
        _scrollInnerView.backgroundColor = [UIColor yellowColor];
        
        [_scrollInnerView addSubview:self.blueView];
        [_scrollInnerView addSubview:self.tableView];
    }
    return _scrollInnerView;
}

#pragma mark --------- tableView

- (UITableView *)tableView {
    if (_tableView == nil) {
        _tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, _blueHeight, SCREEN_WIDTH, SCREEN_HEIGHT - _redHeight) style:(UITableViewStylePlain)];
        _tableView.delegate = self;
        _tableView.dataSource = self;
        _tableView.rowHeight = 55;
        MJRefreshNormalHeader *header = [MJRefreshNormalHeader headerWithRefreshingTarget:self refreshingAction:@selector(loadData)];
        header.lastUpdatedTimeLabel.hidden = YES;
        _tableView.mj_header = header;
        _tableView.mj_footer = [MJRefreshAutoNormalFooter footerWithRefreshingBlock:^{
             [self loadDataMore];
         }];
        _tableView.showsVerticalScrollIndicator = NO;
    }
    
    return _tableView;
}

-(void)loadData {
    [self.tableView.mj_footer endRefreshing];
    [self.tableView.mj_header endRefreshing];
    [self.view makeToast:@"下拉刷新了一次" duration:1 position:CSToastPositionCenter style:[[CSToastStyle alloc] initWithDefaultStyle]];

}

-(void)loadDataMore {
    [self.tableView.mj_footer endRefreshing];
    [self.tableView.mj_header endRefreshing];
    [self.view makeToast:@"上拉加载了一次" duration:1 position:CSToastPositionCenter style:[[CSToastStyle alloc] initWithDefaultStyle]];
}



- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell * cell = [[UITableViewCell alloc] init];
    cell.textLabel.text = self.array[indexPath.row];
    return cell;
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.array.count;
}



- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    
    CGFloat offsetY = scrollView.contentOffset.y;

    if (scrollView == self.scrollView) {
        
        CGFloat maxOffsetY = _blueHeight;
       if (offsetY >= maxOffsetY) {
            scrollView.contentOffset = CGPointMake(0, maxOffsetY);
            _vccanScroll = NO;
        }else {
            if (_vccanScroll == NO) {
                scrollView.contentOffset = CGPointMake(0, maxOffsetY);
            }
        }
    }else if(scrollView == self.tableView){
        CGPoint point = [scrollView.panGestureRecognizer translationInView:scrollView];
        CGFloat taboffsetY = point.y;
        if (offsetY < 0) {
            _vccanScroll = YES;
        }
           if (taboffsetY < 0) {
               if(self.scrollView.contentOffset.y < _blueHeight){
                   self.tableView.contentOffset = CGPointZero;
               }
           } else {
               if (offsetY > 0) {
                   self.scrollView.contentOffset = CGPointMake(0, _blueHeight);
               }else if (offsetY < 0){
                   if (self.scrollView.contentOffset.y > 0 && self.scrollView.contentOffset.y < _blueHeight) {
                       self.tableView.contentOffset = CGPointZero;
                   }
               }
              
           }
        
    }
}

b.scrollView的滑动监听

这里需要对scrollView的滑动位置做监听,不但需要监听scrollview的contentOffset.y值,还需要监听tableView的contentOffset.y 同时需要做一些处理,因为UITableView继承自UIScrollView,所以在tableView设置delegate时候,同样能够监听到tableView的滑动位置和状态

UIKIT_EXTERN API_AVAILABLE(ios(2.0)) @interface UITableView : UIScrollView <NSCoding, UIDataSourceTranslating>

具体的监听方法是scrollViewDidScroll:(UIScrollView *)scrollView,如果tableView和scrollView在同一个控制器中,可以简单的用
scrollView == self.tableView 和scrollView == self.scrollView来区分

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    CGFloat offsetY = scrollView.contentOffset.y;
}

c.tableview的上下拉操作的监听

苹果提供了一个很好用的方法来监听scrollVIew的手势操作,这里可以很方便的判断出,当前用户对tableView的操作是上滑还是下滑,便于处理对应的临界值情况的效果

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
     CGPoint point = [scrollView.panGestureRecognizer translationInView:scrollView];
        CGFloat offsetY = point.y;
        if (offsetY < 0) {
            /// 上滑
            
        } else {
            /// 下滑
           
        }
}

d.底部scrollView的实际可滑动状态记录

这里的布尔值类似一个锁,初始化的默认值是YES,当用户拖拽了tableview背后的scrollview并且拖拽到了scrollview的偏移距离大于blueview.height的时候vccanScroll值为NO,锁住了scrollview,不让scrollview进行偏移,不管往上滑动还是往下滑动,并将scrollview的偏移量改为blueview.height.当且仅当tableView.offset.y < 0的时候,也就是tableView被进行了下拉操作的时候,这种情况下说明tableview已经进入到了最顶端的位置,这时候,可以对scrollview进行滑动解锁,也就是把vccanScroll的值再改为YES,这种情况下可以将scrollview可以正常上下滑动了

@property (nonatomic, assign) BOOL vccanScroll;

网上搜索的答案都会稍微有点瑕疵,最后总结之后完善了一下,结果见上面的gif

流程监听tableView的上下滑中参考:
监听tableview滑动

                                     ----------------- 真是嚼一路辛苦,饮一路汗水💦

\color{red}{20210823更新:}

因为之前demo中viewDidLoad中使用的blueHeight初始化的值200.00没有问题
但是在实际接入中因为blueHeight的初始化值是根据一个label的高度动态计算出来之后在viewDidLoad中赋值,我这里的blueHeight计算的打印值是225.027这样的数据,会导致scrollview上的blueView上滑出去之后tableView不会向上滑动的bug

排查原因:
虽然blueHeight是225.027.但是在实际scrollview的- (void)scrollViewDidScroll:(UIScrollView *)scrollView 滑动监听方法中最大滑动距离scrollView.contentOffset.y只能打印到225.00000,这种情况不知道是不是scrollview自身的bug
于是我将初始化的值写成定值225.000,不能下滑的异常情况就消失了

于是我的解决方案,在动态计算完成blueHeight之后,将计算的最终值进行取整之后再赋值给blueHeight可以避免这个问题.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
1人点赞
总资产28共写了8.4W字获得278个赞共39个粉丝

推荐阅读更多精彩内容