内容简介:把18年做的事情整理一下,这次是个人中心布局的架构实现UI大写开头的,UIScrollView这种是指本次要做的是拥有公共头部,支持两个tab横滑的页面,其实就是抖音的个人中心
把18年做的事情整理一下,这次是个人中心布局的架构实现
简介
UI大写开头的,UIScrollView这种是指 类
小写开头的,scrollView这种是指 实现
本次要做的是拥有公共头部,支持两个tab横滑的页面,其实就是抖音的个人中心
支持手势横滑的tab是用UIScrollView完成的,UIScrollView是在固定宽高的窗口内可以上下左右滑动的视图,用来承载屏幕上显示那些在有限区域内放不下的内容
正常的UIScrollView内每个pagingView都是独立的个体,上滑滚动时页面内所有元素跟随滚动
比如这样:
可以看到,UIScrollView内放置了两个和手机屏幕等宽的页面,可以横滑。每个页面都是单独的内容流,可以上滑显示更多内容。注意,顶部的搜索和导航tab是放在UIScrollView外面的,可以看到并没有跟随页面上滑。
本次解决的问题是,每个页面是相对独立的,横滑的时候是页面为单位的横滑,如何保持同一块视图不跟随横滑,并且又能跟随页面上下滚动
实现效果(demo做了简化,没有演示吸顶和点击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]; } } } }
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 最简单的代码,让 WPF 支持响应式布局
- Visual Studio Code 1.25支持新的网格布局和大纲视图
- css经典布局系列三——三列布局(圣杯布局、双飞翼布局)
- 四种方法实现──三栏布局(圣杯布局、双飞翼布局)
- 浅谈CSS三栏布局(包括双飞翼布局和圣杯布局)
- css经典布局——圣杯布局
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
机器学习基础教程
(英)Simon Rogers,、Mark Girolami / 郭茂祖、王春宇 刘扬 刘晓燕、刘扬、刘晓燕 / 机械工业出版社 / 2014-1 / 45.00
本书是一本机器学习入门教程,包含了数学和统计学的核心技术,用于帮助理解一些常用的机器学习算法。书中展示的算法涵盖了机器学习的各个重要领域:分类、聚类和投影。本书对一小部分算法进行了详细描述和推导,而不是简单地将大量算法罗列出来。 本书通过大量的MATLAB/Octave脚本将算法和概念由抽象的等式转化为解决实际问题的工具,利用它们读者可以重新绘制书中的插图,并研究如何改变模型说明和参数取值。......一起来看看 《机器学习基础教程》 这本书的介绍吧!