UITableView下拉刷新原理
我们在用tableView加载数据时,经常会用到下拉刷新这个功能,那么下拉刷新的原理是什么,如何个封装一个好用下拉刷新控件呢?下面由我来详细介绍一下。
下拉刷新
下拉和上拉基本原理相似但是上拉刷新稍微复杂一点,所以我们先从下拉刷新讲起。
基本原理
下拉刷新的基本原理是通过判断tableView的contenOffset的属性变化来做一些相应的处理,实现方式主要用到了状态机模式,下拉过程中主要有三种状态(正常状态、正在下拉、正在刷新)在这三种状态下做不同的处理。
为了使用方便,所以代码的基本都封装在自定义控件中了,不说废话上代码
//
// XQRefresh.h
// 下拉刷新
//
// Created by code_xq on 16/3/5.
// Copyright © 2016年 code_xq. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "UIView+Expand.h"
#ifndef XQRefresh
#define XQRefresh
typedef NS_ENUM(NSInteger, RefreshState) {
RefreshStateNormal = 0,
RefreshStatePulling = 1,
RefreshStateRefreshing = 2,
RefreshStateDefault = 3
};
#endif // XQRefresh
@interface XQRefreshHeader : UIView
+ (instancetype)initWithBlock:(void (^)(void))refreshingBlock;
- (void)beginRefreshing;
- (void)endRefreshing;
@end
这里提供了三个方法所以调用方式也非常简单
__weak typeof (self)weakSelf = self;
XQRefreshHeader *refreshHeader = [XQRefreshHeader initWithBlock:^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
dispatch_get_main_queue(), ^{
// 业务逻辑
....
[weakSelf.tableView reloadData];
[weakSelf.refreshHeader endRefreshing];
});
}];
[tableView addSubview:refreshHeader];
XQRefreshHeader 的实现会用到ios的kvo机制,用来监听tableView的contentOffset的变化,这样做的好处是不用使用scrollView的众多代理方面,少了一层ViewController可以将所有的操作封装到view中实现,这里借鉴了MJRefresh的思路
/**
* 当view被添加到父视图时被调用,父视图销毁时也会被调用此时newSuperview为空
*/
- (void)willMoveToSuperview:(UIView *)newSuperview {
[super willMoveToSuperview:newSuperview];
// 移除监听
[self.superview removeObserver:self forKeyPath:XQRefreshContentOffset];
if (newSuperview) {
self.tableView = (UITableView *)newSuperview;
self.width = newSuperview.width;
self.height = newSuperview.height;
self.bottom = newSuperview.top;
UILabel *textLabel = [[UILabel alloc] init];
[self addSubview:textLabel];
textLabel.width = 100;
textLabel.center = self.center;
textLabel.height = 30;
textLabel.bottom = self.height - 10;
textLabel.textColor = [UIColor colorWithRed:0.5 green:0.5 blue:0.5 alpha:1.0];
textLabel.textAlignment = NSTextAlignmentCenter;
self.textLabel = textLabel;
UIImageView *imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"arrow"]];
[self addSubview:imageView];
imageView.width = 18;
imageView.height = 26;
imageView.right = textLabel.left -5;
imageView.bottom = self.height - 12;
imageView.hidden = YES;
self.imageView = imageView;
UIActivityIndicatorView *activity = [[UIActivityIndicatorView alloc] init];
activity.width = 50;
activity.height = 50;
activity.center = textLabel.center;
[activity setActivityIndicatorViewStyle:UIActivityIndicatorViewStyleGray];
[self addSubview:activity];
self.activity = activity;
// 设置view的背景色
self.backgroundColor = [UIColor colorWithRed:0.9 green:0.9 blue:0.9 alpha:0.9];
self.hidden = YES;
self.curState = RefreshStateDefault;
// 添加监听
[newSuperview addObserver:self forKeyPath:XQRefreshContentOffset options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
}
}
这里用willMoveToSuperview方法初始化控件主要考虑到了这个方法的一个特性,当一个view被添加到父view时newSuperView不为空但是self.superView却为空,当控制器跳转时还会调用一次这个方法,此时正好相反newSuperView为空self.superView不为空,利用这个特性可以用来添加监听和移除监听,如果说只给某个对象的属性添加了kvo监听不去移除监听的话程序会报错。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
// 这里是为了记录初始化完成后的contentInset值
if (!self.tableView.isTracking && self.curState == RefreshStateDefault) {
_startInsetTop = self.tableView.contentInset.top;
return;
}
if ([keyPath isEqualToString:XQRefreshContentOffset]) {
CGFloat offsetY = - [change[@"new"] CGPointValue].y;
CGFloat cValue = offsetY - _startInsetTop;
if (cValue > 0 && cValue < refreshHeigh) {
// 下拉过程但是没有超过给定的高度此时的状态为RefreshStatePulling
} else if (cValue >= refreshHeigh && !_tableView.isDragging) {
// 正在刷新状态此时变化值等于给定的高度且手指离开屏幕 RefreshStateRefreshing
} else if (cValue <= 0){
// 正常状态RefreshStateNormal
} else if (cValue >= refreshHeigh && _tableView.isDragging) {
// 下拉过程但是超过给定的高度此时的状态为RefreshStatePulling
}
}
}
当contentOffset值发生改变时会调用上面的方法,状态方法如下
- (void)setStates:(RefreshState)state offsetValue:(CGFloat)offsetValue {
switch (state) {
case RefreshStateNormal: {
// 清理工作将view中的所有改变了的属性恢复到下拉之前
}
break;
case RefreshStatePulling: {
}
break;
case RefreshStateRefreshing: {
....
// 改变tableView的contentInset值,让它停留在下拉状态(重要)
[UIView animateWithDuration:0.5 animations:^{
self.tableView.contentInset = UIEdgeInsetsMake(offsetValue + refreshHeigh, 0, 0, 0);
}];
// 回调block(重要)
_refreshingBolck();
}
break;
default:
break;
}
// 记录当前的刷新状态
_curState = state;
}
这里还要说明的一个细节是- (void)beginRefreshing方法的实现
- (void)beginRefreshing {
self.hidden = NO;
self.textLabel.text = self.textLabel.text = @"松手刷新...";
[UIView animateWithDuration:0.09 animations:^{
} completion:^(BOOL finished) {
self.curState = RefreshStatePulling;
[self setStates:RefreshStateRefreshing offsetValue:_startInsetTop];
}];
}
因为此方法一般在tableView创建以后立即调用,此时有可能取到的startInsetTop原始值不正确,所有这里采用适当的延迟等tableView显示完成后再取初始值。
上拉刷新
上拉刷新的原理和下拉相同,就是一些细节需要注意:
- 每次刷新时刷新控件footerRefresh的y值要随contentSize的改变而改变
- 下拉完成时也要改变footerRefresh的y值