UITableView下拉刷新原理
我们在用tableView加载数据时,经常会用到下拉刷新这个功能,那么下拉刷新的原理是什么,如何个封装一个好用下拉刷新控件呢?下面由我来详细介绍一下。
下拉刷新
下拉和上拉基本原理相似但是上拉刷新稍微复杂一点,所以我们先从下拉刷新讲起。
基本原理
下拉刷新的基本原理是通过判断tableView的contenOffset的属性变化来做一些相应的处理,实现方式主要用到了状态机模式,下拉过程中主要有三种状态(正常状态、正在下拉、正在刷新)在这三种状态下做不同的处理。
为了使用方便,所以代码的基本都封装在自定义控件中了,不说废话上代码
//// XQRefresh.h// 下拉刷新//// Created by code_xq on 16/3/5.// Copyright © 2016年 code_xq. All rights reserved.//#import#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值