所闻所获3:下拉刷新控件1

  本文主要是讨论在最近项目中遇到的一个下拉刷新控件,这个控件的效果如下图:

 

  在这里会用两篇博文的篇幅来解析这个控件,第一篇解析控件的框架,第二篇解析动画。源代码可以在下面的链接下载:

TestPullToRefresh.zip 

 

1、这个控件由以下几个文件组成:GMPullToAction、CircleProgressView、GMActivityView,其中GMPullToAction文件包含两个类:GMPullToRefresh和UIScrollView (GMPullToAction),CircleProgressView和GMActivityView各自包含一个同名的类。

  在这4个类中,GMPullToRefresh和UIScrollView (GMPullToAction)是控件的框架,CircleProgressView和GMActivityView负责动画。

2、这个控件定义在UIScrollView (GMPullToAction)内,所以使用方必须是UIScrollView或者它的子类的实例,使用时需要调用3个方法(假设当前控制器self有一属性scrollView):

[self.scrollView addPullToRefreshWithActionHandler:^{
    //下拉刷新时执行的代码
    ...
}];

  然后需要实现UIScrollViewDelegate的一个代理方法:

//在scrollView拖动的时候会不断调用这个方法
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    if (scrollView == self.scrollView) {
        [scrollView didScroll];
    }
}

  最后在加载完成后,还要调用以下这个方法来停用控件:

[self.scrollView.pullToRefreshView stopAnimating];

  我们就以这3个方法为入口来解析这个控件。

3、首先来看第一个方法- (void)addPullToRefreshWithActionHandler:(void (^)(void))actionHandler方法:

(1)、这个方法定义在UIScrollView (GMPullToAction)类中,它的代码如下:

- (void)addPullToRefreshWithActionHandler:(void (^)(void))actionHandler {
    GMPullToRefresh *pullToRefreshView = [[GMPullToRefresh alloc] initWithScrollView:self];
    pullToRefreshView.actionHandler = actionHandler;
    self.pullToRefreshView = pullToRefreshView;
}

  它使用initWithScrollView方法实例化了一个GMPullToRefresh类的对象pullToRefreshView,将块参数指定给pullToRefreshView.actionHandler,让pullToRefreshView可以在后续使用这段代码,最后把pullToRefreshView指定给自己的属性self.pullToRefreshView。

  这里需要注意一下,UIScrollView (GMPullToAction)类是一个分类,是不允许直接定义属性的,所以需要用到另外的方法来实现属性的效果,具体代码如下:

@interface UIScrollView (GMPullToAction)
@property (nonatomic, strong) GMPullToRefresh *pullToRefreshView;
@end
...
#import <objc/runtime.h>
static char UIScrollViewPullToRefreshView;
@implementation UIScrollView (GMPullToAction)
@dynamic pullToRefreshView;
...
- (void)setPullToRefreshView:(GMPullToRefresh *)pullToRefreshView {
    [self willChangeValueForKey:@"pullToRefreshView"];
    objc_setAssociatedObject(self, &UIScrollViewPullToRefreshView,
                             pullToRefreshView,
                             OBJC_ASSOCIATION_ASSIGN);
    [self didChangeValueForKey:@"pullToRefreshView"];
}
 
- (GMPullToRefresh *)pullToRefreshView {
    return objc_getAssociatedObject(self, &UIScrollViewPullToRefreshView);
}
...
@end

(2)、然后继续看GMPullToRefresh类的实例化方法initWithScrollView:

- (id)initWithScrollView:(UIScrollView *)scrollView {
    //初始化
    self = [super initWithFrame:CGRectZero];
    self.scrollView = scrollView;
    self.frame = CGRectMake(0, -kFrameHeight, scrollView.bounds.size.width, kFrameHeight);
    [_scrollView addSubview:self];

    //定制提示文字
    self.titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(kContent_Width/2.f-75/4.f, self.bounds.size.height*0.5-10, 75, 20)];
    _titleLabel.font = [UIFont boldSystemFontOfSize:14];
    _titleLabel.backgroundColor = [UIColor clearColor];
    _titleLabel.textColor = kTextColor;
    [self addSubview:_titleLabel];
   
    //矩形上升动画图
    ...
   
    //圆圈转动动画
    ...
   
    //指定state
    self.state = GMPullToRefreshStateHidden;
   
    return self;
}

  其中两个负责动画的类先不讨论,主要看self.state。它是一个枚举值,用来记录控件的状态,它的定义如下:

enum {
    GMPullToRefreshStateHidden = 1,  //隐藏
    GMPullToRefreshStateVisible,     //可见
    GMPullToRefreshStateTriggered,   //已触发刷新
    GMPullToRefreshStateLoading      //已在加载
};

typedef NSUInteger GMPullToRefreshState;

4、然后看第二个入口方法- (void)scrollViewDidScroll:(UIScrollView *)scrollView:

(1)、这个方法会在scrollView拖动的时候不断被调用,在这个方法中主要是执行了代码[scrollView didScroll]。- (void)didScroll方法也是定义在UIScrollView (GMPullToAction)类中,它的代码如下:

- (void)didScroll {
    //这个self是指scrollView
    CGPoint point=self.contentOffset;
    if (self.pullToRefreshView) {
        [self.pullToRefreshView scrollViewDidScroll:point];
    }
}

  在这个方法中,将scrollView实时的contentOffset传给了GMPullToRefresh的方法- (void)scrollViewDidScroll:(CGPoint)contentOffset;

(2)、GMPullToRefresh的- (void)scrollViewDidScroll:(CGPoint)contentOffset方法代码如下:

- (void)scrollViewDidScroll:(CGPoint)contentOffset {
    if (self.state == GMPullToRefreshStateLoading) {
        return;
    }
   
    //起点的y值(负数),它的绝对值也是pullToRefreshView的高度,也是触发刷新状态的高度
    CGFloat scrollOffsetThreshold = self.frame.origin.y;
   
    //已经触发刷新并且放开拖动了,就变成加载状态
    if(self.state == GMPullToRefreshStateTriggered && !self.scrollView.isDragging){
        self.state = GMPullToRefreshStateLoading;
       
    //scrollView开始往下拖动(<0),并且未达到触发刷新的高度(>scrollOffsetThreshold),为可见状态
    }else if(contentOffset.y < 0 &&  contentOffset.y > scrollOffsetThreshold && self.scrollView.isDragging && self.state != GMPullToRefreshStateLoading){
        self.state = GMPullToRefreshStateVisible;
       
    //scrollView往下拖动到已触发刷新的高度(<scrollOffsetThreshold)并且还没放手,为触发状态
    }else if(contentOffset.y <= scrollOffsetThreshold && self.scrollView.isDragging && self.state == GMPullToRefreshStateVisible){
        self.state = GMPullToRefreshStateTriggered;
       
    //scrollView往上推,为隐藏状态
    }else if(contentOffset.y >= 0 && self.state != GMPullToRefreshStateHidden){
        self.state = GMPullToRefreshStateHidden;
    }
   
    //拖动过程中的动画效果
    ...
}

  这个方法根据scrollView实时的contentOffset来决定状态self.state,这会调用self.state的set方法。

(3)、GMPullToRefresh的setState:方法代码如下:

- (void)setState:(GMPullToRefreshState)newState {
    _state = newState;
    switch (newState) {
        case GMPullToRefreshStateHidden:
            ... //动画
            [self setScrollViewContentInsetTop:0];            
            break;
           
        case GMPullToRefreshStateVisible:
            _titleLabel.text = NSLocalizedString(@"下拉刷新...",);
            ...
            break;
           
        case GMPullToRefreshStateTriggered:
            _titleLabel.text = NSLocalizedString(@"松开刷新...",);
            ...
            break;
           
        case GMPullToRefreshStateLoading:
            _titleLabel.text = NSLocalizedString(@"正在载入...",);
            ...
            [self setScrollViewContentInsetTop:self.frame.size.height];           
            if(_actionHandler)
                _actionHandler();
            break;
    }
}

  可以看到,对于不同的state,会指定不同的文字(指定动画的语句省略了,会在下一篇讨论),而在加载状态,会调用第一个入口方法传进来的块代码,即是执行加载的语句。

(4)、在上面的方法中,在隐藏状态和加载状态还会调用一个方法setScrollViewContentInset:来指定scrollView的contentInset。当状态为隐藏的时候,将contentInset的top置为0;当状态为加载的时候,将contentInset的top置为pullToRefreshView的高度。并且将这个过程做成动画效果:

- (void)setScrollViewContentInsetTop:(CGFloat)top {
    UIEdgeInsets inset = self.scrollView.contentInset;
    inset.top = top;
    [UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionAllowUserInteraction|UIViewAnimationOptionBeginFromCurrentState animations:^{
        self.scrollView.contentInset = inset;
       
    } completion:^(BOOL finished) {
    }];
}

5、最后一个入口方法,是在当数据加载完毕的时候调用的,调用这个方法会把state置为隐藏,让加载画面动画恢复到下拉前状态:

- (void)stopAnimating {
    self.state = GMPullToRefreshStateHidden;
}

6、至此完成了下拉刷新控件的框架,下一篇博文会分析这个控件里的动画效果。

原文地址:https://www.cnblogs.com/shayneyeorg/p/4713221.html