iOS 使用MJRefresh實現(xiàn)刷新

?? MJRefresh是Github上點贊次數(shù)最多的刷新控件榄笙,本文主要演示如何使用MJRefresh實現(xiàn)刷新和MJRefresh的內(nèi)部實現(xiàn)原理豁翎。

1.使用MJRefresh實現(xiàn)刷新——UIScrollView+MJRefresh.h

??使用MJRefresh實現(xiàn)刷新非常簡單,主要分3步:1.導(dǎo)入框架。2.定義一個Scrollview;2.為Scrollview添加刷新控件统求。

// 1.導(dǎo)入框架
#import "MJRefresh.h"

@interface ViewController()
@property (nonatomic, strong) UITableView * tableView;
@end;

@implementation ViewController;

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    // 2.定義一個UITableView
    self.tableView = [[UITableView alloc] initWithFrame: self.view.bounds];
    [self.view addSubview:self.tableView];
    
    // 3.添加刷新控件
    // 下拉刷新
    __weak typeof(self)  weakSelf = self;
    self.tableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
        // 模擬延遲加載數(shù)據(jù),因此2秒后才調(diào)用(真實開發(fā)中攻柠,可以移除這段gcd代碼)
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            // 結(jié)束刷新
            [weakSelf.tableView.mj_header endRefreshing];
        });
    }];
    
    // 上拉刷新
    self.tableView.mj_footer = [MJRefreshBackNormalFooter footerWithRefreshingBlock:^{
        // 模擬延遲加載數(shù)據(jù)球订,因此2秒后才調(diào)用(真實開發(fā)中,可以移除這段gcd代碼)
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            // 結(jié)束刷新
            [weakSelf.tableView.mj_footer endRefreshing];
        });
    }];
  }

??MJRefresh定義了一個類別UIScrollView+MJRefresh.h瑰钮,利用類別為Tableview增加了兩個屬性mj_header和mj_footer冒滩。

#import <UIKit/UIKit.h>
#import "MJRefreshConst.h"

@class MJRefreshHeader, MJRefreshFooter;

@interface UIScrollView (MJRefresh)
/** 下拉刷新控件 */
@property (strong, nonatomic) MJRefreshHeader *mj_header;
/** 上拉刷新控件 */
@property (strong, nonatomic) MJRefreshFooter *mj_footer;

@end

??由于在類別中添加屬性,并不能在編譯期間自動添加成員變量浪谴、set和get方法(因為類的結(jié)構(gòu)已經(jīng)確定开睡,在類別中再添加成員變量會影響已經(jīng)添加的成員變量的存儲。)苟耻,所以我們要objc_setAssociatedObjectobjc_getAssociatedObject的兩個方法自己實現(xiàn)篇恒。

#pragma mark - header
static const char MJRefreshHeaderKey = '\0';
- (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
    }
}

- (MJRefreshHeader *)mj_header
{
    return objc_getAssociatedObject(self, &MJRefreshHeaderKey);
}

?? MJRefreshHeaderKey這是定義了一個靜態(tài)字符凶杖,用它的地址作為存儲mj_header的key胁艰,用最小的存儲空間實現(xiàn)了key的定義。
?? 通過上文中的代碼我們可以看出智蝠,所謂的添加下拉刷新控件就是在Scrollview上加了一個View腾么。下文我們將介紹MJRefresh是怎么定義這個View(mj_header)的。

2.繼承關(guān)系

??MJRefresh總共分四層杈湾,我將從最底層MJRefreshComponent開始介紹MJRefresh的實現(xiàn)原理解虱。


繼承結(jié)構(gòu)圖.png

3.MJRefreshComponent

?? 從這個類我們可以知道,MJRefreshHeader的實現(xiàn)主要是利用KVO監(jiān)聽ScrollView的contentOffset漆撞,contentInset殴泰,contentSize三個屬性的變化來確定ScrollView的刷新狀態(tài)(MJRefreshState)。在- (void)setState:(MJRefreshState)state中實現(xiàn)ScrollView的各個狀態(tài)下的UI變化和調(diào)用回調(diào)方法浮驳。

// 當(dāng)刷新空間將要添加到父視圖中/從父視圖中移除
- (void)willMoveToSuperview:(UIView *)newSuperview
{
    [super willMoveToSuperview:newSuperview];
    
    // 如果不是UIScrollView悍汛,不做任何事情
    if (newSuperview && ![newSuperview isKindOfClass:[UIScrollView class]]) return;
    
    // 舊的父控件移除監(jiān)聽
    [self removeObservers];
    
    // 添加到父視圖
    //  如果newSuperview為nil,表示從父視圖中移除
    if (newSuperview) { // 新的父控件
        // 設(shè)置寬度
        self.mj_w = newSuperview.mj_w;
        // 設(shè)置位置
        self.mj_x = -_scrollView.mj_insetL;
        
        // 記錄UIScrollView
        _scrollView = (UIScrollView *)newSuperview;
        // 設(shè)置永遠(yuǎn)支持垂直彈簧效果
        _scrollView.alwaysBounceVertical = YES;
        // 記錄UIScrollView最開始的contentInset
        _scrollViewOriginalInset = _scrollView.mj_inset;
        
        // 添加監(jiān)聽
        [self addObservers];
    }
}

#pragma mark - KVO監(jiān)聽
- (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];
}

- (void)removeObservers
{
    [self.superview removeObserver:self forKeyPath:MJRefreshKeyPathContentOffset];
    [self.superview removeObserver:self forKeyPath:MJRefreshKeyPathContentSize];
    [self.pan removeObserver:self forKeyPath:MJRefreshKeyPathPanState];
    self.pan = nil;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    // 遇到這些情況就直接返回
    if (!self.userInteractionEnabled) return;
    
    // 這個就算看不見也需要處理
    if ([keyPath isEqualToString:MJRefreshKeyPathContentSize]) {
        [self scrollViewContentSizeDidChange:change];
    }
    
    // 看不見
    if (self.hidden) return;
    if ([keyPath isEqualToString:MJRefreshKeyPathContentOffset]) {
        [self scrollViewContentOffsetDidChange:change];
    } else if ([keyPath isEqualToString:MJRefreshKeyPathPanState]) {
        [self scrollViewPanStateDidChange:change];
    }
}
// 當(dāng)ContentOffset變化的時候調(diào)用
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{}
// 當(dāng)ContentSize變化的時候調(diào)用
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change{}
- (void)scrollViewPanStateDidChange:(NSDictionary *)change{}

?? - (void)willMoveToSuperview:(nullable UIView *)newSuperview;這個方法在View添加到父視圖(addSubView)和從父視圖中移除(removeFromSuperView)都會調(diào)用至会。當(dāng)newSuperview存在的時候员凝,代表的是添加到父視圖,此時添加KVO奋献,并為每個屬性設(shè)置了回調(diào)方法健霹。 MJRefreshComponent的子類MJRefreshHeader實現(xiàn)- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change;,根據(jù)ContentOffset的變化來確定ScrollView的刷新狀態(tài)瓶蚂。所以糖埋,我們研究MJRefresh刷新的實現(xiàn),就研究 MJRefreshComponent的各個子類在這個方法中的實現(xiàn)即可窃这。
?? 此外還定義了兩個方法- (void)prepare;- (void)placeSubviews;瞳别,都需要在子類中實現(xiàn)。在- (void)prepare;方法中杭攻,主要是進(jìn)行數(shù)據(jù)的初始化祟敛,- (void)placeSubviews;中主要是實現(xiàn)控件的擺放,- (void)prepare;先于- (void)placeSubviews;執(zhí)行兆解。

4.MJRefreshHeader

??在MJRefreshHeader中實現(xiàn)了- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change方法馆铁,確定了ScrollView的刷新狀態(tài),就是確定了這個屬性@property (assign, nonatomic) MJRefreshState state;的值锅睛。
??分兩種情況埠巨,一種是ScrollView正在刷新(MJRefreshStateRefreshing),通過修改ScrollView的contentInset现拒,增加了contentInset.top的值辣垒,增加了MJRefreshHeader的高度,讓MJRefreshHeader完全顯示出來印蔬,以達(dá)到Hearder懸停效果勋桶;另一種是其他狀態(tài),此時來判斷什么時候開始刷新侥猬。

刷新時的Scrollview.png

- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
    [super scrollViewContentOffsetDidChange:change];
    
    // 此時ScrollView正在刷新
    if (self.state == MJRefreshStateRefreshing) {
        // 如果視圖還沒有添加到KeyWindow上例驹,返回。
        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;
    }
    
    // 跳轉(zhuǎn)到下一個控制器時陵究,contentInset可能會變
     _scrollViewOriginalInset = self.scrollView.mj_inset;
    
    // 當(dāng)前的contentOffset
    CGFloat offsetY = self.scrollView.mj_offsetY;
    // 頭部控件剛好出現(xiàn)的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) {
            // 轉(zhuǎn)為即將刷新狀態(tài)
            self.state = MJRefreshStatePulling;
        } else if (self.state == MJRefreshStatePulling && offsetY >= normal2pullingOffsetY) {
            // 轉(zhuǎn)為普通狀態(tài)
            self.state = MJRefreshStateIdle;
        }
    } else if (self.state == MJRefreshStatePulling) {// 即將刷新 && 手松開
        // 開始刷新
        [self beginRefreshing];
    } else if (pullingPercent < 1) {
        self.pullingPercent = pullingPercent;
    }
}

?? MJRefreshComponent的每個子類都實現(xiàn)了- (void)setState:(MJRefreshState)state這個方法。MJRefreshHeader實現(xiàn)了通過改變和恢復(fù)inset和offset來實現(xiàn)Scrollview的刷新效果铜邮。當(dāng)state == MJRefreshStateIdle的時候仪召,恢復(fù)inset。當(dāng)state == MJRefreshStateRefreshing的時候松蒜,增加inset.top一個MJRefreshHeader的高度扔茅,并且滾動到頂部。

- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState
    
    // 根據(jù)狀態(tài)做事情
    if (state == MJRefreshStateIdle) {
        if (oldState != MJRefreshStateRefreshing) return;
        
        // 保存刷新時間
        [[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:self.lastUpdatedTimeKey];
        [[NSUserDefaults standardUserDefaults] synchronize];
        
        // 恢復(fù)inset和offset
        [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
            self.scrollView.mj_insetT += self.insetTDelta;
            
            // 自動調(diào)整透明度
            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;
                // 增加滾動區(qū)域top
                self.scrollView.mj_insetT = top;
                // 設(shè)置滾動位置
                CGPoint offset = self.scrollView.contentOffset;
                offset.y = -top;
                [self.scrollView setContentOffset:offset animated:NO];
            } completion:^(BOOL finished) {
                [self executeRefreshingCallback];
            }];
         });
    }
}

??這個類還定義了兩個構(gòu)造方法秸苗,在構(gòu)造方法中保存了頭部刷新的回調(diào)召娜。

 + (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock;
 + (instancetype)headerWithRefreshingTarget:(id)target refreshingAction:(SEL)action;

5.MJRefreshStateHeader

?? 設(shè)置lastUpdatedTimeLabel和stateLabel的位置以及顯示的內(nèi)容。這個類非常簡單惊楼,沒有什么可講的玖瘸。這個類里的- (void)setState:(MJRefreshState)state;實現(xiàn)的是這兩個Label顯示的內(nèi)容秸讹。

- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState
    
    // 設(shè)置狀態(tài)文字
    self.stateLabel.text = self.stateTitles[@(state)];
    
    // 重新設(shè)置key(重新顯示時間)
    self.lastUpdatedTimeKey = self.lastUpdatedTimeKey;
}
MJRefreshStateHeader.jpg

6. MJRefreshNormalHeader(MJRefreshGifHeader)

?? MJRefreshNormalHeader刷新的時候是一個菊花,MJRefreshGifHeader可以自定義MJRefreshHeader各個狀態(tài)的動畫雅倒,其實兩個的實現(xiàn)思路大同小異璃诀,只是一個是UIActivityIndicatorView,一個是UIImageView,兩者的位置都是相同的蔑匣。剩下的就是動畫效果的是實現(xiàn)了劣欢,這應(yīng)該屬于最基本的動畫了,在這里我就不講了裁良,有不懂的可以自行百度凿将。
?? 此外還有一個箭頭(arrowView),這個箭頭就是UIImageView价脾,它和UIActivityIndicatorView(Gif)交替展示牧抵,就是一個隱藏,一個就顯示彼棍。這個類里的- (void)setState:(MJRefreshState)state;實現(xiàn)的是arrowView和loadingView的動畫效果灭忠。

- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState
    
    // 根據(jù)狀態(tài)做事情
    if (state == MJRefreshStateIdle) {
        if (oldState == MJRefreshStateRefreshing) {
            self.arrowView.transform = CGAffineTransformIdentity;
            
            [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
                self.loadingView.alpha = 0.0;
            } completion:^(BOOL finished) {
                // 如果執(zhí)行完動畫發(fā)現(xiàn)不是idle狀態(tài),就直接返回座硕,進(jìn)入其他狀態(tài)
                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的動畫完畢動作沒有被執(zhí)行
        [self.loadingView startAnimating];
        self.arrowView.hidden = YES;
    }
}

?? MJRefresh的分層非常清晰弛作,一目了然,一些個功能的實現(xiàn)非常巧妙华匾,而且大部分還有注釋映琳,這是了解UIScrollView以及刷新機制的非常好的一個框架。下一篇我將分析MJRefreshFooter蜘拉。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末萨西,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子旭旭,更是在濱河造成了極大的恐慌谎脯,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件持寄,死亡現(xiàn)場離奇詭異源梭,居然都是意外死亡,警方通過查閱死者的電腦和手機稍味,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進(jìn)店門废麻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人模庐,你說我怎么就攤上這事烛愧。” “怎么了?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵怜姿,是天一觀的道長慎冤。 經(jīng)常有香客問我,道長沧卢,這世上最難降的妖魔是什么粪薛? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮搏恤,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘湃交。我一直安慰自己熟空,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布搞莺。 她就那樣靜靜地躺著息罗,像睡著了一般。 火紅的嫁衣襯著肌膚如雪才沧。 梳的紋絲不亂的頭發(fā)上迈喉,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天,我揣著相機與錄音温圆,去河邊找鬼挨摸。 笑死,一個胖子當(dāng)著我的面吹牛岁歉,可吹牛的內(nèi)容都是我干的得运。 我是一名探鬼主播,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼锅移,長吁一口氣:“原來是場噩夢啊……” “哼熔掺!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起非剃,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤置逻,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后备绽,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體券坞,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年疯坤,在試婚紗的時候發(fā)現(xiàn)自己被綠了报慕。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡压怠,死狀恐怖眠冈,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤蜗顽,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布布卡,位于F島的核電站,受9級特大地震影響雇盖,放射性物質(zhì)發(fā)生泄漏忿等。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一崔挖、第九天 我趴在偏房一處隱蔽的房頂上張望贸街。 院中可真熱鬧,春花似錦狸相、人聲如沸薛匪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽逸尖。三九已至,卻和暖如春瘸右,著一層夾襖步出監(jiān)牢的瞬間娇跟,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工太颤, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留苞俘,地道東北人。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓栋齿,卻偏偏與公主長得像苗胀,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子瓦堵,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,086評論 2 355

推薦閱讀更多精彩內(nèi)容