• MJRefresh源码框架分析


    MJRefresh是一款非常优秀的刷新控件。代码简洁,优雅。今天有时间对源代码阅读了一下。对MJRefresh的宏观设计非常赞叹。所谓大道至简就是这样吧。
     
    MJRefresh所采用的主要设计模式非常简单,是类继承 + 模版方法设计模式。
    所以子类也主要围绕着这几个模版方法和继承方法进行定制行为的。
     
    模版方法设计模式:
    由父类MJRefreshComponent定义方法接口并添加到执行步骤中,对象执行中,在特定时间一定会调用的方法。由子类在需要的时候进行自定义实现。
    在MJRefreshComponent类中的重要模版方法如下:
    [self prepare];//在父类initWithFrame方法调用
    [self placeSubviews];//在父类layoutSubviews方法调用

    类继承:父类定义了方法的基本实现,子类在此基础上进行持续增加,达到复杂功能。与模版方法的区别是没有固定的执行步骤。

    在MJRefreshComponent类中的重要继承方法如下:
    //状态设置
    - (void)setState:(MJRefreshState)state
    //事件监听
    - (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{}
    - (void)scrollViewContentSizeDidChange:(NSDictionary *)change{}
    - (void)scrollViewPanStateDidChange:(NSDictionary *)change{}
    MJRefresh作为刷新组件,核心逻辑根据ScrollView的Offset不同更新相应的状态和数据,
    根据方法名字应该是MJRefreshComponent类中的重要继承方法:
    - (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{}
     
    下面看一下其子类MJRefreshHeader对这个方法的实现:
    MJRefreshHeader是父类MJRefreshComponent的子类,其方法声明结构如下:

    红框内是主要实现代码应该就是这四个“覆盖父类方法”了
     
    子类MJRefreshHeader的两个模版方法实现如下:
     
    - (void)prepare
    {
        [super prepare];
        
        // 设置key
        self.lastUpdatedTimeKey = MJRefreshHeaderLastUpdatedTimeKey;
        
        // 设置高度
        self.mj_h = MJRefreshHeaderHeight;
    }
    
    - (void)placeSubviews
    {
        [super placeSubviews];
        
        // 设置y值(当自己的高度发生改变了,肯定要重新调整Y值,所以放到placeSubviews方法中设置y值)
        self.mj_y = - self.mj_h - self.ignoredScrollViewContentInsetTop;
    }
    方法prepare:设置属性值
    方法placeSubviews:更新UI布局
    子类填充后,父类按照约定的步骤时机执行。over!
     
    子类MJRefreshHeader的覆盖方法实现如下:
     
    - (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
    {
        [super scrollViewContentOffsetDidChange:change];
        
        // 在刷新的refreshing状态
        if (self.state == MJRefreshStateRefreshing) {
            // 暂时保留
            //My:当NavigationBar从一个页面滑出时,可能被移除页面,其window为nil
            if (self.window == nil) return;
            
            // sectionheader停留解决
            //My:当scrollView向下偏移的距离超过它的contentInset的上间隔时,取距离大的
            CGFloat insetT = - self.scrollView.mj_offsetY > _scrollViewOriginalInset.top ? - self.scrollView.mj_offsetY : _scrollViewOriginalInset.top;
            //My:当这个距离超过了(刷新控件的高度 + 它的contentInset的上间隔)时,取它们的和值
            insetT = insetT > self.mj_h + _scrollViewOriginalInset.top ? self.mj_h + _scrollViewOriginalInset.top : insetT;
            //My:将这个合理的最大值,设置到它的contentInset的上间隔上。
            self.scrollView.mj_insetT = insetT;
            //My:实际露出的刷新空间高
            self.insetTDelta = _scrollViewOriginalInset.top - insetT;
            return;
        }
        
        // 跳转到下一个控制器时,contentInset可能会变
        _scrollViewOriginalInset = self.scrollView.mj_inset;
        
        // 当前的contentOffset
        CGFloat offsetY = self.scrollView.mj_offsetY;
        // 头部控件刚好出现的offsetY
        CGFloat happenOffsetY = - self.scrollViewOriginalInset.top;
        
        // 如果是向上滚动到看不见头部控件,直接返回
        // >= -> >
        if (offsetY > happenOffsetY) return;
        
        // 普通 和 即将刷新 的临界点
        //My:下拉距离正好是(刷新控件高度+contentInset的上间隔)
        CGFloat normal2pullingOffsetY = happenOffsetY - self.mj_h;
        //My:露出的高度/总高度
        CGFloat pullingPercent = (happenOffsetY - offsetY) / self.mj_h;
        
        if (self.scrollView.isDragging) { // 如果正在拖拽
            self.pullingPercent = pullingPercent;
           
            if (self.state == MJRefreshStateIdle && offsetY < normal2pullingOffsetY) {
                 //My:下拉度超过临界值
    
                // 转为即将刷新状态
                self.state = MJRefreshStatePulling;
            } else if (self.state == MJRefreshStatePulling && offsetY >= normal2pullingOffsetY) {
                //My:下拉度小于临界值
    
                // 转为普通状态
                self.state = MJRefreshStateIdle;
            }
        } else if (self.state == MJRefreshStatePulling) {
            // 即将刷新 && 手松开
    
            // 开始刷新
            [self beginRefreshing];
        } else if (pullingPercent < 1) {
            self.pullingPercent = pullingPercent;
        }
    }
    该方法会随着ScrollView的滚动,其Offset会不断更新,此方法不不断被触发。
    操作步骤大概思路是:
    1.如果当前处于刷新状态,offset的改变时,设置scrollView的offset为(刷新控件的高度 + 它的contentInset的上间隔)。
    2.否则的话,如果处于拖拽时,根据拖拽距离和当前控件状态,更新下一步控件的状态。
    详细描述见上面的注释。
     
    带有NavigationBar的UIScrollView,默认它的offset = {0, -64}; 默认它的contentInset = {64,0,0,0}
    内容展示部分刚好在NavigationBar的下面
     
    子类MJRefreshHeader的状态设置后,会调用如下方法,刷新控件的UI:
     
    - (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) {
            MJRefreshDispatchAsyncOnMainQueue({
                [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
                    if (self.scrollView.panGestureRecognizer.state != UIGestureRecognizerStateCancelled) {
                        CGFloat top = self.scrollViewOriginalInset.top + self.mj_h;
                        // 增加滚动区域top
                        self.scrollView.mj_insetT = top;
                        // 设置滚动位置
                        CGPoint offset = self.scrollView.contentOffset;
                        offset.y = -top;
                        [self.scrollView setContentOffset:offset animated:NO];
                    }
                } completion:^(BOOL finished) {
                    [self executeRefreshingCallback];
                }];
            })
        }
    }
    宏MJRefreshCheckState:检查旧状态与新状态是否一致,一致的话就返回。
    从刷新转普通状态时:
    保存刷新时间,调整菊花透明度,移动offset
    转换成刷新状态时:
    设置contentInset.top,设置offset
     
    逻辑主干是上面的四个方法,其他的逻辑枝叶,想自己研究的话可以翻看源代码。
     
     
  • 相关阅读:
    肥胖儿筛选标准
    文章索引
    面向对象66原则
    [精]Xpath路径表达式
    [精]XPath入门教程
    孕产期高危因素
    “华而不实”的转盘菜单(pie menu)
    xmind用例导excel用例,然后再用python排版
    NSObject
    [self class]与[super class]
  • 原文地址:https://www.cnblogs.com/zhou--fei/p/10344544.html
Copyright © 2020-2023  润新知