下拉刷新(上拉加載更多)是大家經常用到的功能,本篇文章將帶大家詳細介紹下拉刷新原理,一步步實現下拉刷新效果。下拉刷新的核心原理是先自定義一個refreshView,然后將自定義的view添加到tableView(collectionView上)監聽tableView(或者collectionView)的contentOffset屬性,根據偏移量動態修改refreshView的子控件即可。下面一步步實現。
1.給UIScroolVIew添加一個分類UIScrollView+ZCRefresh.h,在該文件中添加如下代碼:
// UIScrollView+ZCRefresh.h
// ZCRefreshExample
// Created by MrZhao on 16/6/28.
// Copyright (c) 2016年 MrZhao. All rights reserved.
#import <UIKit/UIKit.h>
@class ZCHeaderRefreshView,ZCFooterRefreshView;
@interface UIScrollView (ZCRefresh)
/*
* 下拉刷新
*/
@property (nonatomic,strong)ZCHeaderRefreshView *zc_headerRefreshView;
/*
* 上拉加載更多
*/
@property (nonatomic,strong)ZCFooterRefreshView *zc_footerRefreshView;
@end
在UIScrollView+ZCRefresh.m文件中添加如下代碼:
// UIScrollView+ZCRefresh.m
// ZCRefreshExample
// Created by MrZhao on 16/6/28.
// Copyright (c) 2016年 MrZhao. All rights reserved.
#import "UIScrollView+ZCRefresh.h"
#import "ZCHeaderRefreshView.h"
#import "ZCFooterRefreshView.h"
#import <objc/runtime.h>
static const voidvoid *zc_headerRefresh_key = @"zc_headerRefresh_key";
static const voidvoid *zc_footerRefresh_key = @"zc_footerRefresh_key";
@implementation UIScrollView (ZCRefresh)
#pragma mark 實現下拉刷新控件的get set 方法
- (void)setZc_headerRefreshView:(ZCHeaderRefreshView *)zc_headerRefreshView {
if (zc_headerRefreshView != self.zc_headerRefreshView) {
//先刪除舊的
[self.zc_headerRefreshView removeFromSuperview];
[self insertSubview:zc_headerRefreshView atIndex:0];
//添加新的
objc_setAssociatedObject(self, zc_headerRefresh_key, zc_headerRefreshView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
}
- (ZCHeaderRefreshView *)zc_headerRefreshView {
return objc_getAssociatedObject(self, zc_headerRefresh_key);
}
- (void)setZc_footerRefreshView:(ZCFooterRefreshView *)zc_footerRefreshView {
if (zc_footerRefreshView != self.zc_footerRefreshView) {
[self.zc_footerRefreshView removeFromSuperview];
[self addSubview:zc_footerRefreshView];
//添加新的
objc_setAssociatedObject(self, zc_footerRefresh_key, zc_footerRefreshView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
}
- (ZCFooterRefreshView *)zc_footerRefreshView {
return objc_getAssociatedObject(self, zc_footerRefresh_key);
}
@end
添加分類的目的是在使用時,可直接是self.tableView.zc_headerRefresh = ***,這樣使用。其中用到了runtime里面的部分api,主要就是關聯相關的api,大家不熟悉可以自行查找相關文檔學習下。
2.自定義下拉刷新的View,ZCHeaderRefreshView.h,其中下拉的子控件和布局可以根據公司具體需求改動。ZCHeaderRefreshView.h中的代碼如下。
// ZCRefreshExample
// Created by MrZhao on 16/6/26.
// Copyright (c) 2016年 MrZhao. All rights reserved.
// git:https://github.com/MrZhaoCn/Refresh
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
@interface ZCHeaderRefreshView : UIView
/*
*提供有個工廠方法
*/
+ (instancetype)addHeaderRefreshViewWithTarget:(id)target action:(SEL)action;
/*
*開始下拉刷新
*/
- (void)beginRefreshing;
/*
*結束下拉刷新
*/
- (void)endHeaderRefreshing;
/*
*設置下拉刷新相關圖片
*/
//正常狀態的圖片
- (void)setHeaderNormorlImageWithName:(NSString *)imageName;
/*
*設置pullin狀態關圖片
*/
- (void)setHeaderPullingImageWithName:(NSString *)imageName;
/*
*設置動畫圖片
*/
- (void)setAnimantionImages:(NSArray *)images;
@end
ZCHeaderRefreshView.m中的代碼如下:
//
// ZCRefreshExample
//
// Created by MrZhao on 16/6/26.
// Copyright (c) 2016年 MrZhao. All rights reserved.
//
#import "ZCHeaderRefreshView.h"
#import <objc/message.h>
#define ZCContentOffset @"contentOffset"
#define ScreenWidth [UIScreen mainScreen].bounds.size.width
const static int headerRefreshHeight = 60 ;
/*
*枚舉下拉刷新的幾種狀態
*/
typedef enum {
kZCStateNomorl = 0, //默認狀態
kZCStatePulling, //下拉狀態
kZCStateRefreshing //正在刷新狀態
} state;
@interface ZCHeaderRefreshView ()
/*
*用于設置圖片
*/
@property (nonatomic,weak)UIImageView *imageView;
/*
*用于設置文字
*/
@property (nonatomic,weak)UILabel *label;
/*
*菊花控件
*/
@property (nonatomic,weak)UIActivityIndicatorView *activityView;
/*
* 記錄當前狀態
*/
@property (nonatomic, assign)int currentState;
/*
* 父控件
*/
@property (nonatomic,weak)UIScrollView *superview;
/*
* 記錄父控件初始時的偏移量,用于判定是否含有導航欄
*/
@property (nonatomic,assign)CGFloat contentOffSetY;
/*
* 目標
*/
@property(nonatomic,weak)id target;
/*
* 目標的方法,即刷新即將調用的方法
*/
@property(nonatomic,assign)SEL action;
/*
*下拉刷新正常時的圖片設置圖片
*/
@property(nonatomic,strong)UIImage *headerNormoalImage;
/*
*下拉時的圖片設置圖片
*/
@property(nonatomic,strong)UIImage *headerPullingImage;
/*
*執行動畫時的圖片數組
*/
@property(nonatomic,strong)NSArray *animationImages;
@end
@implementation ZCHeaderRefreshView
//工廠方法
+ (instancetype)addHeaderRefreshViewWithTarget:(id)target action:(SEL)action {
ZCHeaderRefreshView *refreash = [[self alloc] init];
refreash.frame = CGRectMake(0, -headerRefreshHeight, ScreenWidth, headerRefreshHeight);
//背景顏色可根據需求設置或者取消
refreash.backgroundColor = [UIColor colorWithRed:230/255.0 green:230/255.0 blue:230/255.0 alpha:1.0];
refreash.currentState = kZCStateNomorl;
if (target != nil &&action != nil) {
refreash.target = target;
refreash.action = action;
}else {
NSLog(@"請設置刷新時調用的方法!!!");
}
return refreash;
}
#pragma mark子控件布局
- (void)layoutSubviews {
[super layoutSubviews];
//圖片位置
CGFloat imagViewWH = 40;
//做了簡單的適配
CGFloat imagViewX = ScreenWidth * 0.3;
self.imageView.frame = CGRectMake( imagViewX, (self.frame.size.height - imagViewWH) / 2, imagViewWH, imagViewWH);
//文字位置
CGFloat labelX = CGRectGetMaxX(self.imageView.frame);
self.label.frame = CGRectMake(labelX , (self.frame.size.height - imagViewWH) / 2, 100, imagViewWH);
//菊花位置
self.activityView.frame = CGRectMake( imagViewX, (self.frame.size.height - imagViewWH) / 2, imagViewWH, imagViewWH);
}
#pragma 加到父控件時會調用該方法
- (void)willMoveToSuperview:(UIView *)newSuperview {
//是可以滾動的SCroolView才可以監聽滾動事件
if ([newSuperview isKindOfClass:[UIScrollView class]]) {
//刷新控件添加的到的父控件
self.superview = (UIScrollView *)newSuperview;
//為父控件添加觀察者,觀察父控件的contentOffset.y值的變化。
[newSuperview addObserver:self forKeyPath:ZCContentOffset options:NSKeyValueObservingOptionNew context:nil];
}
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change: (NSDictionary *)change context:(voidvoid *)context {
if ([keyPath isEqualToString:ZCContentOffset]) {
[self adjustRefreshView];
}
}
#pragma 當正在操作下拉刷新控件時調用該方法
- (void)adjustRefreshView {
//主要用來區別控制器有無導航欄
if (self.superview.contentInset.top == 64.000000) {
static dispatch_once_t one;
dispatch_once(&one, ^{
self.contentOffSetY = self.superview.contentInset.top;
});
}
CGFloat y = self.superview.contentOffset.y;
if (self.superview.isDragging) { //正在拖動
if (y< -self.contentOffSetY &&y> -self.contentOffSetY - headerRefreshHeight && self.currentState ==kZCStatePulling) { //正常狀態->下拉
self.currentState = kZCStateNomorl;
}else if (y <= -self.contentOffSetY - headerRefreshHeight && self.currentState == kZCStateNomorl)//下拉->正常
{
self.currentState = kZCStatePulling;
}
}else if(self.currentState ==kZCStatePulling &&y <= -self.contentOffSetY - headerRefreshHeight) { //手釋放
self.currentState = kZCStateRefreshing;
}
}
#pragma 重寫setState方法,在該方法中修改文字,圖片
if (_currentState == currentState) { //相等直接返回
return;
}
_currentState = currentState;
if (_currentState ==kZCStateNomorl) {//默認狀態什么都不
self.imageView.hidden = NO;
[self.activityView stopAnimating];
self.activityView.hidden = YES;
[UIView animateWithDuration:0.5 animations:^{
[self.imageView stopAnimating];
self.label.text = @"下拉刷新";
//如果沒有設置動畫圖片
if (self.animationImages == nil) {
if (self.headerNormoalImage == nil) {//沒有設置正常圖片,則采用默認的
self.imageView.image = [UIImage imageNamed:@"down"];
} else {//采用設置的圖片
self.imageView.image = self.headerNormoalImage;
}
} else {// 如果設置了動畫,則采用第一張做正常時的圖片
self.imageView.image = self.animationImages[0];
}
}];
}else if (_currentState ==kZCStatePulling){//下拉狀態
self.imageView.hidden = NO;
[self.activityView stopAnimating];
self.activityView.hidden = YES;
[UIView animateWithDuration:0.5 animations:^{
[self.imageView stopAnimating];
self.label.text= @"釋放立即刷新";
//如果沒有設置動畫圖片
if (self.animationImages == nil) {
if (self.headerPullingImage == nil) {//沒有設置下拉圖片,則采用默認的
self.imageView.image = [UIImage imageNamed:@"up"];
} else {//采用設置的圖片
self.imageView.image = self.headerPullingImage;
}
} else {
// 如果設置了動畫,則采用第一張做下拉時的圖片
self.imageView.image = self.animationImages[0];
}
}];
} else if (_currentState == kZCStateRefreshing){ //釋放刷新
self.label.text = @"正在刷新...";
//沒有動畫圖片,默認采用菊花控件
if (self.animationImages == nil) {
self.activityView.hidden = NO;
self.imageView.hidden = YES;
[self.activityView startAnimating];
} else {
self.imageView.hidden = NO;
self.activityView.hidden = YES;
self.imageView.animationDuration = 0.1 * self.animationImages.count;
[self.imageView startAnimating];
}
//放手之后不能立即返回
[UIView animateWithDuration:0.25 animations:^{
self.superview.contentInset = UIEdgeInsetsMake(self.superview.contentInset.top + headerRefreshHeight, self.superview.contentInset.left, self.superview.contentInset.bottom, self.superview.contentInset.right);
}];
//不能直接調用objec.msgSend()
void (*action)(id, SEL) = (void (*)(id, SEL)) objc_msgSend;
action(self.target,self.action);
}
}
#pragma 開始刷新
- (void)beginRefreshing {
self.currentState = kZCStateRefreshing;
}
#pragma 結束下拉刷新
- (void)endHeaderRefreshing {
if (self.currentState == kZCStateRefreshing) {
self.currentState = kZCStateNomorl;
[UIView animateWithDuration:0.25 animations:^{
self.superview.contentInset = UIEdgeInsetsMake(self.superview.contentInset.top - headerRefreshHeight, self.superview.contentInset.left, self.superview.contentInset.bottom, self.superview.contentInset.right);
}];
}
}
#pragma mark 一定要記得移除觀察者,不然會崩
- (void)dealloc {
[self.superview removeObserver:self forKeyPath:ZCContentOffset];
}
#pragma mark 設置圖片相關方法
- (void)setHeaderNormorlImageWithName:(NSString *)imageName {
self.headerNormoalImage = [UIImage imageNamed:imageName];
}
- (void)setHeaderPullingImageWithName:(NSString *)imageName {
self.headerPullingImage = [UIImage imageNamed:imageName];
}
- (void)setAnimantionImages:(NSArray *)images {
self.animationImages = images;
self.imageView.animationImages = self.animationImages;
}
#pragma mark懶加載子控件,放到最后這樣不影響主邏輯
// 1 圖片控件
- (UIImageView *)imageView {
if (_imageView == nil) {
UIImageView *imageView = [[UIImageView alloc] init];
//如果沒有設置動畫圖片
if (self.animationImages == nil)
{
if (self.headerNormoalImage == nil) {//沒有設置正常圖片,則采用默認的
imageView.image = [UIImage imageNamed:@"down"];
}
else {//采用設置的圖片
imageView.image = self.headerNormoalImage;
}
}else {// 如果設置了動畫,則采用第一張做正常時的圖片
imageView.image = self.animationImages[0];
}
imageView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin;
[self addSubview: imageView];
_imageView = imageView;
}
return _imageView;
}
//2 文本控件
- (UILabel *)label {
if (_label == nil) {
//2 文本控件
UILabel *label = [[UILabel alloc] init];
label.textColor = [UIColor darkGrayColor];
label.font = [UIFont systemFontOfSize:13];
label.backgroundColor = [UIColor clearColor];
label.textAlignment = NSTextAlignmentCenter;
label.text = @"下拉刷新";
[label sizeToFit];
[self addSubview:label];
_label = label;
}
return _label;
}
//菊花控件
- (UIActivityIndicatorView *)activityView {
if (_activityView == nil) {
UIActivityIndicatorView *activityView = [[UIActivityIndicatorView alloc] init];
self.activityView = activityView;
activityView.bounds = self.imageView.bounds;
activityView.autoresizingMask = self.imageView.autoresizingMask;
[self addSubview: activityView];
}
return _activityView;
}
@end
說明:
1)子控件的多少根位置可根據需求改變。
2)在控制器里面self.tableView.zc_headerRefresh = ***這段代碼時會調用willMoveToSuperView這個方法,在這個方法中就可以拿到父視圖tableView(collectionView),給tableView(collectionView)添加contOffSet觀察者。
3)根據contOffSet的變化,不斷調整刷新控件里面子控件的狀態,包括切換圖片,動圖等等.
4)注意在不同狀態下要調整tableView的contInset屬性.
3.在控制器里面可以這樣用:
//添加下拉刷新控件
ZCHeaderRefreshView *refreshView = [ZCHeaderRefreshView addHeaderRefreshViewWithTarget:self action:@selector(loadNewDataSoure)];
//如果沒有設置動畫則采用默認的菊花轉,且下拉和正常狀態圖片由代碼提供,若果不提供,則采用默認圖片,
//如果設置了動畫,則用動畫轉動,且下拉,和正常狀態的圖片采用動畫的第一張圖片。
UIImage *image1 = [UIImage imageNamed:@"icon_listheader_animation_1"];
UIImage *image2 = [UIImage imageNamed:@"icon_listheader_animation_2"];
NSArray *animationImages = @[image1,image2];
[refreshView setAnimantionImages:animationImages];
self.tableView.zc_headerRefreshView = refreshView;
[self.tableView.zc_headerRefreshView beginRefreshing];
好了,以上就是下拉刷新的基本寫法,下拉加載更多原理跟上拉加載更多類似,大家自行看代碼。
代碼地址:https://github.com/MrZhaoCn/ZCRefresh
倉庫里還有直播類的開源項目,自動計算cell高度的代碼,歡迎大家start.