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值