本文主要是讨论在最近项目中遇到的一个下拉刷新控件,这个控件的效果如下图:
在这里会用两篇博文的篇幅来解析这个控件,第一篇解析控件的框架,第二篇解析动画。源代码可以在下面的链接下载:
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、至此完成了下拉刷新控件的框架,下一篇博文会分析这个控件里的动画效果。