iOS 仿微博、美团、饿了么,UITableView或UICollectionView混合公用HeaderView的布局

你是否需要实现一个这种UITableView或UICollectionView(也可以是仅有其中一类)混合公用HeaderView的界面呢?大致效果如下方Demo动态图的效果:


LMComposeViewDemoGif.gif

这种界面和交互在目前的很多主流的APP里都有使用。比如曾经的简书的个人页面就用到了类似的交互方式,饿了么,微博,爱奇艺,美团等等 都用了这种类似的界面。我基本上把这些类似的交互都实现了一遍,最终使用了一种最容易理解,也是最好使用的方式,从项目内脱敏拿了出来写了一个Demo。下面我们就通过这个Demo来讲解一下如何一行代码实现类似上面Gif展示的交互的界面。使用Demo内的LMComposeView类一行代码就能完成上面这类交互的界面。

Demo地址:

LMComposeView Demo GitHub地址

实现方法筛选

第1种解决方案

最初遇到这类需求的时候,是多个列表展示不同的Cell样式,不需要支持左右滑动切换,那么最简单的方式就是只1个UITableView来做,把不同的Cell都注册给这个UITabelView,一个HeaderView,然后切换不同的数据源,缓存每个不同列表的数据,和展示不同的Cell。但是好景不长的,这种效果一定不能维持太久,因为用户体验也不是特别好。所以我们进入2个解决方案

第2种解决方案

那么要支持横向左右滚动的话,就需要横向摆放多个UITableView或者UICollectionView了。但是这两者本身是无法直接公用一个HeaderView的。然后我第一反应就是饿了么的商家点单页面,也是这么一个类似的交互。然后找到了饿了么公开的实现该界面的文章:饿了么移动组实现该交互的原理介绍
大致原理就是饿了么的大大们用UIKit Dynamics模仿了UIScrollView的交互方式,重写了很多UIScrollView的种种特性,类似物理弹簧效果之类的,都进行了模仿,具体的原理和需要“打怪升级”的地方上面的文章里面有详细说明。笔者使用上面的原理模仿着写了写,始终写不出来特别好的物理效果,而且还有很多交互的问题。应该是我有些地方写的有问题导致的,所以第2种方案是看上去很美系列,想要挑战自我的同学或者需求不是很紧的同学可以尝试一下,可以让自己对Dynamics有更深的理解,能最终实现的话是超酷的。由于我并没有太长的时间,所以我把饿了么的实现方案给先放下了。

第3种解决方案

然后我又想到了微博的发现页面,就是这个类似的交互:


微博发现页面.gif

但是你仔细看一下你就对比出来了,微博的发现页面还有有些跟这个交互不太一样的地方,在向下滑动到中部的分类区域(视频,头条,榜单,北京这个分类位置)的时候,导航栏固定了,分类区域在上方固定了位置无法移动了,再想回到初始的状态只能点击左侧的返回按钮,界面回到顶部。而且还有个细节,如果你不松手从上向下滚动,中部的分类悬停之后,你的滚动手势会被中断掉,如果你想继续向上滚动,你需要手指离开屏幕,然后再次接触屏幕一次。如果用这种交互方式来实现我们本篇内容要讲的这种交互,在用户体验上始终不是特别流畅。微博应该是在这个界面有意为之,因为微博在个人主页使用的也是本文要讲的这种交互方式。其实如果达到发现页面的这种交互,就是把底部的横向滑动的ScrollView在到达分类选择区域的位置时候,传递给了另外一个控制器。这种方式在我们的有些界面也使用过,单不作为本文的讲解内容。这种方式其实并不如本文要讲的这种交互界面用户体验好。根据需求不同可能要做不同的选择。

第4种解决方案

在这些方案都尝试过之后,始终是在实现方式和用户体验上都有不如人意的地方。先分析一下这类界面的统一特点:
1.公用HeaderView:这个用UIKit本身给的Api是不可能实现的,要是像饿了么的实现方式成本有点高,但是我们可以从位置摆放上给用户一种公用HeaderView的错觉。
2.横向滚动:一定是要有一个横向的UIScrollView在最下层盛装着N个竖向滚动的UIScrollView(UITableView和UICollectionView都继承自UIScrollView)。
3.HeaderView都要跟着竖直方向滚动:我们可以监听竖直方向的UIScrollView 的offset来让HeaderView跟着动,来实现这个效果。
根据上面几个特点,我们可以实现一下这种架构图:


LMComposeView绘图结构图.jpeg

运行起来之后,在Xcode的结构查看里面 是这个样子的:


LMComposeView Xcode.jpeg

根据Demo里面的代码来介绍一下用法

先声明一下:如果你对UICollectionView的要求比较高,需要多个Section的UICollectionView,LMComposeView目前只适用于一个Section的UICollectionView。不过原理是一样的,如果你需要支持多个Section的UICollectionView,你可以用本文的方法进行自定义。
你需要用到的其实只有LMComposeView这一个类,UIView+LMViewHelper是一个属性分类为了方便设置frame,LMSegmentView是临时写的分类选择区域的自定义View,如果你对分类选择的定制要求比较高,你可以重写一下这个类,来实现自定义分类选择界面。

在使用LMComposeView的时候只要一行代码就能搞定:

#import "LMComposeView.h"
@interface DemoController ()<LMComposeViewDelegate>
@property(nonatomic,strong) LMComposeView * composeView;
@end
-(LMComposeView *)composeView{
    if (!_composeView) {
        _composeView = [[LMComposeView alloc]init];
        _composeView.delegate = self;
        [self.view addSubview:_composeView];
    }
    return _composeView;
}
//LMComposeViewDelegate 返回当前选中的是第几个分类列表
-(void)composeViewDidClickSegementButtonWithIndex:(NSInteger)index{

    NSLog(@"---滚动到了%ld---",(long)index);
}

- (void)viewDidLoad {
    [super viewDidLoad];
//在初始化界面的时候 调用该方法
 [self.composeView confirmComposeViewWithScrollViewArray:scrollViewArray withSegmentButtonTitleArray:titleArray withHeaderView:self.headerView withComposeViewFrame:CGRectMake(0, 64,self.view.width, self.view.height-64)];
}

主要逻辑和代码都在LMComposeView的confirmUI方法内:

-(void)confirmUI{
    __weak typeof(self) weakSelf = self;
    
    [self.scrollViewArray enumerateObjectsUsingBlock:^(UIScrollView * scrollView, NSUInteger idx, BOOL * _Nonnull stop) {
       
        scrollView.tag = 9000+idx;
        scrollView.frame = CGRectMake(SCREEN_WIDTH*idx, 0, weakSelf.width, weakSelf.height);
        [weakSelf.backScrollView addSubview:scrollView];
        
        if ([scrollView isKindOfClass:[UITableView class]]) {
            UITableView * tableView = (UITableView *)scrollView;
            if (tableView.tableHeaderView) {
                UIView * headerView = tableView.tableHeaderView;
                headerView.frame = (CGRect){0, 0, SCREEN_WIDTH, HEAD_HEIGHT};
                tableView.tableHeaderView = headerView;
            }else{
                UIView *headerView = [[UIView alloc] initWithFrame:(CGRect){0, 0, SCREEN_WIDTH, HEAD_HEIGHT}];
                tableView.tableHeaderView = headerView;
            }
        }else if ([scrollView isKindOfClass:[UICollectionView class]]){
            UICollectionView * collectionView = (UICollectionView *)scrollView;
            [collectionView.collectionViewLayout setValue:[NSValue valueWithUIEdgeInsets:[weakSelf getFixCollectionViewLayoutInsetWithInsetString:[NSString stringWithFormat:@"%@",[collectionView.collectionViewLayout valueForKey:@"sectionInset"]]]] forKey:@"sectionInset"];
        }
        
        [scrollView addObserver:weakSelf forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionInitial context:nil];
    }];
}

对UITableView和UICollectionView进行判断

UITableView:默认给这个tableview添加一个跟HeaderView等高的headerView
UICollectionView:由于UICollectionView添加HeaderView的方式跟UITableView不一样,我曾经一度想要动态的给collectionView添加一些代理方法以达到添加HeaderView的效果,不过最终我想到另外一种方式,那就是给collectionView默认的sectionInset的top增加跟HeaderView的高度一样的限制,这样也可以达到一样的效果。不过就是如果你需要UICollectionView有不同的section的话,你需要订制一下collectionView,原理是一样的。

监听每个UITableView和UICollectionView

用KVO的方式给UITableView和UICollectionView添加监听,监听它们的contentOffset,在回调方法里面做一个统一处理,让其他的scollview的offset跟最大的offset一致。并且让顶部的HeaderView跟着一起移动。这样就达到了公用HeaderView的假象

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    if ([object tag]%9000!=self.self.currentIndex) return;
    if ([keyPath isEqualToString:@"contentOffset"]) {
        UIScrollView *scrollView = object;
        CGFloat contentOffsetY = scrollView.contentOffset.y;
        // 如果滑动没有超过临界值
        if (contentOffsetY < self.headerView.height) {
            // 让这几个个tableView的偏移量相等
            for (UIScrollView * allscrollView in self.scrollViewArray) {
                if (allscrollView.contentOffset.y != scrollView.contentOffset.y) {
                    allscrollView.contentOffset = scrollView.contentOffset;
                }
            }
            //动态修改y值
            self.headerView.y = -contentOffsetY;
            // 一旦大于等于临界值点了,让headerView的y值等于临界值点,就停留在上边了
            self.segmentView.y = self.headerView.height-contentOffsetY;
            
        }
        else if (contentOffsetY >= self.headerView.height) {
            self.headerView.y = -self.headerView.height;
            self.segmentView.y = 0;
            
        }
        
        [self reloadMaxOffsetY];
    }
}

出现的问题

触摸顶部的HeaderView区域不能竖直方向让列表滚动

原因很简单,因为顶部的HeaderView盖住了竖直方向的ScrollView所以对应的touch事件都被屏蔽了。这里就要用到UIKit的HitTest机制,对自定义的HeaderView重写HitTest方法。如果你对HitTest的原理不是很了解,推荐你看一下这篇文章,可以让你更了解UIKit的事件响应机制:iOS事件处理之Hit-Testing

//当touch的pints在视图的子视图时,返回子视图,否则将事件透传到下面的视图
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    UIView *hitTestView = [super hitTest:point withEvent:event];
    if (hitTestView == self) {
        hitTestView = nil;
    }
    return hitTestView;
}

简单的说就是,对可以HeaderView内进行点击或者触摸之后,如果你触发的是这个HeaderView本身,则将响应事件渗透下去,这样渗透的话自然就渗透到了当前的ScrollView。如果不是HeaderView本身,那么就是HeaderView的子视图,那么就让子视图响应就可以了。

如果有任何问题,可以留言,会尽快帮你解决。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容

  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    X先生_未知数的X阅读 15,960评论 3 119
  • 莫言 学呗教育 ️️ 电话:69910209 1967年冬天,我12岁那年,临近春节的一个早晨,母亲苦着脸,...
    寒寒_恰同学少年阅读 189评论 0 0
  • 今日话题:如果给三年前的自己打电话,你想说什么? 我只想说:别做珠宝别开店,速度在关山大道买房! 自从开始筹备开店...
    时光蜜糖阅读 506评论 0 50