Tantan Fu

Nov 01, 2015

从零开始 Pull to refresh

notion image
仿照「把大象放进冰箱」的思路,Pull To Refresh 的过程可以分解成如下步骤:
  1. 在顶部添加下拉出来的视图
  1. 当下拉到一定位置的时候固定视图,写个动画得瑟一下
  1. 动画过程中进行访问服务器等操作
  1. 等活儿干完了通知下拉视图躲起来

准备工作

其实需要准备的并不多,首先新建一个工程,使用 TableView,造一些 fake data,让视图更美观一些。当然,如果你不想做这些无用功,可以在 这里 下载我们的 Start Project。
目前,视图应该长这样:
notion image
代码中只是简单设置了下有多少行数据,每行显示一些简单的文字。

添加下拉视图

我们需要在 TableView 的顶端添加一个视图,在下拉的时候,将它一同「拽」下来。我们新建一个 UIView 的子类 RefreshView
内容也非常简单,设置了 frame。同时,为了记录下拉的程度,添加了一个progress变量来记录它。用isRefreshing来表示是否正在刷新,并设置了一个背景颜色来更好的区分。我们在 ViewController 中来添加这个视图到我们的 tableView 上。
ViewController 中添加一个实例变量来表示下拉视图:
viewDidLoad() 方法中,添加如下代码:
运行之后,会发现下拉的时候,上面会出现绿色的区域,这就是我们添加的 RefreshView 视图啦。

下拉时的事件响应

在下拉的过程中,有时我们会需要根据下拉的长度来做相应的动画,比如,下拉过程中,开始画一个圆,下拉到一定的程度之后,整个圆就绘制完成了。为了达到这个目的,需要知道表格视图向下滑动了多少距离。
UITableViewUIScrollView的子类,而UITableViewController又实现了UIScrollViewDelegate协议。在下拉的过程中, UIScrollViewDelegate 的方法会得到调用,我们可以在这里动态获得下拉的距离。
ViewController 类的外面,添加如下代码
类的扩展,extension 可以帮助我们分割代码块区域,使得代码在源文件中看起来更加有条理,可以参照示例代码中的 UITableViewDataSource 和 UITableViewDelegate 的实现。
这里我们可以进行计算,得到下拉的距离,但是作为一个对代码整洁有追求的程序员,我们来思考一下职责单一的问题。
RefreshView作为下拉的视图,不应该与视图控制器过度耦合。我们可以把tableView的滚动事件传递给RefreshView,让它自己来计算,判断,进行各种逻辑的处理。
从上面的代码来看,scrollViewDidScroll(scrollView: UIScrollView)函数在下拉的时候,不断调用,其实是把每次更新后的scrollView传递过来,然后通过它的属性变化来做处理,这里我们可以将scrollView参数传递给我们的RefreshView视图。
思路明确,在 ViewControllerscrollViewDidScroll 方法调用的时候,我们调用 RefreshView 的方法,将 scrollView 参数传进去。我们干脆使用和 UIScrollViewDelegate 协议中一样的方法名,我们让 RefreshView 实现 UIScrollViewDelegate 协议,这样也方便方法名补全。
在 RefreshView 类外面,添加如下代码:
于是,我们可以在 ViewControllerscrollViewDidScroll 方法中,调用该方法。在该方法中添加如下语句
运行,下拉视图,可以发现,不断有 print 语句输出。成功!
根据这个思路,我们来计算下拉程度,在 RefreshViewscrollViewDidScroll 方法中,添加如下代码
计算过程中,如果开始时候向上拉动,不计入拉动距离,如果已经超过了 RefreshView 视图的高度,progress 仍然为 1。之后判断是否正在刷新,如果不是,进行下拉时候的动画。这里我们添加一些动画调用接口,方便之后的使用。为 RefreshView 类添加如下方法:
动画接口已经完成,我们之后可以很方便在这些方法中实现我们的动画效果。

设置 delegate

在下拉到一定程度,松开手指的时候,会触发刷新,这时候让去做一些像访问服务器等比较费时的操作,同时,RefreshView 需要固定在 tableView 视图的上端,在完成网络访问等操作之后,再取消固定,恢复当初的模样。
为了能够让触发刷新时,执行一些 ViewController 中定义的工作,我们需要使用 Delegate 模式。在 RefreshView 类上面,定义个一个 protocol:
这里我们定义了一个 protocol,简单来说就是一个占位符,我们将在适当的时候,调用协议中的方法,而对于 RefreshView 来说,它对方法的具体内容一无所知,方法的将在遵循这个协议的具体类中实现。为了能够调用该方法,我们需要为 RefreshView 添加一个成员变量
现在我们来思考,什么时候调用协议的方法呢?当用户下拉到一定程度(这里是超过 RefreshView)的高度,松开手指的时候,我们需要调用该方法来实现一些费时的操作。为了响应下拉之后松开的事件,我们需要实现 UIScrollViewDelegate 中的 scrollViewWillEndDragging:withVelocity:targetContentOffset: 方法,按照同样的思路,我们将这个方法的参数传递给 RefreshView,让它自行处理
RefreshViewscrollViewDidScroll 方法下面,添加如下方法:
在方法中,如果没有进行刷新,并且下拉的程度足够的话,就进行刷新,同时调用 RefreshViewDelegate 协议中的方法,并开始刷新的动画。下面需要在 ViewController 中,将松开手指的事件传递过来,在 ViewControllerscrollViewDidScroll 方法下面,添加如下代码:
这里与之前的做法相同,只是将所有参数传递给了 RefreshView
为了在触发刷新的时候进行相应的事物处理操作,需要让 ViewController 遵循 RefreshViewDelegate 协议,并实现相应的方法,这里我们只是简单的输出了一条语句。
同时,在 viewDidLoad 实例化 RefreshView 之后,将它的 delegate 设置为 ViewController

固定 RefreshView 视图

编译运行,哦吼,出现问题了,这里的视图并没有在执行刷新动画(目前仅是输出一条语句)的时候固定住。我们希望在刷新被触发的时候,顶部的视图能够固定住,然后,当 RefreshViewDelegate 中的方法执行完成的时候,再隐藏顶部视图。
思路明确,要能够固定住视图,并且能够取消固定。为了固定该视图,我们需要增加 ScrollViewcontentInset.top,取消的话将该值复原就可以了。这里为了能够改变 ScrollView 的这个属性,我们需要拿到 ScrollView 的引用,修改 RefreshViewinit 方法,改为如下:
同时修改 ViewController 中的 refreshView 实例化代码,改为:
下面为 RefreshView 添加是否固定视图的方法
在触发刷新的时候需要固定视图,在 scrollViewWillEndDragging 方法中的 if 语句中,添加如下代码:
这时候编译运行,下拉到一定程度之后,会固定住视图。
notion image
下面要做的就是在用户完成访问网络等操作之后,显式调用方法来取消固定。修改 endRefreshing 方法如下:
修改 ViewControllerrefreshViewDidRefresh 方法:
这里模拟费时的操作,在 3 秒之后,取消 RefreshView 的视图固定。
编译运行,默数 1,2,3,果然视图取消固定了!但是这里取消固定的时候有些突兀,我们加上一个简单的动画,让它看起来更自然一些,修改 RefreshViewendRefreshing 方法:
到这里,从零开始做一个下拉刷新控件已经完成了,从视觉上并没有多么漂亮,但是一些动画的接口我们已经留好,只需要添加一些动画的代码就可以了。如果你需要完成后的代码,可以从这里下载
参考资料

Copyright © 2024 Tantan Fu