下拉刷新控件 在一些应用里经常会使用到,当用户下拉时就会更新应用里的内容;还有一种就是上拉加载更多,例如我们在浏览微博时,不停往上拉,下面就会出现提示“加载更多”。 下面我们来了解它的实现原理:
下拉刷新控件有两种布局方式:
(1) 刷新控件加载在UITableView的父视图上,不随着tableView移动
(2) 刷新控件加载在UITableView上随着tableView移动
两者没有太大区别。
下拉刷新主要有上下两部分组成:上部分是下拉才出现的刷新视图,定为headView;下部分是需要更新的内容视图,定为footerView。
进一步解析该控件的实现。
1. 定义headView,上面添加两个UILabel控件,和一个旋转的刷新圆圈。其刷新圆圈定义了一个CircleView,
2. 定义footerView,与headView类似,它的刷新圆圈用的是UIActivityIndicatorView。
headView.m内对两个label控件的实现,一个是显示刷新操作提示,第二个是刷新时间。就创建这两个控件,因代码简单,就不上代码了,下面实现的是它的一个初始化与刷新圆圈的代码实现。
#define CLLDefaultRefreshTotalPixels 60
//刷新圆圈
- (CLLRefreshCircleView *)circleView
{
if (!_circleView) {
_circleView = [[CLLRefreshCircleView alloc] initWithFrame:CGRectMake(110,15,CLLRefreshCircleViewHeight,CLLRefreshCircleViewHeight)];
}
return _circleView;
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
self.backgroundColor = [UIColor whiteColor];
[self addSubview:self.statusLabel];
[self addSubview:self.timeLabel];
[self addSubview:self.circleView];
}
return self;
}
headView的显示图:
FooterView.h与HeadView里的代码类似,有一个label是显示加载更多的提示。
#define CLLRefreshFooterViewHeight 40
- (UIActivityIndicatorView *)indicatorView {
if (!_indicatorView) {
_indicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
_indicatorView.frame = CGRectMake(110,(self.bounds.size.height - 20) * 0.5,20,20);
_indicatorView.hidesWhenStopped = YES;
}
return _indicatorView;
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor whiteColor];
[self addSubview:self.indicatorView];
[self addSubview:self.statusLabel];
}
return self;
}
- (void)resetView
{
if (_indicatorView.isAnimating) {
[_statusLabel sizeToFit];
CGRect tmpFrame = _indicatorView.frame;
tmpFrame.origin.x = (self.bounds.size.width - tmpFrame.size.width - _statusLabel.frame.size.width - 5) * 0.5;
tmpFrame.origin.y = (self.bounds.size.height - tmpFrame.size.height) * 0.5;
_indicatorView.frame = tmpFrame;
tmpFrame.origin.x = _indicatorView.frame.origin.x + _indicatorView.frame.size.width + 5;
tmpFrame.origin.y = (self.bounds.size.height - _statusLabel.frame.size.height) * 0.5;
tmpFrame.size = _statusLabel.frame.size;
_statusLabel.frame = tmpFrame;
}else {
[_statusLabel sizeToFit];
CGRect tmpFrame = _statusLabel.frame;
tmpFrame.origin.x = (self.bounds.size.width - tmpFrame.size.width ) * 0.5;
tmpFrame.origin.y = (self.bounds.size.height - tmpFrame.size.height) * 0.5;
_statusLabel.frame = tmpFrame;
}
}
footerView的显示效果:
然后就是headView内圆圈CircleView的实现
1. 在headView的位置及绘画
2. 实现了圆圈的一个动画效果,不停的旋转。
//开始画圆圈时的offset
#define CLLRefreshCircleViewHeight 20
1. (CABasicAnimation*)repeatRotateAnimation {
CABasicAnimation *rotateAni = [CABasicAnimation animationWithKeyPath: @"transform.rotation.z"];
rotateAni.duration = 0.25;
rotateAni.cumulative = YES;
rotateAni.removedOnCompletion = NO;
rotateAni.fillMode = kCAFillModeForwards;
rotateAni.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
rotateAni.toValue = [NSNumber numberWithFloat:M_PI / 2];
rotateAni.repeatCount = MAXFLOAT;
return rotateAni;
}
2. (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(context, [UIColor colorWithRed:173 / 255.0 green:53 / 255.0 blue:60 / 255.0 alpha:1].CGColor);
CGContextSetLineWidth(context, 1.f);
static CGFloat radius = 9;
if (!_isRefreshViewOnTableView) {
static CGFloat startAngle = M_PI / 2;
CGFloat endAngle = (ABS(_offsetY) / _heightBeginToRefresh) * (M_PI * 19 / 10) + startAngle;
CGContextAddArc(context, CGRectGetWidth(self.frame) / 2, CGRectGetHeight(self.frame) / 2, radius, startAngle, endAngle, 0);
} else {
static CGFloat startAngle = 3 * M_PI / 2.0;
CGFloat endAngle = (ABS(_offsetY) / _heightBeginToRefresh) * (M_PI * 19 / 10) + startAngle;
CGContextAddArc(context, CGRectGetWidth(self.frame) / 2, CGRectGetHeight(self.frame) / 2, radius, startAngle, endAngle, 0);
}
CGContextDrawPath(context, kCGPathStroke);
}
3.重要的是对headView与footerView的整合,定义一个类RefreshHeadController,继承NSObject,里面的方法较多,下面就抽象的说下它的方法调用。
定义的一些属性有:
@property (nonatomic,strong)UIScrollView *scrollView;
@property (nonatomic,strong)CLLRefreshHeadView *refreshHeadView; // 刷新头视图
@property (nonatomic,strong)CLLRefreshFooterView *refreshFooterView; //加载更多尾视图
@property (nonatomic,weak)id<CLLRefreshHeadControllerDelegate>delegate; // 代理
@property (nonatomic, readwrite) CGFloat originalTopInset; //原始离顶点的偏移量(相对虚拟机)
@property (nonatomic, assign) CLLRefreshState refreshState; //刷新状态
@property (nonatomic, assign) CLLLoadMoreState loadMoreState; //加载更多的状态
@property (nonatomic, assign) CLLRefreshViewLayerType refreshViewLayerType; //刷新layer的布局类型
@property (nonatomic, assign) BOOL isPullDownRefreshed; //下拉是否刷新
@property (nonatomic, assign) BOOL isPullUpLoadMore; //上拉是否加载
@property (nonatomic, assign) BOOL pullDownRefreshing; //是否刷新
@property (nonatomic, assign) BOOL pullDownMoreLoading; //是否加载
- 首先有个初始化方法,添加一个scrollView与代理。
- (id)initWithScrollView:(UIScrollView *)scrollView viewDelegate:(id <CLLRefreshHeadControllerDelegate>)delegate
{
self = [super init];
if (self) {
self.delegate = delegate;
self.scrollView = scrollView;
[self setup];
}
return self;
}
//添加头视图
- (void)setup
{
self.originalTopInset = self.scrollView.contentInset.top;
[self configuraObserverWithScrollView:self.scrollView];
self.refreshHeadView.timeLabel.text = @"刷新时间";
self.refreshHeadView.statusLabel.text = @"下拉刷新";
self.refreshState = CLLRefreshStateNormal;
if (self.refreshViewLayerType == CLLRefreshViewLayerTypeOnSuperView) {
self.scrollView.backgroundColor = [UIColor clearColor];
UIView *currentSuperView = self.scrollView.superview;
if (self.isPullDownRefreshed) {
[currentSuperView insertSubview:self.refreshHeadView belowSubview:self.scrollView];
}
} else if (self.refreshViewLayerType == CLLRefreshViewLayerTypeOnScrollViews) {
if (self.isPullDownRefreshed) {
[self.scrollView addSubview:self.refreshHeadView];
}
}
}
2.使用观察者,观察到偏移量的改变,通过协议改变下拉刷新的状态,加载数据,加载完后停止刷新
//下拉刷新的状态
typedef NS_ENUM(NSInteger, CLLRefreshState) {
CLLRefreshStatePulling = 0,
CLLRefreshStateNormal = 1,
CLLRefreshStateLoading = 2,
CLLRefreshStateStopped = 3,
};
//上拉加载更多的状态
typedef NS_ENUM(NSInteger, CLLLoadMoreState) {
CLLLoadMoreStateNormal = 10,
CLLLoadMoreStateLoading = 11,
CLLLoadMoreStateStopped = 12,
};
//刷新视图布局类型
typedef NS_ENUM(NSInteger, CLLRefreshViewLayerType) {
CLLRefreshViewLayerTypeOnScrollViews = 0,
CLLRefreshViewLayerTypeOnSuperView = 1,
};
3.定义的协议方法
@protocol YXYRefreshHeadControllerDelegate <NSObject>
@required
/**
* 1.下拉开始刷新
*/
- (void)beginPullDownRefreshing;
/**
* 2.上拉加载更多
*/
- (void)beginPullUpLoading;
@optional
/**
* 1、标识下拉刷新是UIScrollView的子view,还是UIScrollView父view的子view
*
* @return 如果没有实现该delegate方法,默认是scrollView的子View,为CLLRefreshViewLayerTypeOnScrollViews
**/
- (YXYRefreshViewLayerType)refreshViewLayerType;
/**
* 2、UIScrollView的控制器是否保留iOS7新的特性,意思是:tablView的内容是否可以显示导航条后面
*
* @return 如果不实现该delegate方法,默认是不支持的
**/
- (BOOL)keepiOS7NewApiCharacter;
/**
* 3. 是否显示 上拉更多视图
* @return 如果不实现该delegate方法,默认是没有更多
**/
- (BOOL)hasRefreshFooterView;
@end
4.在界面下拉或上拉时进行刷新,通过观察者,观察界面下拉视图的偏移量的改变触发刷新事件。
为scrollView添加了三个观察者,为contentSize、contentInset和contentOffset,这三个是scrollView的基本的属性。
contentSize是scrollView可以滚动的区域;contentInset是scrollView的contentView的顶点相对于scrollView的位置;contentOffset是scrollView当前显示区域顶点相对于frame顶点的偏移量。
- (void)configuraObserverWithScrollView:(UIScrollView *)scrollView {
[scrollView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil];
[scrollView addObserver:self forKeyPath:@"contentInset" options:NSKeyValueObservingOptionNew context:nil];
[scrollView addObserver:self forKeyPath:@"contentSize" options:NSKeyValueObservingOptionNew context:nil];
}
当它观察的值发生改变,就会调用下面的方法:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([keyPath isEqualToString:@"contentOffset"]) {
CGPoint contentOffset = [[change valueForKey:NSKeyValueChangeNewKey] CGPointValue];
if (self.isPullDownRefreshed) {
// 下拉刷新的逻辑方法
if(self.refreshState != CLLRefreshStateLoading) {
// 如果不是加载状态的时候
if (ABS(self.scrollView.contentOffset.y + [self getAdaptorHeight]) >= CLLRefreshCircleViewHeight) {
self.refreshHeadView.circleView.offsetY = MIN(ABS(self.scrollView.contentOffset.y + [self getAdaptorHeight]), CLLDefaultRefreshTotalPixels) - CLLRefreshCircleViewHeight;
[self.refreshHeadView.circleView setNeedsDisplay];
}
CGFloat scrollOffsetThreshold;
scrollOffsetThreshold = -(CLLDefaultRefreshTotalPixels + self.originalTopInset);
if(!self.scrollView.isDragging && self.refreshState == CLLRefreshStatePulling) {
self.pullDownRefreshing = YES;
self.refreshState = CLLRefreshStateLoading;
} else if(contentOffset.y < scrollOffsetThreshold && self.scrollView.isDragging && self.refreshState == CLLRefreshStateStopped) {
self.refreshState = CLLRefreshStatePulling;
} else if(contentOffset.y >= scrollOffsetThreshold && self.refreshState != CLLRefreshStateStopped) {
self.refreshState = CLLRefreshStateStopped;
}
} else {
CGFloat offset;
UIEdgeInsets contentInset;
offset = MAX(self.scrollView.contentOffset.y * -1, 0.0f);
offset = MIN(offset, self.refreshTotalPixels);
contentInset = self.scrollView.contentInset;
self.scrollView.contentInset = UIEdgeInsetsMake(offset, contentInset.left, contentInset.bottom, contentInset.right);
}
}
if (self.isPullUpLoadMore) {
if(self.loadMoreState != CLLLoadMoreStateLoading) {
contentOffset.y += self.scrollView.bounds.size.height;
float scrollOContentSizeHeight = self.scrollView.contentSize.height + CLLRefreshFooterViewHeight;
if(!self.scrollView.isDragging && contentOffset.y > scrollOContentSizeHeight) {
self.pullDownMoreLoading = YES;
self.loadMoreState = CLLLoadMoreStateLoading;
}
}else {
if (self.pullDownMoreLoading) {
CGFloat offset;
UIEdgeInsets contentInset;
offset = 0;
offset = MAX(offset, CLLRefreshFooterViewHeight);
contentInset = self.scrollView.contentInset;
self.scrollView.contentInset = UIEdgeInsetsMake(contentInset.top, contentInset.left, offset, contentInset.right);
}
}
}
} else if ([keyPath isEqualToString:@"contentInset"]) {
} else if ([keyPath isEqualToString:@"contentSize"]) {
BOOL hasFooterView = [self isPullUpLoadMore];
if (hasFooterView) {
CGRect tmpFrame = self.refreshFooterView.frame;
tmpFrame.origin.y = self.scrollView.contentSize.height;
self.refreshFooterView.frame = tmpFrame;
}else {
[self.refreshFooterView removeFromSuperview];
self.refreshFooterView = nil;
}
}
}
5.同时要注意它的内存管理,在这个scrollView销毁的时候,要移除它的观察。
- (void)removeObserverWithScrollView:(UIScrollView *)scrollView {
[scrollView removeObserver:self forKeyPath:@"contentOffset" context:nil];
[scrollView removeObserver:self forKeyPath:@"contentInset" context:nil];
[scrollView removeObserver:self forKeyPath:@"contentSize" context:nil];
}
6.在对界面进行下拉与上拉刷新时,通过观察者能监听到视图的偏移量,进而改变它们对应的状态改变它们的位置。
上拉加载更多:
- (void)setLoadMoreState:(CLLLoadMoreState)loadMoreState {
switch (loadMoreState) {
case CLLLoadMoreStateStopped:
case CLLLoadMoreStateNormal:{
//上拉加载更多
self.refreshFooterView.statusLabel.text = @"上拉加载更多";
[self.refreshFooterView.indicatorView stopAnimating];
}
break;
case CLLLoadMoreStateLoading:{
//加载中
self.refreshFooterView.statusLabel.text = @"加载中";
[self.refreshFooterView.indicatorView startAnimating];
if (self.pullDownMoreLoading) {
[self callBeginPullUpLoading];
}
}
break;
default:
break;
}
if (_refreshFooterView) {
[_refreshFooterView resetView];
}
_loadMoreState = loadMoreState;
}
下拉刷新
- (void)setRefreshState:(CLLRefreshState)refreshState
{
switch (refreshState) {
case CLLRefreshStateStopped:
case CLLRefreshStateNormal: {
self.refreshHeadView.statusLabel.text = @"下拉刷新";
break;
}
case CLLRefreshStateLoading: {
if (self.pullDownRefreshing) {
self.refreshHeadView.statusLabel.text = @"正在加载";
[self setScrollViewContentInsetForLoading];
if(_refreshState == CLLRefreshStatePulling) {
[self animationRefreshCircleView];
}
}
break;
}
case CLLRefreshStatePulling:
self.refreshHeadView.statusLabel.text = @"释放立即刷新";
break;
default:
break;
}
_refreshState = refreshState;
}
headView内的刷新圆圈动画
//刷新动画 <圆圈旋转>
- (void)animationRefreshCircleView {
if (self.refreshHeadView.circleView.offsetY != CLLDefaultRefreshTotalPixels - CLLRefreshCircleViewHeight) {
self.refreshHeadView.circleView.offsetY = CLLDefaultRefreshTotalPixels - CLLRefreshCircleViewHeight;
[self.refreshHeadView.circleView setNeedsDisplay];
}
// 先去除所有动画
[self.refreshHeadView.circleView.layer removeAllAnimations];
// 添加旋转的动画
[self.refreshHeadView.circleView.layer addAnimation:[CLLRefreshCircleView repeatRotateAnimation] forKey:@"rotateAnimation"];
[self callBeginPullDownRefreshing];
}