转载

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

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

所闻所获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、至此完成了下拉刷新控件的框架,下一篇博文会分析这个控件里的动画效果。

正文到此结束
Loading...