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以及刷新機(jī)制的非常好的一個框架。下一篇我將分析MJRefreshFooter。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容