iOS支持横滑的多tab布局

栏目: IOS · 发布时间: 5年前

内容简介:把18年做的事情整理一下,这次是个人中心布局的架构实现UI大写开头的,UIScrollView这种是指本次要做的是拥有公共头部,支持两个tab横滑的页面,其实就是抖音的个人中心

把18年做的事情整理一下,这次是个人中心布局的架构实现

简介

UI大写开头的,UIScrollView这种是指
小写开头的,scrollView这种是指 实现

本次要做的是拥有公共头部,支持两个tab横滑的页面,其实就是抖音的个人中心

支持手势横滑的tab是用UIScrollView完成的,UIScrollView是在固定宽高的窗口内可以上下左右滑动的视图,用来承载屏幕上显示那些在有限区域内放不下的内容

正常的UIScrollView内每个pagingView都是独立的个体,上滑滚动时页面内所有元素跟随滚动

比如这样:

iOS支持横滑的多tab布局

可以看到,UIScrollView内放置了两个和手机屏幕等宽的页面,可以横滑。每个页面都是单独的内容流,可以上滑显示更多内容。注意,顶部的搜索和导航tab是放在UIScrollView外面的,可以看到并没有跟随页面上滑。

本次解决的问题是,每个页面是相对独立的,横滑的时候是页面为单位的横滑,如何保持同一块视图不跟随横滑,并且又能跟随页面上下滚动

实现效果(demo做了简化,没有演示吸顶和点击tab):

iOS支持横滑的多tab布局

具体思路

这里的实现方式是,把共享视图单独做一个UIView,盖在UIScrollView上面,这样UIScrollView横滑时,共享视图保持不动(共享视图命名为headerView)

随之而来是三个需要解决的问题:

  • headerView不会随着页面上下滚动
  • 手指在headerView上滑动的时候,scrollView不会接收手势事件进行滑动
  • 手指在headerView的subview(子集UIView)上滑动的时候,scrollView不会接收手势事件进行滑动

下面我们一个一个的解决这些问题:

让headerView随着页面上下滚动

先讲一下层级关系,根据包含层级,从底层到上层:UIViewController > UIScrollView > pagingView(单独创建的页面UIView) > UICollectionView(流布局)

headerView和scrollView的父级都是viewController,但是headerView盖在scrollView上面。scrollView里面包含了两个pagingView,横滑切换就是两个pagingView位置的切换

实现方式是在pagingView内实现UIScrollView的这个delegate:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView;

接收到pagingView在上下滑动的通知后,修改headerView的位置

代码一共涉及四个文件:

GXFMineViewController : UIViewController

GXFScrollView : UIScrollView

GXFMinePagingView : UIView

GXFMineHeaderAreaView : UIView

- (void)scrollViewDidScrollWithY {
    if (self.isPagingScrolling) {
        return;
    }
    
    GXFMinePagingView *currentCell = [_pagingView cellAtIndex:self.pagingCurrentIndex];
    CGFloat offsetY = currentCell.collectionView.contentOffset.y - 64 - 20;
    // 这里的210是headerView的高度,这里是demo简单写死了
    CGFloat headerOffsetY = 210;
    if (offsetY <= headerOffsetY) {
        headerOffsetY = offsetY;
    }
    
    CGRect frame = self.headerAreaView.frame;
    frame.origin.y = -headerOffsetY;
    self.headerAreaView.frame = frame;
}

这样就可以实现headerView随着页面上下滚动了,但要注意scrollView横滑的时候要及时修正headerView的位置,因为两个pagingView的滚动值contentOffset.y是不同的

因此,也需要在viewController内实现UIScrollView的这个delegate,不过上次处理的是纵向的滑动,这次处理的是横向的滑动:

-(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{
    self.pagingCurrentIndex = _pagingView.currentCellIndex;
}

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    self.isPagingScrolling = YES;
    
    GXFMinePagingView *currentCell = [_pagingView cellAtIndex:self.pagingCurrentIndex];
    if(currentCell && _pagingView.currentCellIndex == self.pagingCurrentIndex){
       GXFMinePagingView * cell = [_pagingView cellAtIndex:(self.pagingCurrentIndex+1)%2];
        CGFloat offsetY = currentCell.collectionView.contentOffset.y;
        CGFloat contentH = cell.collectionView.contentSize.height;

        // 从页面A滑动到页面B时,如果B的真实高度比较小,为了避免显示空内容,页面B纵向滚动到吸顶位置
        offsetY = (offsetY >= contentH) ? 210 : offsetY;

        if(cell){
            [cell.collectionView setContentOffset:CGPointMake(0, offsetY) animated:NO];
        }
    }
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    self.pagingCurrentIndex = _pagingView.currentCellIndex;
    self.isPagingScrolling = NO;
}

接收headerView的滑动手势事件

一次触摸事件是由一组UITouch对象状态变化引起的一组Touch message的转发和派送,然后UIResponser类用来接收和处理事件,常见的UIResponser有UIView及子类,UIViController,AppDelegate,UIApplication等等

用户点击之后会从第一个UIWindow对象开始,先判断UIWindow是否合格,其次判断点击位置在不在这个Window内,如果不在,返回nil,就换下一个UIWindow;如果在的话,并且UIWindow没有subView就返回自己,整个过程结束。如果UIWindow有subViews,就从后往前遍历整个subViews,做和UIWindow类似的事情,直到找到一个View。如果没有找到就不做传递

显示控件有两个方法来做上面这件事,就是常说的hitTest:

 // 先判断点是否在View内部,然后遍历subViews
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;  
//判断点是否在这个View内部
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;

我们初始化headerView的时候,传递pagingView的collectionView进去:

- (GXFMineHeaderAreaView *)headerAreaView {
    if (_headerAreaView == nil) {
        _headerAreaView = [[GXFMineHeaderAreaView alloc] initWithFrame:CGRectMake(20, 64 + 20, self.view.frame.size.width - 40, 210)];
        _headerAreaView.backgroundColor = [UIColor lightGrayColor];
        _headerAreaView.delegate = self;
        
        __weak typeof(self)weakSelf = self;
        _headerAreaView.getRespondView = ^UIView *{
            if (weakSelf == nil) {
                return nil ;
            }
            GXFMinePagingView * cell = [weakSelf.pagingView cellAtIndex:weakSelf.pagingView.currentCellIndex];
            if (cell) {
                return cell.collectionView;
            }
            return weakSelf.headerAreaView;
        };
    }
    return _headerAreaView;
}

然后重写headerView的hitTest方法,将headerView的手势事件转移给pagingView的collectionView,在headerView上滑动就相当于在pagingView的collectionView上滑动

- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    UIView *respondView = nil;
    
    if ((_headerView && _headerView.superview && CGRectContainsPoint(_headerView.frame, point))) {
        NSArray *subviews = self.headerView.subviews;
        for (UIView *subview in subviews) {
            // 这里判断headerView上的UIButton类型子视图依旧响应手势
            // 不然,按钮不识别手势就无法处理点击事件了
            // 也就是说按钮这块滑动,不会响应scrollView的滑动
            if ([subview isKindOfClass:[UIButton class]]) {
                
                CGPoint newPoint = [subview convertPoint:point fromView:self.headerView];
                
                if (CGRectContainsPoint(subview.bounds, newPoint)) {
                    respondView = [super hitTest:point withEvent:event];
                }
            }
        }
        if(respondView == nil){
            if(self.getRespondView){
                respondView = self.getRespondView();
            }
        }
    }
    if(respondView == nil){
        respondView = [super hitTest:point withEvent:event];
    }
    
    return respondView;
}

headerView上子视图的滑动手势事件

因为按钮一定要响应手势事件的,所以不能使用上面的解决方案。于是实现了headerView的手势事件,根据手势事件对象的点击位置,对前后移动位置计算差值,模拟上下滑动效果

headerView.m

- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        self.currentPointForPan = CGPointZero;
        [self addSubview: self.headerView];
        
        // 添加手势事件
        _verticalPanGesture = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(onPanGesture:)];
        _verticalPanGesture.delegate = self;
        [self addGestureRecognizer:_verticalPanGesture];
    }
    return self;
}

// 手势delegate,满足一定条件才允许手势状态更改
-(BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
	// 如果手势识别为拖动
    if([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) {
        UIPanGestureRecognizer *panGesture = (UIPanGestureRecognizer*)gestureRecognizer;
        CGPoint velocityPoint = [panGesture velocityInView:self.superview];
        // 如果手指运动是上下滑动
        if(fabs(velocityPoint.y) > fabs(velocityPoint.x)) {
            return YES;
        }
    }
    return NO;
}
// 实现手势事件
-(void)onPanGesture:(UIPanGestureRecognizer*)gesture {
    CGPoint locationPoint = [gesture locationInView:self.superview];
    CGPoint velocityPoint =  [gesture velocityInView:self.superview];
    
    if(!CGPointEqualToPoint(self.currentPointForPan, CGPointZero) &&
       (gesture.state == UIGestureRecognizerStateEnded || gesture.state == UIGestureRecognizerStateCancelled)) {
        
        CGFloat offsetY = locationPoint.y - self.currentPointForPan.y;
        if(self.delegate &&
           [self.delegate conformsToProtocol:@protocol(GXFMineHeaderViewAreaDelegate)] &&
           [self.delegate respondsToSelector:@selector(onDraftedHeaderAreaViewWithOffset:)]){
            
            [self.delegate onDraftedHeaderAreaViewWithOffset:CGPointMake(0, offsetY)];
        }
        self.currentPointForPan = CGPointZero;
    }
    
    if(fabs(velocityPoint.y) > fabs(velocityPoint.x)){
        switch (gesture.state) {
            case UIGestureRecognizerStateBegan:
            {
                self.currentPointForPan = locationPoint;
            }
                break;
            case UIGestureRecognizerStateChanged:
            {
                CGFloat offsetY = locationPoint.y - self.currentPointForPan.y;
                if(self.delegate &&
                   [self.delegate conformsToProtocol:@protocol(GXFMineHeaderViewAreaDelegate)] &&
                   [self.delegate respondsToSelector:@selector(onDraftingHeaderAreaViewWithOffset:)]){
                    
                    [self.delegate onDraftingHeaderAreaViewWithOffset:CGPointMake(0, offsetY)];
                }
                self.currentPointForPan = locationPoint;
            }
                break;
            case UIGestureRecognizerStateCancelled:
            case UIGestureRecognizerStateEnded:
            {
            }
            default:
                break;
        }
    }
}

GXFMineViewController.m

-(void)onDraftingHeaderAreaViewWithOffset:(CGPoint)offset{
    GXFMinePagingView * cell = [self.pagingView cellAtIndex:self.pagingView.currentCellIndex];
    if(cell){
        CGPoint currentOffset = cell.collectionView.contentOffset;
        currentOffset.x += offset.x;
        currentOffset.y += (-offset.y);
        if(currentOffset.y > 0){
            [cell.collectionView setContentOffset:currentOffset animated:NO];
        }
    }
}

-(void)onDraftedHeaderAreaViewWithOffset:(CGPoint)offset{
    GXFMinePagingView * cell = [self.pagingView cellAtIndex:self.pagingView.currentCellIndex];
    if(cell){
        CGPoint currentOffset = cell.collectionView.contentOffset;
        currentOffset.x += offset.x;
        currentOffset.y += (-offset.y);
        CGFloat maxOffset = fabs(cell.collectionView.contentSize.height - cell.collectionView.bounds.size.height);
        if(currentOffset.y < 0){
            [cell.collectionView setContentOffset:CGPointMake(0, 0) animated:YES];
        }
        else{
            if(ceil(currentOffset.y) <= maxOffset && cell.collectionView.contentSize.height > cell.collectionView.bounds.size.height){
                [cell.collectionView setContentOffset:currentOffset animated:NO];
            }
            else{
                CGFloat offsetY = 0.f;
                if(cell.collectionView.contentSize.height > cell.collectionView.bounds.size.height){
                    offsetY = maxOffset;
                }
                [cell.collectionView setContentOffset:CGPointMake(0, offsetY) animated:YES];
            }
        }
    }
}

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Learning Vue.js 2

Learning Vue.js 2

Olga Filipova / Packt Publishing / 2017-1-5 / USD 41.99

About This Book Learn how to propagate DOM changes across the website without writing extensive jQuery callbacks code.Learn how to achieve reactivity and easily compose views with Vue.js and unders......一起来看看 《Learning Vue.js 2》 这本书的介绍吧!

URL 编码/解码
URL 编码/解码

URL 编码/解码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换