内容简介:把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经典布局——圣杯布局
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。