下拉刷新控件 在一些应用里经常会使用到,当用户下拉时就会更新应用里的内容;还有一种就是上拉加载更多,例如我们在浏览微博时,不停往上拉,下面就会出现提示“加载更多”。 下面我们来了解它的实现原理:
下拉刷新控件有两种布局方式:
(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的显示图:

SmartRefreshLayout实现列表下拉刷新 下拉刷新控件_控件

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的显示效果:

SmartRefreshLayout实现列表下拉刷新 下拉刷新控件_ci_02

然后就是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; //是否加载
  1. 首先有个初始化方法,添加一个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];
}