咱們在用tableView加載數據時,常常會用到下拉刷新這個功能,那麼下拉刷新的原理是什麼,如何個封裝一個好用下拉刷新控件呢?下面由我來詳細介紹一下。ios
下拉和上拉基本原理類似可是上拉刷新稍微複雜一點,因此咱們先從下拉刷新講起。ui
下拉刷新的基本原理是經過判斷tableView的contenOffset的屬性變化來作一些相應的處理,實現方式主要用到了狀態機模式,下拉過程當中主要有三種狀態(正常狀態、正在下拉、正在刷新)在這三種狀態下作不一樣的處理。spa
爲了使用方便,因此代碼的基本都封裝在自定義控件中了,不說廢話上代碼.net
// // 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的思路code
/** * 當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監聽不去移除監聽的話程序會報錯。orm
- (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值發生改變時會調用上面的方法,狀態方法以下server
- (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顯示完成後再取初始值。rem
上拉刷新的原理和下拉相同,就是一些細節須要注意: