一、功能效果
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