IOS开发入门iOS: MJRefresh源码分析
白羽 2019-05-21 来源 :网络 阅读 648 评论 0

摘要:本文将带你了解IOS开发入门iOS: MJRefresh源码分析,希望本文对大家学IOS有所帮助。

    本文将带你了解IOS开发入门iOS: MJRefresh源码分析,希望本文对大家学IOS有所帮助。


IOS开发入门iOS: MJRefresh源码分析


MJRefresh代码的核心思想
   
   上图为MJRefresh项目的项目结构
   在MJRefresh中,使用了KVO、runtime、继承、GCD等知识
   核心思想
   –
   MJRefreshComponent是刷新控件的基类,在MJRefreshComponent添加了KVO监听、prepare方法和placeSubviews方法。
   当MJRefreshComponent中KVO监听到之后,响应会在MJRefreshHeader和MJRefreshFooter中实现,MJRefreshHeader和MJRefreshFooter其实响应KVO方法,主要就是设置state状态,然后在他们的子类中会分别调用setState方法,根据不同的state状态进行不同的变化
   prepare方法和placeSubviews方法。prepare是设置刷新控件,包括文字、gif图片、风格等等;placeSubviews是调整刷新控件的子控件的位置。在MJRefreshComponent的每一个子类中都会先调用父类对应方法,然后根据自身的特点进行不同实现
   MJRefresh分析
   上一篇分析了MJRefresh的框架结构和核心思想,现在选择最简单的一个分支来进行分析。
   MJRefreshNormalHeader -> MJRefreshStateHeader -> MJRefreshHeader  -> MJRefreshComponent
   一般开始使用MJRefresh的时候,往往都是几行代码就调用了,例如:
   MJRefreshNormalHeader *header = [MJRefreshNormalHeader  headerWithRefreshingBlock:^{    [self  reloadData];    [self.tableView.mj_header  endRefreshing];}];self.tableView.mj_header = header;
   现在来看看,上面的几行代码到底经历着怎样的实现?
   上面的代码,是创建了一个MJRefreshNormalHeader的对象,然后将它赋给了self.tableView.mj_header,mj_header是什么呢?然后找到UIScrollView+MJRefresh.h文件,可以看到这是一个分类
   #import#import "MJRefreshConst.h" @class  MJRefreshHeader, MJRefreshFooter; @interface UIScrollView (MJRefresh)/**  下拉刷新控件 */@property (strong, nonatomic) MJRefreshHeader *mj_header;@property  (strong, nonatomic) MJRefreshHeader *header  MJRefreshDeprecated("使用mj_header");/** 上拉刷新控件 */@property (strong,  nonatomic) MJRefreshFooter *mj_footer;@property (strong, nonatomic)  MJRefreshFooter *footer  MJRefreshDeprecated("使用mj_footer"); #pragma mark - other-  (NSInteger)mj_totalDataCount;@property (copy, nonatomic) void (^mj_reloadDataBlock)(NSInteger  totalDataCount);@end
   作者利用runtime的技巧给这个分类添加了五个属性和一个方法,然后将封装好的刷新控件添加给UIScrollview
     (void)setMj_header:(MJRefreshHeader *)mj_header{    if  (mj_header != self.mj_header) {    //  删除旧的,添加新的    [self.mj_header  removeFromSuperview];    [self insertSubview:mj_header  atIndex:0];     // 存储新的    [self  willChangeValueForKey:@"mj_header"]; //  KVO    objc_setAssociatedObject(self,  &MJRefreshHeaderKey,                             mj_header,  OBJC_ASSOCIATION_ASSIGN);    [self  didChangeValueForKey:@"mj_header"]; //  KVO    }} - (void)setMj_footer:(MJRefreshFooter  *)mj_footer{    if (mj_footer != self.mj_footer)  {    // 删除旧的,添加新的    [self.mj_footer  removeFromSuperview];    [self insertSubview:mj_footer  atIndex:0];     // 存储新的    [self  willChangeValueForKey:@"mj_footer"]; //  KVO    objc_setAssociatedObject(self,  &MJRefreshFooterKey,                             mj_footer,  OBJC_ASSOCIATION_ASSIGN);    [self  didChangeValueForKey:@"mj_footer"]; // KVO    }}
   所以其实现在可以理解,self.tableView.mj_header =  header;其实就是给tableview添加一个头部的刷新控件.而增加的属性MJRefreshHeader就是刚才创建的MJRefreshNormalHeader的基类。MJRefreshHeader继承于MJRefreshComponent,  MJRefreshComponent是整个刷新控件的基类。
   创建了MJRefreshNormalHeader的对象,直接调用了一个类方法headerWithRefreshingBlock,这个方法是它父类MJRefreshHeader的一个方法
  “MJRefreshHeader.m”文件+  (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock{    MJRefreshHeader  *cmp = [[self alloc] init];    cmp.refreshingBlock =  refreshingBlock;    return cmp;}
   此方法是为了创建一个MJRefreshHeader对象,在创建对象init的时候,又会调用到MJRefreshHeader的父类MJRefreshComponent的方法
   @implementation MJRefreshComponent#pragma mark - 初始化-  (instancetype)initWithFrame:(CGRect)frame{    if (self =  [super initWithFrame:frame])  {        //  准备工作        [self  prepare];         //  默认是普通状态        self.state =  MJRefreshStateIdle;    }    return  self;}
   注意:MJRefreshComponent 类中的prepare方法,会被它的子类都进行调用,每个字类的prepare方法,都会调用父类中的prepare方法,然后增加自己特有的执行操作。
   执行完init方法,最后会返回一个MJRefreshNormalHeader对象,然后添加给self.scrollview,添加上去后,便会开始执行MJRefreshComponent中的-  (void)willMoveToSuperview:(UIView *)newSuperview方法
   - (void)willMoveToSuperview:(UIView  *)newSuperview{    [super  willMoveToSuperview:newSuperview];     //  如果不是UIScrollView,不做任何事情    if (newSuperview &&  ![newSuperview isKindOfClass:[UIScrollView class]])  return;     //  旧的父控件移除监听    [self  removeObservers];     if (newSuperview) { //  新的父控件        //  设置宽度        self.mj_w =  newSuperview.mj_w;        //  设置位置        self.mj_x =  0;         //  记录UIScrollView        _scrollView =  (UIScrollView  *)newSuperview;        //  设置永远支持垂直弹簧效果        _scrollView.alwaysBounceVertical  = YES;        //  记录UIScrollView最开始的contentInset        _scrollViewOriginalInset  =  _scrollView.contentInset;         //  添加监听        [self  addObservers];    }}
   监听了三个值,分别是UIScrollView的ContentOffSet、ContentSize、滑动手势的状态
   #pragma mark - KVO监听-  (void)addObservers{    NSKeyValueObservingOptions options  = NSKeyValueObservingOptionNew |  NSKeyValueObservingOptionOld;    [self.scrollView  addObserver:self forKeyPath:MJRefreshKeyPathContentOffset options:options  context:nil];    [self.scrollView addObserver:self  forKeyPath:MJRefreshKeyPathContentSize options:options  context:nil];    self.pan =  self.scrollView.panGestureRecognizer;    [self.pan  addObserver:self forKeyPath:MJRefreshKeyPathPanState options:options  context:nil];}
   利用KVO监听到之后,都会响应相应的didChange方法,比如下拉刷新,下拉必然会让contentOffSet发生变化,必然会响应对应的方法:
   MJRefreshHeader文件-  (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{    [super  scrollViewContentOffsetDidChange:change];     //  在刷新的refreshing状态    if (self.state ==  MJRefreshStateRefreshing) {        if  (self.window == nil)  return;         //  sectionheader停留解决        CGFloat  insetT = - self.scrollView.mj_offsetY > _scrollViewOriginalInset.top ? -  self.scrollView.mj_offsetY : _scrollViewOriginalInset.top;        insetT  = insetT > self.mj_h + _scrollViewOriginalInset.top ? self.mj_h +  _scrollViewOriginalInset.top :  insetT;        self.scrollView.mj_insetT  =  insetT;         self.insetTDelta  = _scrollViewOriginalInset.top -  insetT;        return;    }     //  跳转到下一个控制器时,contentInset可能会变    _scrollViewOriginalInset =  self.scrollView.contentInset;     //  当前的contentOffset    CGFloat offsetY =  self.scrollView.mj_offsetY;    // 头部控件刚好出现的offsetY    CGFloat  happenOffsetY = -  self.scrollViewOriginalInset.top;     //  如果是向上滚动到看不见头部控件,直接返回    // >= ->  >    if (offsetY > happenOffsetY)  return;     // 普通 和 即将刷新  的临界点    CGFloat normal2pullingOffsetY = happenOffsetY -  self.mj_h;    CGFloat pullingPercent = (happenOffsetY -  offsetY) / self.mj_h;     if (self.scrollView.isDragging)  { //  如果正在拖拽        self.pullingPercent =  pullingPercent;        if (self.state  == MJRefreshStateIdle && offsetY < normal2pullingOffsetY)  {            //  转为即将刷新状态            self.state  = MJRefreshStatePulling;        }  else if (self.state == MJRefreshStatePulling && offsetY >=  normal2pullingOffsetY)  {            //  转为普通状态            self.state  = MJRefreshStateIdle;        }    }  else if (self.state == MJRefreshStatePulling) {// 即将刷新 &&  手松开        //  开始刷新        [self beginRefreshing];    }  else if (pullingPercent < 1)  {        self.pullingPercent =  pullingPercent;    }}
   上面的其实就是根据拖动的时候,scrollview的contentOffSet的变化进行state的设置:临界点就是scrollView的Inset.top与刷新控件的高度相加的值。进行相应的操作,然后更改state,在每一次更改state的时候,就发生了哪些变化呢,看看下面的方法
   MJRefreshHeader文件-  (void)setState:(MJRefreshState)state{ MJRefreshCheckState     //  根据状态做事情    if (state == MJRefreshStateIdle)  {        if (oldState !=  MJRefreshStateRefreshing)  return;         //  保存刷新时间        [[NSUserDefaults  standardUserDefaults] setObject:[NSDate date]  forKey:self.lastUpdatedTimeKey];        [[NSUserDefaults  standardUserDefaults] synchronize];         //  恢复inset和offset        [UIView  animateWithDuration:MJRefreshSlowAnimationDuration  animations:^{            self.scrollView.mj_insetT  +=  self.insetTDelta;             //  自动调整透明度            if  (self.isAutomaticallyChangeAlpha) self.alpha = 0.0;    }  completion:^(BOOL finished)  {            self.pullingPercent  =  0.0;             if  (self.endRefreshingCompletionBlock)  {                self.endRefreshingCompletionBlock();            }        }];    }  else if (state == MJRefreshStateRefreshing)  {        dispatch_async(dispatch_get_main_queue(),  ^{            [UIView  animateWithDuration:MJRefreshFastAnimationDuration  animations:^{                CGFloat  top = self.scrollViewOriginalInset.top + self.mj_h;                //  增加滚动区域top                self.scrollView.mj_insetT  = top;                //  设置滚动位置                [self.scrollView  setContentOffset:CGPointMake(0, -top) animated:NO];        }  completion:^(BOOL finished)  {                [self  executeRefreshingCallback];            }];        });    }}
   执行setState方法的时候,进行了界面的操作。如果是正常状态的时候,恢复inset和offset;如果是刷新状态,那就设置inset和offset,将scrollview的视图往下挤一点。
   再看看MJRefreshNormalHeader文件的实现
   MJRefreshNormalHeader文件#pragma  mark - 重写父类的方法- (void)prepare{    [super  prepare];     self.activityIndicatorViewStyle =  UIActivityIndicatorViewStyleGray;} -  (void)placeSubviews{    [super  placeSubviews];     //  箭头的中心点    CGFloat arrowCenterX = self.mj_w *  0.5;    if (!self.stateLabel.hidden)  {        CGFloat stateWidth =  self.stateLabel.mj_textWith;        CGFloat  timeWidth = 0.0;        if  (!self.lastUpdatedTimeLabel.hidden)  {            timeWidth  =  self.lastUpdatedTimeLabel.mj_textWith;        }        CGFloat  textWidth = MAX(stateWidth,  timeWidth);        arrowCenterX -=  textWidth / 2 +  self.labelLeftInset;    }    CGFloat  arrowCenterY = self.mj_h * 0.5;    CGPoint arrowCenter =  CGPointMake(arrowCenterX, arrowCenterY);     //  箭头    if (self.arrowView.constraints.count == 0)  {        self.arrowView.mj_size =  self.arrowView.image.size;        self.arrowView.center  = arrowCenter;    }     //  圈圈    if (self.loadingView.constraints.count == 0)  {        self.loadingView.center =  arrowCenter;    }     self.arrowView.tintColor  = self.stateLabel.textColor;} - (void)setState:(MJRefreshState)state{    MJRefreshCheckState     //  根据状态做事情    if (state == MJRefreshStateIdle)  {        if (oldState ==  MJRefreshStateRefreshing) {            self.arrowView.transform  =  CGAffineTransformIdentity;             [UIView  animateWithDuration:MJRefreshSlowAnimationDuration animations:^{                self.loadingView.alpha  =  0.0;            }  completion:^(BOOL finished) {                //  如果执行完动画发现不是idle状态,就直接返回,进入其他状态                if  (self.state != MJRefreshStateIdle) return;                 self.loadingView.alpha  =  1.0;                [self.loadingView  stopAnimating];                self.arrowView.hidden  =  NO;            }];        }  else  {            [self.loadingView  stopAnimating];            self.arrowView.hidden  = NO;            [UIView  animateWithDuration:MJRefreshFastAnimationDuration  animations:^{                self.arrowView.transform  = CGAffineTransformIdentity;         }];        }    }  else if (state == MJRefreshStatePulling)  {        [self.loadingView  stopAnimating];        self.arrowView.hidden  = NO;        [UIView  animateWithDuration:MJRefreshFastAnimationDuration  animations:^{            self.arrowView.transform  = CGAffineTransformMakeRotation(0.000001 -  M_PI);        }];    }  else if (state == MJRefreshStateRefreshing)  {        self.loadingView.alpha =  1.0; // 防止refreshing ->  idle的动画完毕动作没有被执行        [self.loadingView  startAnimating];        self.arrowView.hidden  = YES;    }}
   上面的placeSubviews方法设置了刷新控件的子控件的位置以及大小,然后setState方法就是更加具体的根据不同state来进行界面的变换:当state由刷新变为正常时,停止loadingView的动画,显示箭头;当state状态为Pulling的时候,箭头会发生变化,转个方向;当state为刷新时,loadingView开始动画,隐藏箭头。    

本文由职坐标整理并发布,希望对同学们有所帮助。了解更多详情请关注职坐标移动开发之IOS频道!

本文由 @白羽 发布于职坐标。未经许可,禁止转载。
喜欢 | 0 不喜欢 | 0
看完这篇文章有何感觉?已经有0人表态,0%的人喜欢 快给朋友分享吧~
评论(0)
后参与评论

您输入的评论内容中包含违禁敏感词

我知道了

助您圆梦职场 匹配合适岗位
验证码手机号,获得海同独家IT培训资料
选择就业方向:
人工智能物联网
大数据开发/分析
人工智能Python
Java全栈开发
WEB前端+H5

请输入正确的手机号码

请输入正确的验证码

获取验证码

您今天的短信下发次数太多了,明天再试试吧!

提交

我们会在第一时间安排职业规划师联系您!

您也可以联系我们的职业规划师咨询:

小职老师的微信号:z_zhizuobiao
小职老师的微信号:z_zhizuobiao

版权所有 职坐标-一站式IT培训就业服务领导者 沪ICP备13042190号-4
上海海同信息科技有限公司 Copyright ©2015 www.zhizuobiao.com,All Rights Reserved.
 沪公网安备 31011502005948号    

©2015 www.zhizuobiao.com All Rights Reserved

208小时内训课程