仿写知乎日报 - 主页面(Part 1)

转自我自己的 blog

失业后发现自己的项目经验太少了,除了公司的 App 和自己的游戏之外就几乎为零了,所以必须要增加自己的实战经验。之前写 UI 都是纯代码,为了熟悉 Storyboard,特地选择了知乎日报来练手。

在之前这个仿知乎日报的 iOS App 已经很出名了,我也是借鉴(就是抄)了部分的实现,图片和 API 则是完全照搬。当然我也有我自己的不同之处,我的 UI 是尽可能采用 Storyboard+xib 来实现,另外也在一些细节上更贴近正版知乎日报。

这篇主要讲一下首页的实现,以下是动图。

首页结构

首页主要由以下几部分组成:

  1. 顶部的图片轮播
  2. 下面的 TableView
  3. 顶部的其他小东西:展开侧边栏的按钮,刷新控件,「今日新闻」的标题,和一个随着 TableView 上滑出现的 View

上滑效果

先说一下 TableView 的实现。首先自定义一个 UITableViewCell,按照原版的把大小和位置设定好,这个不复杂,如下图:

接下来弄 TableView,这个 TableView 是和父视图同样大小的,也就是充满屏幕(注意,TableView 的父视图不是 HomeViewController 的 UIView,而是其下的 Subview,轮播视图以及其他控件都是放在这个 View 中的,至于为什么不直接放在 HomeViewController 的 View 里面,下一篇讲侧边栏实现的时候再解释……)。

在视觉上,第一感觉这个 TableView 好像应该是放在轮播图片的下面的(也就是 TableView 的 top 贴着轮播图片的 bottom),最开始我也是这样做的。
但是后来做上滑效果的时候才发现这样不行,因为上滑的时候需要 Cell 和轮播图片同时向上移动,这样 TableView 的 origin 就会改变,contentOffset 就不好计算了,而轮播图片的移动全靠这个 offset 来决定。

我也试过将 TableView 的初始 contentOffset 设为轮播图片的下面,但是滑上去就下不来了……所以,最后的解决办法是将 TableView 铺满屏幕,上面加一个和轮播图片同样高度的 Header,完美!

上面说过,上滑效果全靠 TableView 的 contentOffset 来实现。HomeViewController 要实现 UIScrollViewDelegate 中的 scrollViewDidScroll: 这个方法。 在这个方法里面,加入以下代码:

CGFloat offsetY = scrollView.contentOffset.y;
if (offsetY > 0) {
    if (!self.topView) {
        self.topView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, kScreenWidth, 64)];
        [self.homeView insertSubview:self.topView belowSubview:self.showSideMenuButton];
    }
    CGFloat alpha = offsetY / 64;
    self.topView.backgroundColor = [UIColor colorWithRed:60.f / 255.f
                                                   green:198.f / 255.f
                                                    blue:253.f / 255.f
                                                   alpha:alpha];
    self.carouselViewTop.constant = -offsetY;

}

这段代码做了这些:

  1. 首先判断 contentOffset.y,如果大于零那就是在向上滑动。
  2. 如果 topView 不存在的话,就新生成一个,并且 topView 的背景颜色随着滑动距离变化。
  3. 最后设置轮播图片距离父视图 top 的约束,这个变量是从 Storyboard 中拖过来的,将这个约束设为 -offsetY 就可以实现轮播图片和 Cell 一起向上滑动的效果了。

还需要注意的是,展示侧边栏的按钮,还有刷新控件和「今日新闻」的 UILabel,必须在层级上高于这个 topView,不然就会被 topView 盖住。

有一个小细节的地方,困扰了我好久,就是 TableView 的第一个 Cell 和上面的轮播图片始终有一段距离。最后各种尝试和搜索后才找到解决方法:在 Storyboard 中选中 HomeViewController,在 Attributes Inspector 中把 Adjust Scroll View Insets 这个选项勾掉。

图片轮播

这部分在实现思路上基本完全借鉴了上面提及的那个仿作,整个控件的容器是一个 UIScrollView,里面并排摆放所有的图片,还有一个 UIPageControl 来显示对应的索引。

自定义一个 BannerView,用来显示每一个轮播的图片以及标题。上面的容器里装的就是这个 View。

重要的轮播逻辑是这样的:通过 API 获取的轮播个数是5个,但是容器中的 View 是7个(5+2)。这一排的 BannerView 按照序号是这样排列的,5-1-2-3-4-5-1,也就是把第一个和最后一个复制一份添加到数组的尾和头。
而 ScrollView 的初始 offset 是数组的第二个(也就是序号为1的)。这样,1在右划的时候会在左面显示5,5在左划的时候会显示1。如果 ScrollView 的 contentOffset 停留在数组的第一个(5),那么就把 contentOffset 设为数组的第6个(正确顺序的5)。同理,如果 ScrollView 的 contentOffset 停留在数组的最后一个(1),那么就把 contentOffset 设为数组的第2个(正确顺序的1)。这样就实现了一个可以无限循环的轮播。

相关代码如下:

-(void)scrollViewDidScroll:(UIScrollView *)scrollView {
    CGFloat offsetX = scrollView.contentOffset.x;
    if (offsetX ==  6 * kScreenWidth) {
        _scrollView.contentOffset = CGPointMake(kScreenWidth, 0);
        _pageControl.currentPage = 0;
    } else if (offsetX == 0) {
        _scrollView.contentOffset = CGPointMake(5 * kScreenWidth, 0);
        _pageControl.currentPage = 4;
    } else {
        _pageControl.currentPage = offsetX/kScreenWidth - 1;
    }
}

具体运行时的效果如下:

刷新动画

这里有两部分内容,一是刷新控件的实现,二是刷新控件的控制。

刷新控件的是由两部分组成的,一个 UIActivityIndicatorView 和由 CAShapeLayer 绘制的圆环。
定义一个 RefreshView,初始化中加入以下代码:

- (void)customInit {
    _indicatorView = [[UIActivityIndicatorView alloc]initWithFrame:self.bounds];

    _grayCircleShapeLayer = [CAShapeLayer layer];
    _grayCircleShapeLayer.lineWidth = 2.f;
    _grayCircleShapeLayer.strokeColor = [UIColor grayColor].CGColor;
    _grayCircleShapeLayer.fillColor = [UIColor clearColor].CGColor;
    _grayCircleShapeLayer.opacity = 0;
    _grayCircleShapeLayer.path = [UIBezierPath bezierPathWithOvalInRect:self.bounds].CGPath;

    _whiteCircleShapeLayer = [CAShapeLayer layer];
    _whiteCircleShapeLayer.lineWidth = 2.f;
    _whiteCircleShapeLayer.strokeColor = [UIColor whiteColor].CGColor;
    _whiteCircleShapeLayer.fillColor = [UIColor clearColor].CGColor;
    _whiteCircleShapeLayer.opacity = 0;
    _whiteCircleShapeLayer.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.width/2, self.width/2)
                                                                 radius:self.width/2
                                                             startAngle:M_PI_2 endAngle:M_PI * 5 / 2
                                                              clockwise:YES].CGPath;
    _whiteCircleShapeLayer.strokeEnd = 0;

    [self addSubview:_indicatorView];
    [self.layer addSublayer:_grayCircleShapeLayer];
    [self.layer addSublayer:_whiteCircleShapeLayer];
}

圆环由一个灰色的背景圆环和一个表示进度的白色圆弧组成,下拉过程中更新白色圆弧的长度,到指定位置后,整个圆环消失,开始 UIActivityIndicatorView 的动画。
更新圆环进度的代码如下:

-(void)updateProgress:(CGFloat)progress {
    if (progress <= 0) {
        _whiteCircleShapeLayer.opacity = 0;
        _grayCircleShapeLayer.opacity = 0;
    } else {
        _whiteCircleShapeLayer.opacity = 1;
        _grayCircleShapeLayer.opacity = 1;
    }

    if (progress > 1) {
        progress = 1;
    }
    _whiteCircleShapeLayer.strokeEnd = progress;
}

对刷新控件的控制其实和上滑的控制一样,也在 HomeViewController 中的 scrollViewDidScroll: 中,这部分逻辑就是 offsetY < 0 的那一部分。

self.carouselViewHeight.constant = 220 - offsetY;
if (offsetY <= -kRefreshOffsetY * 1.5) {
   self.tableView.contentOffset = CGPointMake(0, -kRefreshOffsetY * 1.5);
} else if (offsetY <= 0 && offsetY >= -kRefreshOffsetY * 1.5) {
   if (self.isRefreshing) {
       [self.refreshView updateProgress:0];
   } else {
       [self.refreshView updateProgress:-offsetY / kRefreshOffsetY];
   }
}
if (offsetY < -kRefreshOffsetY && !scrollView.isDragging) {
   [self.refreshView startAnimation];
   self.isRefreshing = YES;
   dispatch_after(
                  dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)),
                  dispatch_get_main_queue(), ^{
                      [self.refreshView stopAnimation];
                      self.isRefreshing = NO;
                  });
}

这段代码的逻辑有:

  1. 下拉时增加轮播图片的高度。
  2. TableView 不是无限下拉的,只能下拉到一个指定的位置,超过的话,TableView 就不再下滑了。
  3. 下拉一段后再上滑,如果进入了刷新状态,不显示圆环;如果没进入刷新状态,那么就根据 下拉距离/下拉阈值 来更新圆环进度。
  4. 如果下拉距离达到了阈值并且松手了(没有拖动),那么就进入刷新状态。我这里做了个2秒刷新时间。

遗留

  1. 目前这个主页只做了展示,点击没有任何效果。
  2. TableView 滑上再滑下的时候,topView 不会完全消失,可能会有淡淡地残留,这点还没有优化。
  3. 原版的轮播图片底部和顶部有黑色阴影的渐变,这样在纯白的图片下,按钮和文字标题都可以清晰显示出来,这点我也没做。

另外,代码请戳 github

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

推荐阅读更多精彩内容