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];
            }
        }
    }
}

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

查看所有标签

猜你喜欢:

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

机器学习基础教程

机器学习基础教程

(英)Simon Rogers,、Mark Girolami / 郭茂祖、王春宇 刘扬 刘晓燕、刘扬、刘晓燕 / 机械工业出版社 / 2014-1 / 45.00

本书是一本机器学习入门教程,包含了数学和统计学的核心技术,用于帮助理解一些常用的机器学习算法。书中展示的算法涵盖了机器学习的各个重要领域:分类、聚类和投影。本书对一小部分算法进行了详细描述和推导,而不是简单地将大量算法罗列出来。 本书通过大量的MATLAB/Octave脚本将算法和概念由抽象的等式转化为解决实际问题的工具,利用它们读者可以重新绘制书中的插图,并研究如何改变模型说明和参数取值。......一起来看看 《机器学习基础教程》 这本书的介绍吧!

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

SHA 加密
SHA 加密

SHA 加密工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具