MJRefresh

基本原理:

通過header view和 foot view 添加對scrollview contentOffset屬性 kvo 實現(xiàn)動態(tài)刷新牲蜀。在runtime對scrollview添加header的時候茵宪,在view的willMoveToSuperView生命周期方法中進行注冊峡碉。

需要記憶理解的地方:

1.scrollview往上滑動的時候,contentOffsize的y是正數(shù)甲喝。scrollview往下滑動的時候厢破,contentOffsize的y是負數(shù)。

2.header和footer的位置和大小是在view的layoutSubviews生命周期方法里設置的贾虽。

3.通過設置header或者footer的狀態(tài)的時候控制其行為

小技巧:

1.在子類中調用了父類的方法,如果要求子類在其實現(xiàn)中必須調用父類的方法吼鱼,可以在方法聲明的時候添加 NS_REQUIRES_SUPER 宏蓬豁,編譯器在編譯過程可進行檢查。

代碼:

1.MJRefreshNormalHeader

在初始化的時候,會調用init方法蛉抓,在Init方法里庆尘,系統(tǒng)默認調用了initWithFrame方法,frame傳的CGRectZero.在MJRefreshNormalHeader的父類

- (instancetype)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame]) {
 
        // 準備工作
 
        [self prepare];
 
        // 默認是普通狀態(tài)
 
        self.state = MJRefreshStateIdle;
    }
    return self;
}

prepare方法設置了header的寬高和x的位置:

- (void)prepare
{
    [super prepare];  
 
    // 設置key
 
    self.lastUpdatedTimeKey = MJRefreshHeaderLastUpdatedTimeKey;
 
    // 設置高度
 
    self.mj_h = MJRefreshHeaderHeight;
}

設置state屬性方法:

- (void)setState:(MJRefreshState)state
{
//這個宏主要是來以下的邏輯只有是在狀態(tài)發(fā)生變化的時候才執(zhí)行巷送,避免重復的邏輯。
    MJRefreshCheckState
    // 根據(jù)狀態(tài)做事情
 
 
//從其他狀態(tài)變成初始靜止的狀態(tài)
    if (state == MJRefreshStateIdle) {
 
     //這里相當于僅用于處理從刷新中的狀態(tài)轉化成靜止狀態(tài)
        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) {
//這個屬性主要是用來控制隨著下拉的距離來反應到header的透明度
            self.pullingPercent = 0.0;
 
        }];
 
    } else if (state == MJRefreshStateRefreshing) {
 
        [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^
        {
 
            // 增加滾動區(qū)域
 
            CGFloat top = self.scrollViewOriginalInset.top + self.mj_h;
 
            self.scrollView.mj_insetT = top;
 
             
            // 設置滾動位置
 
            self.scrollView.mj_offsetY = - top;
 
        } completion:^(BOOL finished) {
//業(yè)務邏輯層的刷新數(shù)據(jù)
            [self executeRefreshingCallback];
 
        }];
 
    }
 
}

根據(jù)拖拽進度設置透明度:

- (void)setPullingPercent:(CGFloat)pullingPercent
 
{
    _pullingPercent = pullingPercent;
 
    if (self.isRefreshing) return;
 
    if (self.isAutomaticallyChangeAlpha) {
 
        self.alpha = pullingPercent;
    }
}

在UIScrollVIEW+MJRefresh中的setMj_header方法矛辕,給UIScrollView動態(tài)的添加了一個header笑跛,并將它加到了ScrollView上。

- (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
    }
}

將header加到ScrollView上的時候飞蹂,系統(tǒng)會調用willMoveToSuperview方法,將header從ScrollView刪除的時候也會調用這個方法翻屈,區(qū)別是刪除的時候參數(shù)傳的nil陈哑,添加的時候參數(shù)為當前scrollView.在這個方法中設置了header的x位置以及header的寬度,并且添加了對scrollview的監(jiān)聽

- (void)willMoveToSuperview:(UIView *)newSuperview
{
    [super willMoveToSuperview:newSuperview];
    // 如果不是UIScrollView,不做任何事情
    if (newSuperview && ![newSuperview isKindOfClass:[UIScrollView class]]) 
    return;
 
    // 舊的父控件移除監(jiān)聽
    [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;
 
        // 添加監(jiān)聽
        [self addObservers];
    }
}

添加監(jiān)聽的方法:

- (void)addObservers
{
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | 
    NSKeyValueObservingOptionOld;
 
 
//監(jiān)測滑動距離惊窖,判斷是否到達需要刷新的程度
    [self.scrollView addObserver:self forKeyPath:MJRefreshKeyPathContentOffset 
    options:options context:nil];
//檢測contentSize變化刽宪,比如上拉加載更多,動態(tài)改變footer的位置
    [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];
}

接下來界酒,系統(tǒng)會開始調用layoutSubViews方法:在MJRefreshComponent中重寫了這個方法

- (void)layoutSubviews
{
    [super layoutSubviews];
    [self placeSubviews];
}
  
//placeSubviews方法是在各個MJRefreshComponent的子類中實現(xiàn)的圣拄,
//繼承鏈:MJRefreshNormalHeader -> MJRefreshStateHeader -> MJRefreshHeader -> MJRefreshComponent
  
//以下是MJRefreshHeader中的實現(xiàn),設置了header的y的位置
- (void)placeSubviews
{
    [super placeSubviews];
    // 設置y值(當自己的高度發(fā)生改變了,肯定要重新調整Y值毁欣,
    //所以放到placeSubviews方法中設置y值)
    self.mj_y = - self.mj_h - self.ignoredScrollViewContentInsetTop;
}
  
//以下是MJRefreshStateHeader中的實現(xiàn),添加刷新狀體和更新時間
- (void)placeSubviews
{
    [super placeSubviews];
    if (self.stateLabel.hidden) return;
 
    BOOL noConstrainsOnStatusLabel = self.stateLabel.constraints.count == 0;
 
    if (self.lastUpdatedTimeLabel.hidden) {
 
        // 狀態(tài)
 
        if (noConstrainsOnStatusLabel) self.stateLabel.frame = self.bounds;
 
    } else {
 
        CGFloat stateLabelH = self.mj_h * 0.5;
 
        // 狀態(tài)
 
        if (noConstrainsOnStatusLabel) {
 
            self.stateLabel.mj_x = 0;
 
            self.stateLabel.mj_y = 0;
 
            self.stateLabel.mj_w = self.mj_w;
 
            self.stateLabel.mj_h = stateLabelH;
        }
 
        // 更新時間
        if (self.lastUpdatedTimeLabel.constraints.count == 0) {
 
            self.lastUpdatedTimeLabel.mj_x = 0;
 
            self.lastUpdatedTimeLabel.mj_y = stateLabelH;
 
            self.lastUpdatedTimeLabel.mj_w = self.mj_w;
 
            self.lastUpdatedTimeLabel.mj_h = self.mj_h - self.lastUpdatedTimeLabel.mj_y;
        }
    }
}
  
//以下是MJRefreshNormalHeader中的實現(xiàn)庇谆,添加剪頭和loadingview
- (void)placeSubviews
{
    [super placeSubviews];
    // 箭頭的中心點
 
    CGFloat arrowCenterX = self.mj_w * 0.5;
 
    if (!self.stateLabel.hidden) {
        arrowCenterX -= 100;
    }
 
    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;
    }
}

header中監(jiān)控下拉距離的方法:

- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
    [super scrollViewContentOffsetDidChange:change];
 
    // 在刷新的refreshing狀態(tài)
 
    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;
 
    // 頭部控件剛好出現(xiàn)的offsetY
    CGFloat happenOffsetY = - self.scrollViewOriginalInset.top;
    // 如果是向上滾動到看不見頭部控件凭疮,直接返回
    if (offsetY > happenOffsetY) return;
 
    // 普通 和 即將刷新 的臨界點, self.mj_h是當前header的高度
    CGFloat normal2pullingOffsetY = happenOffsetY - self.mj_h;
    CGFloat pullingPercent = (happenOffsetY - offsetY) / self.mj_h;
 
    if (self.scrollView.isDragging) { // 如果正在拖拽
        self.pullingPercent = pullingPercent;
//header正好完全露出來饭耳,開始轉為刷新狀態(tài)
        if (self.state == MJRefreshStateIdle && offsetY < normal2pullingOffsetY
            ) {
            // 轉為即將刷新狀態(tài)
            self.state = MJRefreshStatePulling;
        } else if (self.state == MJRefreshStatePulling && offsetY >= 
            normal2pullingOffsetY) {
            // 轉為普通狀態(tài)
            self.state = MJRefreshStateIdle;
        }
    } else if (self.state == MJRefreshStatePulling) {// 即將刷新 && 手松開
        // 開始刷新
        [self beginRefreshing];
    } else if (pullingPercent < 1) {
        self.pullingPercent = pullingPercent;
    }
}

footer同header一樣都繼承自MJFfreshComponent,都自動檢測scrollview的行為执解,各自實現(xiàn)具體的被通知的邏輯

MJRefreshAutoFooter的實現(xiàn):

- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
    [super scrollViewContentOffsetDidChange:change];
    if (self.state != MJRefreshStateIdle || !self.automaticallyRefresh || self.
        mj_y == 0) return;
 
  
    if (_scrollView.mj_insetT + _scrollView.mj_contentH > _scrollView.mj_h) { 
    // 內容超過一個屏幕
 
        // 這里的_scrollView.mj_contentH替換掉self.mj_y更為合理
 
        if (_scrollView.mj_offsetY >= _scrollView.mj_contentH - _scrollView.
            mj_h + self.mj_h * self.triggerAutomaticallyRefreshPercent + 
            _scrollView.mj_insetB - self.mj_h) {
 
            // 防止手松開時連續(xù)調用
 
            CGPoint old = [change[@"old"] CGPointValue];
 
            CGPoint new = [change[@"new"] CGPointValue];
 
            if (new.y <= old.y) return;
 
            // 當?shù)撞克⑿驴丶耆霈F(xiàn)時寞肖,才刷新
 
            [self beginRefreshing];
        }
    }
}
 
- (void)scrollViewPanStateDidChange:(NSDictionary *)change
{
    [super scrollViewPanStateDidChange:change];
 
    if (self.state != MJRefreshStateIdle) return;
 
    if (_scrollView.panGestureRecognizer.state == UIGestureRecognizerStateEnded
    ) {// 手松開
        if (_scrollView.mj_insetT + _scrollView.mj_contentH <= _scrollView.mj_h
        ) {  // 不夠一個屏幕
            if (_scrollView.mj_offsetY >= - _scrollView.mj_insetT) { // 向上拽
                [self beginRefreshing];
            }
        } else { // 超出一個屏幕
            if (_scrollView.mj_offsetY >= _scrollView.mj_contentH + _scrollView
                .mj_insetB - _scrollView.mj_h) {
                [self beginRefreshing];
            }
        }
    }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
禁止轉載,如需轉載請通過簡信或評論聯(lián)系作者材鹦。
  • 序言:七十年代末逝淹,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子桶唐,更是在濱河造成了極大的恐慌栅葡,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,826評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件尤泽,死亡現(xiàn)場離奇詭異欣簇,居然都是意外死亡,警方通過查閱死者的電腦和手機坯约,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評論 3 395
  • 文/潘曉璐 我一進店門熊咽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人闹丐,你說我怎么就攤上這事横殴。” “怎么了卿拴?”我有些...
    開封第一講書人閱讀 164,234評論 0 354
  • 文/不壞的土叔 我叫張陵衫仑,是天一觀的道長。 經(jīng)常有香客問我堕花,道長文狱,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,562評論 1 293
  • 正文 為了忘掉前任缘挽,我火速辦了婚禮瞄崇,結果婚禮上呻粹,老公的妹妹穿的比我還像新娘。我一直安慰自己苏研,他們只是感情好等浊,可當我...
    茶點故事閱讀 67,611評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著楣富,像睡著了一般凿掂。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上纹蝴,一...
    開封第一講書人閱讀 51,482評論 1 302
  • 那天庄萎,我揣著相機與錄音,去河邊找鬼塘安。 笑死糠涛,一個胖子當著我的面吹牛,可吹牛的內容都是我干的兼犯。 我是一名探鬼主播忍捡,決...
    沈念sama閱讀 40,271評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼切黔!你這毒婦竟也來了砸脊?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,166評論 0 276
  • 序言:老撾萬榮一對情侶失蹤纬霞,失蹤者是張志新(化名)和其女友劉穎凌埂,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體诗芜,經(jīng)...
    沈念sama閱讀 45,608評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡瞳抓,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,814評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了伏恐。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片孩哑。...
    茶點故事閱讀 39,926評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖翠桦,靈堂內的尸體忽然破棺而出横蜒,到底是詐尸還是另有隱情,我是刑警寧澤销凑,帶...
    沈念sama閱讀 35,644評論 5 346
  • 正文 年R本政府宣布愁铺,位于F島的核電站,受9級特大地震影響闻鉴,放射性物質發(fā)生泄漏。R本人自食惡果不足惜茂洒,卻給世界環(huán)境...
    茶點故事閱讀 41,249評論 3 329
  • 文/蒙蒙 一孟岛、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦渠羞、人聲如沸斤贰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽荧恍。三九已至,卻和暖如春屯吊,著一層夾襖步出監(jiān)牢的瞬間送巡,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評論 1 269
  • 我被黑心中介騙來泰國打工盒卸, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留骗爆,地道東北人。 一個月前我還...
    沈念sama閱讀 48,063評論 3 370
  • 正文 我出身青樓蔽介,卻偏偏與公主長得像摘投,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子虹蓄,可洞房花燭夜當晚...
    茶點故事閱讀 44,871評論 2 354

推薦閱讀更多精彩內容