码迷,mamicode.com
首页 > 其他好文 > 详细

自定义下拉刷新控件

时间:2016-04-13 23:53:41      阅读:577      评论:0      收藏:0      [点我收藏+]

标签:

一、功能效果

 

1、在很多app中,在信息展示页面,当我们向下拖拽时,页面会加载最新的数据,并有一个短暂的提示控件出现,有些会有加载进度条,有些会记录加载日期、条目,有些还带有加载动画。其基本实现原理都相仿,本文中将探讨其实现原理,并封装出一个简单的下拉刷新控件

2、自定义刷新工具简单的示例

技术分享

 

 

二、系统提供的下拉刷新工具

 

1、iOS6.0以后系统提供了自己的下拉刷新的控件:UIRefreshControl 。例如,refreshControl,作为UITableViewController中的一个属性,使用时只需要实例化即可。

2、refreshControl是UIRefreshControl 类型的对象,UIRefreshControl继承自UIControl,和UIButton是“兄弟”。

3、使用方式(swift语言示例):

(1)直接在UITableViewController中实例化属性refreshControl ,例: self.refreshControl = UIRefreshControl()

(2)给控件添加事件响应,例:self.refreshControl.addTarget(self, action:”reload”,forControlEvents:UIControlEvents.ValueChanged)

4、UIRefreshControl特点:

(1)一旦实例化,就有默认的宽高。

(2)一旦在UITableViewController中实例化,就会在表格中自动配置frame,显示在表格上方。

(3)在表格上面显示一个逐渐展开的“小菊花”进度轮。

(4)当用户下拉刷新时,会触发UIRefreshControl的UIControlEventValueChanged 事件 (类似UIButton会触发TouchUP系列事件)

(5)在UITableViewController实例化并且添加响应事件后,默认的进度轮就可以转了,如果想要停止,使用:endRefreshing方法。

 

 

三、自定义下拉刷新控件

1、系统提供的工具在一般情况下够用了,但是针对一些个性化需求,它还远远不够,作为自定义刷新控件的模范,我们必须向功能强大的MJRefresh致敬。

 

2、笔者设计下拉刷新控件的思路

(1)要添加一个给用户提示信息的“表头”,在下拉刷新时,根据刷新的情况,显示不同的内容,包括一些个性化的动画设计。这个表头要放在表格的最上方,同时这个表头本身就是我们的刷新控件。我们要考虑它的位置、大小和内容。

 

(2)我们的自定义控件类,必须可以通过一种机制,时刻获取到表格的动态,以前我们通过UIScrollerView中的一系列代理方法,可以实现与用户交互的效果,但是代理的使用不是很方便,我们这次尝试效率更高,逻辑上更准确的解决方式:KVO。

 

(3)使用KVO,首先要确定监测的目标,我们要清楚根据什么来决定tableView是否在下拉拖拽,或者拖拽到什么程度了。必须有一个准确的能够反映表格偏移状态的值:contentSize。

 

(4)思考,我们的控件应该是继承自什么类?这里为了和系统的控件进行比较,我们也仿照系统的UIRefreshControl,选择继承自UIControl,UIControl本身是继承自UIView的。

 

(5)下拉刷新是要实现用户的业务逻辑(加载更多数据),我们如何让自己封装好的控件,实施用户逻辑的业务代码,这些代码不可能封装在自定义的刷新控件中,所以,考虑几种思路:代理,通知,block回调。本例使用blcok回调。

 

(6)下拉过程中要人为的定义几种不同的状态,然后根据这几种不同的状态,显示不同的提示信息,以及是否真正加载用户数据。这个牵扯到缜密的逻辑分析,通过KVO可以时刻的获得表格的偏移量,设定一个“刷新点”,那么状态大概有这么几种:

   <1> 正在拖动,没有触发“刷新点”

   <2> 正在拖动,触发了“刷新点”

   <3> 正在拖动,触发了“刷新点”,但是保持不松手,又拖动回了“刷新点”,此时不满足触发“刷新点”条件。

   <4> 取消拖动,松手

     A、在“刷新点”前松手

     B、在“刷新点”后松手    

 

(7)根据上面的几种状态,笔者分析,先分成两大类:1、拖拽中 ,2、没拖拽 ,这里考虑UIScrollView有一个属性:dragging,可以用来做判断。然后,拖拽中又可以分成两个状态:1、满足“刷新点” 2、不满足“刷新点”。至于取消拖动,我们关注的是它是否可以加载数据就好了。

 

(8)综上,我们设定三个状态:Normal、Pulling、Loading。一个状态对应控件的一个显示界面,比如“下拉刷新”、“释放加载”、“正在加载”:

   <1>Normal : 对应(6)中的<1><2>,以及<4>里面的A

   <2>Pulling : 对应 (6)中的 <2>

   <3>Loading : 对应 (6)中的<4> 里面的 B

 

(9)有了状态的设定思路,我们可以在刷新控件中定义一个枚举属性,用来表示三种状态,接着可以在KVO的监测方法里写代码了,根据KVO检测到的scrollview的偏移,设定我们“刷新的状态”。不同的刷新状态,有着不同的文字和动画效果。

 

(10)注意,关于状态属性,我们还有其他可以用的地方,比如,满足加载数据的条件时,控件设定进入Loading状态,此时控件会一直不停的加载数据(轻微的拖动后仍然满足加载条件),这样不合理,我们只需要加载一次。所以,我们要判断,当控件的状态不是“Loading”的时候,满足条件再去加载数据(因为加载数据的时候就设定成Loading了,之后就不会再满足下载条件了)

 

(11)关于提示框中箭头的旋转,使用基本动画实现,包括加载内容时,旋转的进度轮。这里面要注意layer动画实现后其真实frame的问题,旋转的时候要写对角度位置。

 

(12)说了这么多。。。还是上代码吧

 

四、源码

 

1、.h文件

@interface ZQRefresher : UIControl

//定义回调block类型

typedef void (^success)();

/**
 *  结束刷新动作,包括动画,和隐藏刷新控件
 */
-(void)endRefreshing;

/**
 *  类方法实例化控件
 *
 *  @param successBlock 下拉刷新回调的方法,可以在这里实现加载新的数据等业务逻辑
 *
 *  @return
 */
+(instancetype)refreshWithBlock: (success)successBlock;

 

2、.m文件中的属性

 

typedef enum{
    Normal,
    Pulling,
    Loading
}ZQDragingState;

//下拉刷新的回调block,可以再这里实现加载数据的业务逻辑
@property(nonatomic,copy) void(^successCallBack)();

//用来记录父控制器视图
@property(nonatomic,weak) UIScrollView * scrollView;

@property(nonatomic,strong) UILabel * textLabel;

@property(nonatomic,strong) UIImageView * iconImageView;

//定义状态属性
@property(nonatomic,assign) ZQDragingState status;

//是否下拉动画在进行
@property(nonatomic,assign) BOOL isAnimate;

//箭头、进度轮的动画
@property(nonatomic,strong) CABasicAnimation * animation;

@end

 

3、.m文件中的方法实现

 

  1 @implementation ZQRefresher
  2 
  3 //类方法初始化
  4 +(instancetype)refreshWithBlock: (success)successBlock {
  5     ZQRefresher * refresher = [[self alloc]init];
  6     refresher.successCallBack = successBlock ;
  7     [refresher setupUIs];
  8     return refresher;
  9 }
 10 
 11 -(void)setupUIs
 12 {
 13     self.backgroundColor = [UIColor redColor];
 14     [self addSubview:self.textLabel];
 15     
 16     [self addSubview:self.iconImageView];
 17     //固定刷新控件的大小,和位置
 18     self.frame = CGRectMake(0, -35, [UIScreen mainScreen].bounds.size.width, 35);
 19     CGFloat middleX = [UIScreen mainScreen].bounds.size.width/2;
 20     
 21     //设置箭头的frame
 22     self.iconImageView.frame = CGRectMake(middleX-50,3, self.iconImageView.frame.size.width, self.iconImageView.frame.size.height);
 23     //设置文字的frame
 24     self.textLabel.frame = CGRectMake(middleX-15, 10, self.textLabel.frame.size.width, self.textLabel.frame.size.height);
 25 
 26 }
 27 
 28 
 29 //获得父控件
 30 -(void)willMoveToSuperview:(UIView *)newSuperview{
 31 
 32     [super willMoveToSuperview:newSuperview];
 33 
 34     if ([newSuperview isKindOfClass:[UIScrollView class]]) {
 35         
 36         self.scrollView = (UIScrollView *)newSuperview;
 37         //给scrollView添加监听
 38         [self.scrollView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil];
 39     }
 40 }
 41 
 42 
 43 #pragma mark --- 根据状态值,显示不同文字,以及箭头的动画
 44 -(void)setStatus:(ZQDragingState)status
 45 {
 46     _status = status;
 47    switch (status) {
 48         
 49         case 0 :
 50             {
 51                 self.textLabel.text = @"下拉刷新";
 52                 self.iconImageView.image = [UIImage imageNamed:@"tableview_pull_refresh"];
 53                 
 54                 if (self.isAnimate) {
 55                     
 56                     [self circleRun:@"Normal"];
 57                     self.isAnimate = NO;
 58                     
 59                 }
 60         
 61                 break;
 62             }
 63         case 1 :
 64             {
 65                 self.textLabel.text = @"释放加载";
 66         
 67                 if (!self.isAnimate){
 68             
 69                     [self circleRun:@"Pulling"];
 70                     
 71                 }
 72                 
 73                 self.iconImageView.image = [UIImage imageNamed:@"tableview_pull_refresh"];
 74         
 75                 break;
 76             }
 77         case 2 :
 78             {
 79                self.textLabel.text = @"加载中.....";
 80                 self.iconImageView.image = [UIImage imageNamed:@"tableview_loading"];
 81                 [self circleRun:@"Loading"];
 82         
 83                 break;
 84         }
 85     }
 86 }
 87 
 88 
 89 //根据tag不同,执行不同的动画
 90 -(void)circleRun : (NSString *)tag {
 91     
 92     self.isAnimate = YES;
 93     
 94     if ([tag isEqualToString:@"Loading"]){
 95         self.animation.duration = 1;
 96         self.animation.repeatCount = MAXFLOAT;
 97         self.animation.toValue = @(M_PI * 2);
 98         [self.iconImageView.layer addAnimation:self.animation forKey:@"Loading"];
 99         
100     }
101     
102     
103     else if ([tag isEqualToString:@"Pulling"]){
104         
105         self.animation.duration = 0.3;
106         self.animation.repeatCount = 1;
107         self.animation.removedOnCompletion = NO;
108         self.animation.fillMode = kCAFillModeForwards;
109         self.animation.toValue = @(M_PI);
110         [self.iconImageView.layer addAnimation:self.animation forKey:@"Pull"];
111 
112         
113     }
114     else if ([tag isEqualToString:@"Normal"]){
115         self.animation.duration = 0.3;
116         self.animation.repeatCount = 1;
117         self.animation.removedOnCompletion = NO;
118         self.animation.fillMode = kCAFillModeForwards;
119         
120         //这一步很重要,因为旋转的起始固定的,这是旋转到M_PI*2 的位置
121         self.animation.toValue = @(M_PI * 2);
122         [self.iconImageView.layer addAnimation:self.animation forKey:@"Normal"];
123         
124     }
125     
126 }
127 
128 -(void)endRefreshing
129 {
130     [UIView animateWithDuration:0.5 animations:^{
131         self.scrollView.contentInset = UIEdgeInsetsMake(64, 0, 0, 0);
132         [self circleStop];
133         //隐藏掉
134         self.alpha = 0;
135     }];
136 }
137 
138 
139 -(void)circleStop
140 {
141     [self.iconImageView.layer removeAllAnimations];
142     self.isAnimate = NO;
143 }
144 
145 
146 
147 -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{
148 
149 
150     self.alpha = 1;
151     self.scrollView = (UIScrollView *)object;
152     //监听到contentOffset的变化后,再进行状态判断
153     //如果正在拖拽,并且contentOffset 小于0
154     if ( self.scrollView.dragging && self.scrollView.contentOffset.y < 0 ) {
155         //1、没有拖拽到指定位置时
156         if (self.scrollView.contentOffset.y > -120 ){
157             //这是正常状态
158             self.status = Normal;
159         }
160         
161         //2、拖动到超过指定位置,并且还在拖拽并没有松手
162         else if (self.scrollView.contentOffset.y <= -120 ){
163             //现在是拖拽状态了
164             self.status = Pulling;
165         }
166     }
167     //如果没有拖拽
168     else{
169         //不能重复加载数据,所以判断一下,如果是loading,就不用再加载数据了
170         if(self.scrollView.contentOffset.y <= -120 && self.status != Loading){
171             //更改为loading状态
172             self.status = Loading;
173             //让刷新器停留在导航栏下面
174             [UIView animateWithDuration:0.5 animations:^{
175                 self.scrollView.contentInset = UIEdgeInsetsMake(99, 0, 0, 0);
176             } completion:^(BOOL finished) {
177                 self.successCallBack();
178             }];
179         }
180     }
181 }
182 
183 
184 #pragma mark - 懒加载
185 -(UILabel *)textLabel
186 {
187     
188     if (!_textLabel) {
189         
190         _textLabel = [[UILabel alloc]init];
191         _textLabel.text = @"下拉刷新";
192         _textLabel.font = [UIFont systemFontOfSize:13];
193         [_textLabel sizeToFit];
194         _textLabel.textColor = [UIColor orangeColor];
195       
196     }
197     
198     return _textLabel;
199 }
200 -(UIImageView *)iconImageView
201 {
202     if (!_iconImageView){
203         _iconImageView = [[UIImageView alloc]init];
204         _iconImageView.image = [UIImage imageNamed:@"tableview_pull_refresh"];
205         [_iconImageView sizeToFit];
206         
207     }
208     return _iconImageView;
209 }
210 
211 -(CABasicAnimation *)animation
212 {
213     if (!_animation) {
214         _animation= [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
215     }
216     return _animation;
217 }
218 
219 @end

 

五、最后

 

1、源码的注释比较详细,拖过来就能用,swift做个桥接一样好使,使用起来比较方便,轻量级。

 

2、笔者封装的这个下拉控件,只是具备简单的功能,不过借助这个设计理念,控件的功能是可以得到很好的扩展的。iOS的学习者拿来做分析案例,也是不错的。

 

自定义下拉刷新控件

标签:

原文地址:http://www.cnblogs.com/cleven/p/5389313.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!