美文网首页iOS开发(OC)iOS技术点iOS技术相关
iOS 仿微博、美团、饿了么,UITableView或UIC

iOS 仿微博、美团、饿了么,UITableView或UIC

作者: Leesim | 来源:发表于2018-08-13 15:50 被阅读568次

    你是否需要实现一个这种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的子视图,那么就让子视图响应就可以了。

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

    相关文章

      网友评论

      • ChanJaWe:你好,我想对每个子视图做刷新操作,是不是要禁用父视图的bounce属性?我应该怎么修改呢?
      • AceThink:@code
        AceThink:@Ace00000000

      本文标题:iOS 仿微博、美团、饿了么,UITableView或UIC

      本文链接:https://www.haomeiwen.com/subject/vpdubftx.html