美文网首页iOS常用
UIScrollView和UITableView的混合使用

UIScrollView和UITableView的混合使用

作者: 数字d | 来源:发表于2021-08-21 15:57 被阅读0次

    需求效果:

    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可以避免这个问题.

    相关文章

      网友评论

        本文标题:UIScrollView和UITableView的混合使用

        本文链接:https://www.haomeiwen.com/subject/pqifiltx.html