zoukankan      html  css  js  c++  java
  • 自定义下拉刷新控件

    一、功能效果

    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的学习者拿来做分析案例,也是不错的。

  • 相关阅读:
    深入理解jvm分享培训pdf(转载) 老李
    innobackupex自动备份脚本(增量备份,自动压缩)
    多线程调用生成主键流水号存储过程产生主键冲突问题解决方案
    mysql 5.7新数据库sys解析(一)
    根据日期累加金额的mysql
    mysql字符串分割函数(行转列)
    使用innobackupex备份mysql数据库
    css学习inlineblock详解及dispaly:inline inlineblock block 三者区别精要概括
    html良好结构之豆瓣风格
    HTML5学习笔记html5与传统html区别
  • 原文地址:https://www.cnblogs.com/cleven/p/5389313.html
Copyright © 2011-2022 走看看