MJRefresh 淺析

項(xiàng)目中經(jīng)常用到下拉刷新的功能,平時(shí)用的最多的是MJRefresh,這個(gè)第三方庫提供了簡單易用的API,方便使用,下面我們來分析一下源碼。

一、常數(shù)類

55ECBAE1-A4AA-40C3-84EC-651B50F1EA26.png

常量類比較簡單,其中定義了宏,高度、提示語句等常量,以及KVO用到的屬性名。

二、Category


90308612-7C2E-4A97-B7CD-E0FE2A3F9752.png

XX+MJExtension中定義獲取/設(shè)置視圖的x、y、w、h、offset、inset等屬性的方法,方便使用。(PS:之前有用宏來定義過,也能達(dá)到簡化代碼的目的,但是確實(shí)沒有用Category方便,尤其當(dāng)宏定義有多行代碼的時(shí)候);

UIScollView+MJRefresh為ScrollView添加了一些方法,我們來逐個(gè)分析。

#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];
        
        // 存儲(chǔ)新的
        [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);
}

這段代碼利用的runtime的特性,將mj_header綁定到MJRefreshHeaderKey的地址上,從而實(shí)現(xiàn)向Category中添加屬性的效果。在Category中可以使用property,編譯通過,但是運(yùn)行無效。但是在XX+MJExtension中作者也使用到了property,為什么又可以呢?我們仔細(xì)觀察一下代碼:

C2A60E09-2D01-438B-BC74-2BCC834B381F.png

可以發(fā)現(xiàn)setMj_x方法并不是對mj_x進(jìn)行賦值,mj_x方法返回的也并不是mj_x的值,如果是return self.mj_x,運(yùn)行時(shí)同樣會(huì)報(bào)錯(cuò)。

繼續(xù)往下看:

- (NSInteger)mj_totalDataCount
{
    NSInteger totalCount = 0;
    if ([self isKindOfClass:[UITableView class]]) {
        UITableView *tableView = (UITableView *)self;
        
        for (NSInteger section = 0; section<tableView.numberOfSections; section++) {
            totalCount += [tableView numberOfRowsInSection:section];
        }
    } else if ([self isKindOfClass:[UICollectionView class]]) {
        UICollectionView *collectionView = (UICollectionView *)self;
        
        for (NSInteger section = 0; section<collectionView.numberOfSections; section++) {
            totalCount += [collectionView numberOfItemsInSection:section];
        }
    }
    return totalCount;
}

這段代碼獲取了tableView或collectionView的元素個(gè)數(shù)。

繼續(xù):

static const char MJRefreshReloadDataBlockKey = '\0';
- (void)setMj_reloadDataBlock:(void (^)(NSInteger))mj_reloadDataBlock
{
    [self willChangeValueForKey:@"mj_reloadDataBlock"]; // KVO
    objc_setAssociatedObject(self, &MJRefreshReloadDataBlockKey, mj_reloadDataBlock, OBJC_ASSOCIATION_COPY_NONATOMIC);
    [self didChangeValueForKey:@"mj_reloadDataBlock"]; // KVO
}

- (void (^)(NSInteger))mj_reloadDataBlock
{
    return objc_getAssociatedObject(self, &MJRefreshReloadDataBlockKey);
}

- (void)executeReloadDataBlock
{
    !self.mj_reloadDataBlock ? : self.mj_reloadDataBlock(self.mj_totalDataCount);
}
@end

給UIScrollView添加了block對象,并確在設(shè)置的時(shí)候手動(dòng)出發(fā)KVO。

Category的最后一部分代碼:

@implementation UITableView (MJRefresh)

+ (void)load
{
    [self exchangeInstanceMethod1:@selector(reloadData) method2:@selector(mj_reloadData)];
}

- (void)mj_reloadData
{
    [self mj_reloadData];
    
    [self executeReloadDataBlock];
}
@end

+ (void)exchangeInstanceMethod1:(SEL)method1 method2:(SEL)method2
{
    method_exchangeImplementations(class_getInstanceMethod(self, method1), class_getInstanceMethod(self, method2));
}

+ (void)exchangeClassMethod1:(SEL)method1 method2:(SEL)method2
{
    method_exchangeImplementations(class_getClassMethod(self, method1), class_getClassMethod(self, method2));
}

這里用到了Method Swizzling技術(shù),簡而言之就是利用runtime的特性將兩個(gè)類方法或?qū)嵗椒ń粨Q,在這里,作者實(shí)現(xiàn)了每次執(zhí)行完UIScrollView自己的方法reloadData后執(zhí)行[self executeReloadDataBlock]。

三、MJ類
這里直接借用github上的原圖,下圖表示的是MJ各個(gè)類之間的繼承關(guān)系。


687474703a2f2f696d61676573302e636e626c6f67732e636f6d2f626c6f67323031352f3439373237392f3230313530362f3133323233323435363133393137372e706e67.png

我們已Header為例,分析一下每一個(gè)類都做了哪些事情,首先是MJRefreshComponent。

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

- (void)prepare
{
    // 基本屬性
    self.autoresizingMask = UIViewAutoresizingFlexibleWidth;
    self.backgroundColor = [UIColor clearColor];
}

設(shè)置基本屬性,設(shè)置刷新控件的狀態(tài),初始化為Idle,其他狀態(tài)如下:

/** 刷新控件的狀態(tài) */
typedef NS_ENUM(NSInteger, MJRefreshState) {
    /** 普通閑置狀態(tài) */
    MJRefreshStateIdle = 1,
    /** 松開就可以進(jìn)行刷新的狀態(tài) */
    MJRefreshStatePulling,
    /** 正在刷新中的狀態(tài) */
    MJRefreshStateRefreshing,
    /** 即將刷新的狀態(tài) */
    MJRefreshStateWillRefresh,
    /** 所有數(shù)據(jù)加載完畢,沒有更多的數(shù)據(jù)了 */
    MJRefreshStateNoMoreData
};

繼續(xù)往下看:

- (void)willMoveToSuperview:(UIView *)newSuperview
{
    [super willMoveToSuperview:newSuperview];
    
    // 如果不是UIScrollView,不做任何事情
    if (newSuperview && ![newSuperview isKindOfClass:[UIScrollView class]]) return;
    
    // 舊的父控件移除監(jiān)聽
    [self removeObservers];
    
    if (newSuperview) { // 新的父控件
        // 設(shè)置寬度
        self.mj_w = newSuperview.mj_w;
        // 設(shè)置位置
        self.mj_x = 0;
        
        // 記錄UIScrollView
        _scrollView = (UIScrollView *)newSuperview;
        // 設(shè)置永遠(yuǎn)支持垂直彈簧效果
        _scrollView.alwaysBounceVertical = YES;
        // 記錄UIScrollView最開始的contentInset
        _scrollViewOriginalInset = _scrollView.contentInset;
        
        // 添加監(jiān)聽
        [self addObservers];
    }
}

這段代碼在添加MJView的時(shí)候執(zhí)行,addObservers中設(shè)置了KVO的監(jiān)聽,具體處理如下,由于MJRefreshComponent是基類,所以并沒有做什么具體的事情,全部交由子類處理。

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    // 遇到這些情況就直接返回
    if (!self.userInteractionEnabled) return;
    
    // 這個(gè)就算看不見也需要處理
    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];
    }
}
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{}
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change{}
- (void)scrollViewPanStateDidChange:(NSDictionary *)change{}

再往下看:

@implementation UILabel(MJRefresh)
+ (instancetype)label
{
    UILabel *label = [[self alloc] init];
    label.font = MJRefreshLabelFont;
    label.textColor = MJRefreshLabelTextColor;
    label.autoresizingMask = UIViewAutoresizingFlexibleWidth;
    label.textAlignment = NSTextAlignmentCenter;
    label.backgroundColor = [UIColor clearColor];
    return label;
}

作者為了能快速的添加一個(gè)統(tǒng)一樣式的label,所以又給UILabel加了一個(gè)方法。
—————————————————————————
再來看看MJRefreshComponent的子類MJRefreshHeader,負(fù)責(zé)監(jiān)控用戶下拉的狀態(tài)。

pragma mark - 構(gòu)造方法

+ (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock
{
    MJRefreshHeader *cmp = [[self alloc] init];
    cmp.refreshingBlock = refreshingBlock;
    return cmp;
}
+ (instancetype)headerWithRefreshingTarget:(id)target refreshingAction:(SEL)action
{
    MJRefreshHeader *cmp = [[self alloc] init];
    [cmp setRefreshingTarget:target refreshingAction:action];
    return cmp;
}

兩種添加header的方式,block和target方式。

繼續(xù)看:

- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
    [super scrollViewContentOffsetDidChange:change];
    
    // 在刷新的refreshing狀態(tài)
    if (self.state == MJRefreshStateRefreshing) {
        if (self.window == nil) return;
        
        // sectionheader停留解決
        NSLog(@"%f", self.scrollView.mj_offsetY);
        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)到下一個(gè)控制器時(shí),contentInset可能會(huì)變
     _scrollViewOriginalInset = self.scrollView.contentInset;
    
    // 當(dāng)前的contentOffset
    CGFloat offsetY = self.scrollView.mj_offsetY;
    // 頭部控件剛好出現(xiàn)的offsetY
    CGFloat happenOffsetY = - self.scrollViewOriginalInset.top;
    
    // 如果是向上滾動(dòng)到看不見頭部控件,直接返回
    // >= -> >
    if (offsetY > happenOffsetY) return;
    
    // 普通 和 即將刷新 的臨界點(diǎn)
    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;
    }
}

這里通過MJComponent的KVO觸發(fā)scrollViewContentOffsetDidChange,然后根據(jù)當(dāng)前scrollview偏移量設(shè)置控件的狀態(tài)。

再往下:

- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState
    
    // 根據(jù)狀態(tài)做事情
    if (state == MJRefreshStateIdle) {
        if (oldState != MJRefreshStateRefreshing) return;
        
        // 保存刷新時(shí)間
        [[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:self.lastUpdatedTimeKey];
        [[NSUserDefaults standardUserDefaults] synchronize];
        
        // 恢復(fù)inset和offset
        [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
            self.scrollView.mj_insetT += self.insetTDelta;
            
            // 自動(dòng)調(diào)整透明度
            if (self.isAutomaticallyChangeAlpha) self.alpha = 0.0;
        } completion:^(BOOL finished) {
            self.pullingPercent = 0.0;
        }];
    } else if (state == MJRefreshStateRefreshing) {
        [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
            // 增加滾動(dòng)區(qū)域
            CGFloat top = self.scrollViewOriginalInset.top + self.mj_h;
            self.scrollView.mj_insetT = top;
            
            // 設(shè)置滾動(dòng)位置
            self.scrollView.mj_offsetY = - top;
        } completion:^(BOOL finished) {
            [self executeRefreshingCallback];
        }];
    }
}

每次設(shè)置state的時(shí)候,執(zhí)行改段代碼,根據(jù)之前的狀態(tài)進(jìn)行狀態(tài)的轉(zhuǎn)換并且執(zhí)行相應(yīng)的動(dòng)畫。

另外幾個(gè)類比較簡單,MJRefreshStateHeader增加的刷新時(shí)間以及狀態(tài)等的文字顯示,MJRefreshNormalHeader增加了箭頭的顯示,MJRefreshGifHeader增加了自定義動(dòng)畫的顯示。

我們來梳理一下完整的流程:
1、調(diào)用MJRefreshHeader的構(gòu)造方法headerWithRefreshingBlock初始化Header(包括state初始化,設(shè)置KVO,獲取scrollview的gestureRecognizer),并設(shè)置回調(diào)事件;
2、手勢操作-> scrollViewContentOffsetDidChange-> setState->界面更新;

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

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

  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,658評(píng)論 4 61
  • MJRefresh是流行的下拉刷新控件,前段時(shí)間為了修復(fù)一個(gè)BUG,讀了它的源碼,本文總結(jié)一下實(shí)現(xiàn)的原理 下拉刷新...
    晚安的你我閱讀 522評(píng)論 0 0
  • 嗯哼嗯哼蹦擦擦~~~ 轉(zhuǎn)載自:https://github.com/Tim9Liu9/TimLiu-iOS 目錄 ...
    philiha閱讀 5,271評(píng)論 0 6
  • 今天是大年初一,春晚實(shí)在是太難看了,所以就想刷一下微博,看看吐槽大會(huì)好了,結(jié)果一不小心看到了恐怖電影推薦(其實(shí)是自...
    埃文斯楊閱讀 483評(píng)論 0 0
  • 新校區(qū)開張,寒假作業(yè)免費(fèi)輔導(dǎo)啦! 大家新年好!贛州家教大本營新校區(qū)將在水南新區(qū)會(huì)昌路開張,只要家長朋友做到以...
    小麋鹿鹿閱讀 497評(píng)論 0 0

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