网易新闻系列之自定义底部Dock栏目选择器

作为系列第一篇文章,就先从最简单的底部的这个Dock栏开始。
其实说是最简单的,也是应用最广泛的。我们应该看过很多APP的导航界面的样式。基本上就以下两种:

  • 系统原生的UITabBar始终在页面底部,并不跟随界面跳转而隐藏和显示
    系统原生
  • 网易新闻类似的底部TabBar只在每个栏目的首页显示,在第二级或第三级页面隐藏(ps:请自动忽略新闻内容)


    网易新闻

那么首先,既然是自定义控件,就要尽量做到低耦合,高内聚。也就是说外部不用知道内部的具体实现原理,只用公开出一个接口供使用方调用即可。
在这里,外部告诉我要添加一个新的item,同时告诉我这个item的普通图片、选中图片以及显示文字。Dock内部会根据当前添加的item数量规范的调整显示每一个item的位置及状态。
在Dock.h文件中我有一个这个方法:
- (void)addItemWithIcon:(NSString *)icon selectedIcon:(NSString *)selectedIcon title:(NSString *)title;
内部实现为:

- (void)addItemWithIcon:(NSString *)icon selectedIcon:(NSString *)selectedIcon title:(NSString *)title {
    DockItem *dockItem = [[DockItem alloc]init];

    [dockItem setImage:[UIImage imageNamed:icon] forState:UIControlStateNormal];
    [dockItem setImage:[UIImage imageNamed:selectedIcon] forState:UIControlStateSelected];
    [dockItem setTitle:title forState:UIControlStateNormal];
    // 这里你也可以把正常状态和选中状态下的字体颜色开放出去在外部设置
    [dockItem setTitleColor:[UIColor colorWithRed:137/255.0f green:137/255.0f blue:137/255.0f alpha:1] forState:UIControlStateNormal];
    [dockItem setTitleColor:[UIColor colorWithRed:223/255.0f green:41/255.0f blue:43/255.0f alpha:1] forState:UIControlStateSelected];
    [dockItem addTarget:self action:@selector(itemClick:) forControlEvents:UIControlEventTouchDown];
    [self addSubview:dockItem];

    int count = (int)self.subviews.count;
    // Dock默认显示第一项
    if (count == 1) {
        [self itemClick:dockItem];
    }
    CGFloat width = self.frame.size.width / count;
    CGFloat height = self.frame.size.height;
    for (int i = 0; i < count; i++) {
        DockItem *item = self.subviews[i];
        item.tag = i;
        item.frame = CGRectMake(width * i, 0, width, height)
   }
}

在每一次添加新的item的同时,其内部自动的重新去设置每个item的位置以达到友好的显示。
其次,我们知道每一个item实是上是一个UIButton,所以要做到这个Button的显示跟系统的不一样,我重写了UIButton的两个方法以达到图片与文字上下显示的效果,给定一个图片与文字的高度的比例,我这里设置的kImageRatio = 0.7,代码如下:

#pragma mark 调整内部ImageView的frame
- (CGRect)imageRectForContentRect:(CGRect)contentRect
{
  CGFloat imageX = 0;
  CGFloat imageY = 0;
  CGFloat imageWidth = self.frame.size.width;
  CGFloat imageHeight = self.frame.size.height * kImageRatio;
  return CGRectMake(imageX, imageY, imageWidth, imageHeight);
}

#pragma mark 调整内部UILable的frame
- (CGRect)titleRectForContentRect:(CGRect)contentRect
{
  CGFloat titleWidth = self.frame.size.width;
  CGFloat titleHeight = self.frame.size.height * (1 - kImageRatio);
  CGFloat titleX = 0;
  CGFloat titleY = self.imageView.frame.size.height - 3;
  return CGRectMake(titleX, titleY, titleWidth, titleHeight);
}

至此,比如说我现在要给Dock栏添加一个新闻item,并把这个item需要的两张图片传进去,那么我只需要执行[self.dock addItemWithIcon:@"tabbar_icon_news_normal" selectedIcon:@"tabbar_icon_news_highlight" title:@"新闻"];就可以了。

接着我们就来介绍如何做到让底部的这个Dock栏随着控制器的切换而做到自动的跟随每一个需要它跟随的控制器。
在日常的开发中,相信大家基本上每个人都做过顶部标题下部菜单类似的APP。并且我相信更多的是第二种的显示方式,细心的人会发现这个Dock栏始终都会跟随每个栏目的第一个控制器,那么我们是不是可以这样考虑,假设说每一个栏目都是一个导航控制器,那么这个导航控制器下可能会push出N多个子控制器,但是这个导航控制器有且只会有一个根(root)控制器,也就是说我们的Dock栏只需要跟随这个导航控制器的根控制器即可。SO...在这里边其实要处理的无非就是底部Dock这个View。

那么先说一下我的思路:

  • 整个APP一般可能会有3-5个一级的界面,那么先搞一个MainController来管理这么多子页面
  • 一般情况下每个一级的界面都会有2-3个子页面,可见每个子页面都是有一个导航控制器包着的页面
  • 我们要实现的功能就是在每个导航控制器的根控制器显示在最前的时候,显示底部Dock栏,那么如果当前显示在最前的控制器不是导航控制器的根控制器时,就要隐藏Dock栏
  • 这样就很容易想到UINavigationController的代理方法,我们让MainController成为它的每一个子页面的导航控制器的代理,然后统一在代理方法里去做处理

我先在MainController里添加了五个子控制器

#pragma mark - Private Methods
- (void)addChildViewControllers {
  UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:FirstViewController.new];
  nav.delegate = self;
  [self addChildViewController:nav];
    
  UIViewController *vc = [[UIViewController alloc] init];
  nav = [[UINavigationController alloc] initWithRootViewController:vc];
  vc.view.backgroundColor = [UIColor redColor];
  nav.delegate = self;
  [self addChildViewController:nav];
    
  vc = [[UIViewController alloc] init];
  nav = [[UINavigationController alloc] initWithRootViewController:vc];
  vc.view.backgroundColor = [UIColor orangeColor];
  nav.delegate = self;
  [self addChildViewController:nav];
    
  vc = [[UIViewController alloc] init];
  nav = [[UINavigationController alloc] initWithRootViewController:vc];
  vc.view.backgroundColor = [UIColor blueColor];
  nav.delegate = self;
  [self addChildViewController:nav];
    
  vc = [[UIViewController alloc] init];
  nav = [[UINavigationController alloc] initWithRootViewController:vc];
  vc.view.backgroundColor = [UIColor grayColor];
  nav.delegate = self;
  [self addChildViewController:nav];
}

既然是MainController来管理所有的子栏目,那么很显然Dock应该属于MainController的View的子view。也就是说实事上在MainController上显示的每一个子导航控制器的frame的size的height并不是填充了整个屏幕。


MainController中区域分配

那么既然Dock栏并不属于某一个子栏目,我们要怎样使它随着任何一个导航控制器的根控制器来回移动呢,一个处理方法就是:当我们在UINavigationController的代理方法中来做处理。因为当前主控制器(Maincontroller)是所有子栏目导航控制器的代理,这样无论哪一个栏目中的控制器被点击,MainController都能及时的做出处理。
既然我们已经知道当前页面中的逻辑关系,那么接下来要做的就是监听每一次将要push或pop新控制器时,判断当前控制器是不是根控制器。
假如现在要展示一个新的控制器,如果它不是根控制器那么就要拉长当前导航控制器的height,并且此时将Dock从main上边移除,将其添加到root控制器上,这样在某一个导航控制器push出一个新的子页面时,Dock就会跟随root控制器一起被隐藏;同样如果它是根控制器那么就要把拉长的导航控制器的height变回初始高度,并且把Dock从根控制器上重新移回到main上。这里要注意的一点就是,一个操作是在新控制器将要展示之前就要做,另一个是在新控制器展示出来之后再做。至于哪一个是will哪一个是did,大家可以自己考虑一下,如果你真的不太理解可以去看一下这个项目demo的源码(地址在下面会给出)。

Dock栏的移动与隐藏基本上就这样,那么Dock栏既然充当的是一个UITabBar的作用,也就是说我们在选择某一个栏目的同时MainController应该及时的隐藏旧的栏目并显示新的栏目,然而这并不难办,使用代理很简单的就能实现这个功能,在Dock上的item接收到点击事件时改变自身显示的同时告诉自己的代理你现在要给我显示第几个子控制器就行了。至于代码我就不再在这贴了。
最后,我又给这个Dock添加了一个新的小功能,大家在上图应该也能看到第三个和第四个item右上角的一个mark小红点。提示用户某一个栏目中可能有新消息或是活动,添加新消息提示的mark点共有两种模式,一种是只显示有新消息标示而不显示具体数目,另一种是直接显示新消息的条数,那么具体使用哪种模式就看个人爱好了。


注:此文章首发在简书转载请说明出处。
如果你想看到完整的代码,可以去这里

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

推荐阅读更多精彩内容