“下拉刷新”和“上拉加载更多”应该是App标配的功能了,而iOS SDK只提供了UIRefreshControl控件且只有“下拉刷新”功能且只能用于UITableViewController,所以当项目中没有用到UITableViewController或者需要定制“下拉刷新”和“上拉加载更多”时只能自己定制实现,好在这块有好多现成的开源库,国内这块做的比较好的是小码哥的MJRefresh,今天就纪录下学习完MJRefrsh源码后的心得。
原理
总的来说就是给scrollview添加一个头视图(header)和尾视图(footer),通过监听scrollview的contentOffset变化来动态改变header和footer的状态(KVO实现),每种状态预先均已设置了一些监听事件(block实现),处于指定状态时就执行事先设置的事件。
header和footer的状态变化可以是根据scrollview的contentOffset来设置,也可以是刷新结束后主动设置。MJRefresh内部维护一个状态机,分别为MJRefreshStateIdle、MJRefreshStatePulling、MJRefreshStateRefreshing、MJRefreshStateWillRefresh、MJRefreshStateNoMoreData,具体处于哪种状态,默认是MJRefreshStateIdle状态。
以头视图header为例,一开始header的y坐标设置为一个负数这样在默认状态时就看不到header了,然后当下拉srollview时就能看到header了,当下拉到一定位置时也就是说contentOffset达到某个阈值时header就变为刷新状态了,此时就需要设置contentInset了(因为scroolview设置了bounce功能,如果不设置contentInset则下拉松开时scrollview就会回弹到初始原点的位置了,header就看不到了,所以为了让header在刷新状态时停留在顶部就需要设置contentInset)让header停留在顶部,当刷新状态结束时再把contentInset设置为0(原始值),然后header就看不到了,刷新结束。
Runtime
category增加属性
为了给UIScrollView增加header和footer属性,MJRefresh采用category实现,但category不能直接添加属性,但可以通过objc-runtime中objc_getAssociatedObject
和objc_setAssociatedObject方法实现,源码如下:
- (void)setMj_header:(MJRefreshHeader *)mj_header
{
if (mj_header != self.mj_header) {
// 删除旧的,添加新的
[self.mj_header removeFromSuperview];
[self insertSubview:mj_header atIndex:0];
// 存储新的
[self willChangeValueForKey:@"mj_header"]; // KVO
objc_setAssociatedObject(self, &MJRefreshHeaderKey,
mj_header, OBJC_ASSOCIATION_ASSIGN);
[self didChangeValueForKey:@"mj_header"]; // KVO
}
}
- (MJRefreshHeader *)mj_header
{
return objc_getAssociatedObject(self, &MJRefreshHeaderKey);
}
method swizzling
为了方便地在调用UITableView reloadData后执行指定事件,MJRefresh重写了UITableView和UICollectionView的load方法,在load方法中exchange了reloadData和自定义方法,具体技术用的是objc-runtime的method_exchangeImplementations方法。
+ (void)exchangeInstanceMethod1:(SEL)method1 method2:(SEL)method2
{
method_exchangeImplementations(class_getInstanceMethod(self, method1), class_getInstanceMethod(self, method2));
}
+ (void)load
{
[self exchangeInstanceMethod1:@selector(reloadData) method2:@selector(mj_reloadData)];
}
- (void)mj_reloadData
{
[self mj_reloadData];
[self executeReloadDataBlock];
}
下拉刷新具体实现细节
- 重写willMoveToSuperview方法。当header将要添加到目标view时,会调用该方法,在该方法中设置header的宽度、x坐标、最开始的contentInset、设置永远支持垂直弹簧效果及最重要的KVO scrollView的contentOffset、contentInset和panGestureRecognizer属性。
- 重写layoutSubviews方法。当header添加到目标view后会调用该方法,在该方法中设置header的y坐标为负的自身高度(高度是在初始话时设置的),这样header在初始状态时看不到,至此整个header的初始化工作就已完成,剩下的就是监听scrollview的属性改变自身状态。