失业后发现自己的项目经验太少了,除了公司的 App 和自己的游戏之外就几乎为零了,所以必须要增加自己的实战经验。之前写 UI 都是纯代码,为了熟悉 Storyboard,特地选择了知乎日报来练手。
在之前这个仿知乎日报的 iOS App 已经很出名了,我也是借鉴(就是抄)了部分的实现,图片和 API 则是完全照搬。当然我也有我自己的不同之处,我的 UI 是尽可能采用 Storyboard+xib 来实现,另外也在一些细节上更贴近正版知乎日报。
这篇主要讲一下首页的实现,以下是动图。
首页结构
首页主要由以下几部分组成:
- 顶部的图片轮播
- 下面的 TableView
- 顶部的其他小东西:展开侧边栏的按钮,刷新控件,「今日新闻」的标题,和一个随着 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: 这个方法。 在这个方法里面,加入以下代码:
1 | CGFloat offsetY = scrollView.contentOffset.y; |
这段代码做了这些:
- 首先判断 contentOffset.y,如果大于零那就是在向上滑动。
- 如果 topView 不存在的话,就新生成一个,并且 topView 的背景颜色随着滑动距离变化。
- 最后设置轮播图片距离父视图 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)。这样就实现了一个可以无限循环的轮播。
相关代码如下:
1 | -(void)scrollViewDidScroll:(UIScrollView *)scrollView { |
具体运行时的效果如下:
刷新动画
这里有两部分内容,一是刷新控件的实现,二是刷新控件的控制。
刷新控件的是由两部分组成的,一个 UIActivityIndicatorView 和由 CAShapeLayer 绘制的圆环。
定义一个 RefreshView,初始化中加入以下代码:
1 | -(void)customInit { |
圆环由一个灰色的背景圆环和一个表示进度的白色圆弧组成,下拉过程中更新白色圆弧的长度,到指定位置后,整个圆环消失,开始 UIActivityIndicatorView 的动画。
更新圆环进度的代码如下:
1 | -(void)updateProgress:(CGFloat)progress { |
对刷新控件的控制其实和上滑的控制一样,也在 HomeViewController 中的 scrollViewDidScroll: 中,这部分逻辑就是 offsetY < 0 的那一部分。
1 | self.carouselViewHeight.constant = 220 - offsetY; |
这段代码的逻辑有:
- 下拉时增加轮播图片的高度。
- TableView 不是无限下拉的,只能下拉到一个指定的位置,超过的话,TableView 就不再下滑了。
- 下拉一段后再上滑,如果进入了刷新状态,不显示圆环;如果没进入刷新状态,那么就根据 下拉距离/下拉阈值 来更新圆环进度。
- 如果下拉距离达到了阈值并且松手了(没有拖动),那么就进入刷新状态。我这里做了个2秒刷新时间。
遗留
- 目前这个主页只做了展示,点击没有任何效果。
- TableView 滑上再滑下的时候,topView 不会完全消失,可能会有淡淡地残留,这点还没有优化。
- 原版的轮播图片底部和顶部有黑色阴影的渐变,这样在纯白的图片下,按钮和文字标题都可以清晰显示出来,这点我也没做。
另外,代码请戳 github。