iOS-MJRefresh框架

李明杰的MJRefresh應該也算是iOS中使用最廣泛的一個框架了,而且MJ的框架也用了好多中文注釋,這點讓我感覺到很親切,網上也有好多分析的文章,但是別人的畢竟沒自己的印象深刻,現在分析一下MJRefresh每一句代碼是干什么的?為什么要這么寫?

想要寫一個好的框架需要注意兩點:1. 易用性強 2. 可定制性強 3. 設計合理
易用性強才會有人愿意使用,可定制性強才會有更多的場景可以使用,設計合理以后修改的時候才不會太麻煩,別人也很容易理解。

一. 如何實現下拉刷新

  1. 利用contentOffset
    首先如何實現下拉刷新,很顯然下拉的時候scrollView 的contentOffset會改變,我們可以監(jiān)聽這個值的變化來給scrollView添加一個mj_header并實現相應的動畫效果。
  2. 如何添加mj_header
    現在目標是給scrollView添加一個MJRefreshHeader,只要給scrollView添加MJRefreshHeader,其他tableView和collectionView就都有了,但是系統(tǒng)的scrollView沒有這個屬性,這時候我們可以通過給scrollView的分類添加關聯對象的方式,來實現給scrollView添加一個屬性,具體可參考UIScrollView+MJRefresh代碼。
  3. mj_header添加在contentInset.top的位置。
  4. 為什么要有MJRefreshComponent
    接下來我們就想如何去寫一個MJRefreshHeader,顯然,我們不但有下拉刷新還有上拉刷新,這時候我們就需要一個baseView,這個baseView就是MJRefreshComponent,我們的上拉和下拉刷新控件都繼承于這個MJRefreshComponent,MJRefreshComponent繼承于UIView是最基礎的基類,所以關于上拉下拉所有唯一共用的東西我們都可以寫在這里面。

接下來的事情就很簡單了,我們可以層層繼承,在合適的類添加合適的控件實現合適的方法。

二. MJRefreshComponent

1. 定義的東西和成員變量

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

/** 進入刷新狀態(tài)的回調 */
typedef void (^MJRefreshComponentRefreshingBlock)(void);
/** 開始刷新后的回調(進入刷新狀態(tài)后的回調) */
typedef void (^MJRefreshComponentbeginRefreshingCompletionBlock)(void);
/** 結束刷新后的回調 */
typedef void (^MJRefreshComponentEndRefreshingCompletionBlock)(void);

/** 刷新控件的基類 */
@interface MJRefreshComponent : UIView
{
    /** 記錄scrollView剛開始的inset */
    UIEdgeInsets _scrollViewOriginalInset;
    /** 父控件 */
    __weak UIScrollView *_scrollView;
}
  1. 首先定義了刷新狀態(tài)MJRefreshState和三個刷新狀態(tài)的block,這個很容易理解,每個刷新控件一定有這些東西。
  2. 另外還定義了兩個成員變量
    ① _scrollViewOriginalInset這個值記錄scrollView剛開始的inset,這個值會在scrollViewContentOffsetDidChange方法里面使用到,用來設置mj_header的位置。
    ② _scrollView就是父控件,弱引用。

2. 刷新回調

#pragma mark - 刷新回調
/** 正在刷新的回調 */
@property (copy, nonatomic) MJRefreshComponentRefreshingBlock refreshingBlock;
/** 設置回調對象和回調方法 */
- (void)setRefreshingTarget:(id)target refreshingAction:(SEL)action;

/** 回調對象 */
@property (weak, nonatomic) id refreshingTarget;
/** 回調方法 */
@property (assign, nonatomic) SEL refreshingAction;
/** 觸發(fā)回調(交給子類去調用) */
- (void)executeRefreshingCallback;

這里定義了刷新回調,以及回調方法和回調對象,主要介紹executeRefreshingCallback方法:

#pragma mark - 內部方法
//執(zhí)行刷新回調,不同子類都會調用,所以抽取到父類
- (void)executeRefreshingCallback
{
    MJRefreshDispatchAsyncOnMainQueue({
        if (self.refreshingBlock) {
            self.refreshingBlock();
        }
        if ([self.refreshingTarget respondsToSelector:self.refreshingAction]) {
            //消息發(fā)送機制:
            //((void (*)(void *, SEL, UIView *))objc_msgSend)((__bridge void *)(self.refreshingTarget), self.refreshingAction, self);
            MJRefreshMsgSend(MJRefreshMsgTarget(self.refreshingTarget), self.refreshingAction, self);
        }
        if (self.beginRefreshingCompletionBlock) {
            self.beginRefreshingCompletionBlock();
        }
    })
}

這個方法執(zhí)行刷新回調,不同子類都會調用,所以抽取到父類里面。

3. 刷新狀態(tài)控制

#pragma mark - 刷新狀態(tài)控制
/** 進入刷新狀態(tài) */
- (void)beginRefreshing;
- (void)beginRefreshingWithCompletionBlock:(void (^)(void))completionBlock;
/** 開始刷新后的回調(進入刷新狀態(tài)后的回調) */
@property (copy, nonatomic) MJRefreshComponentbeginRefreshingCompletionBlock beginRefreshingCompletionBlock;
/** 結束刷新的回調 */
@property (copy, nonatomic) MJRefreshComponentEndRefreshingCompletionBlock endRefreshingCompletionBlock;
/** 結束刷新狀態(tài) */
- (void)endRefreshing;
- (void)endRefreshingWithCompletionBlock:(void (^)(void))completionBlock;
/** 是否正在刷新 */
@property (assign, nonatomic, readonly, getter=isRefreshing) BOOL refreshing;
//- (BOOL)isRefreshing;
/** 刷新狀態(tài) 一般交給子類內部實現 */
@property (assign, nonatomic) MJRefreshState state;

這里定義了開始結束刷新的方法以及開始結束刷新的block,定義了刷新狀態(tài)以及是否正在刷新的BOOL值來控制刷新狀態(tài)。

① setState:

一般交給子類內部實現,不同狀態(tài)做不同的事情。

- (void)setState:(MJRefreshState)state
{
    _state = state;
    
    // 加入主隊列的目的是等setState:方法調用完畢、設置完文字后再去布局子控件
    //因為文字的變化會引起左側箭頭位置的變化,這時候需要刷新來重制位置。
    MJRefreshDispatchAsyncOnMainQueue([self setNeedsLayout];)
}

② beginRefreshing:

他其實主要就是把state標記為MJRefreshStateRefreshing。但是它還做了另外一層判斷:window的有無。MJ 也做了備注,說明了為什么要有這個判斷,主要是因為預防用戶過早的調用了beginRefresh方法,然而這時候自身還并沒有顯示出來,所以巧妙的先將state標記為了MJRefreshStateWillRefresh。

- (void)beginRefreshing
{
    [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
        self.alpha = 1.0;
    }];
    self.pullingPercent = 1.0;
    // 只要正在刷新,就完全顯示
    if (self.window) {
        self.state = MJRefreshStateRefreshing;
    } else {
        // 預防正在刷新中時,調用本方法使得header inset回置失敗
        if (self.state != MJRefreshStateRefreshing) {
            self.state = MJRefreshStateWillRefresh;
            // 刷新(預防從另一個控制器回到這個控制器的情況,回來要重新刷新一下)
            [self setNeedsDisplay];
        }
    }
}

4. 交給子類實現

#pragma mark - 交給子類們去實現
/** 初始化 */
- (void)prepare NS_REQUIRES_SUPER;
/** 擺放子控件frame */
- (void)placeSubviews NS_REQUIRES_SUPER;
/** 當scrollView的contentOffset發(fā)生改變的時候調用 */
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;
/** 當scrollView的contentSize發(fā)生改變的時候調用 */
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;
/** 當scrollView的拖拽狀態(tài)發(fā)生改變的時候調用 */
- (void)scrollViewPanStateDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;

這些方法主要是交給子類來實現,這里只實現了prepare方法:

- (void)prepare
{
    // 基本屬性
    //保證在橫豎屏切換的時候能夠保證自身相對于父視圖的左右邊距保持不變,這個方法是每個子類都必須的,所以放在了基類MJRefreshComponent中。
    self.autoresizingMask = UIViewAutoresizingFlexibleWidth;
    self.backgroundColor = [UIColor clearColor];
}

方法的調用順序是:

alloc(prepare) -> setState -> willMoveToSuperView -> layoutSubViews(placeSubviews) -> drawRect

5. 拖拽百分比和透明度

#pragma mark - 其他
/** 拉拽的百分比(交給子類重寫) */
@property (assign, nonatomic) CGFloat pullingPercent;
/** 根據拖拽比例自動切換透明度 */
@property (assign, nonatomic, getter=isAutoChangeAlpha) BOOL autoChangeAlpha MJRefreshDeprecated("請使用automaticallyChangeAlpha屬性");
/** 根據拖拽比例自動切換透明度 */
@property (assign, nonatomic, getter=isAutomaticallyChangeAlpha) BOOL automaticallyChangeAlpha;
@end

這里有拖拽百分比和自動切換透明度,pullingPercent一般交給子類來實現,根據拖拽的比例來實現個性定制,默認是根據拖拽百分比自動切換透明度,如下:

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

6. UILabel分類

@interface UILabel(MJRefresh)

+ (instancetype)mj_label;
- (CGFloat)mj_textWith;

@end

實現了兩個方法:

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

- (CGFloat)mj_textWith {
    CGFloat stringWidth = 0;
    CGSize size = CGSizeMake(MAXFLOAT, MAXFLOAT);
    if (self.text.length > 0) {
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000
        stringWidth =[self.text
                      boundingRectWithSize:size
                      options:NSStringDrawingUsesLineFragmentOrigin
                      attributes:@{NSFontAttributeName:self.font}
                      context:nil].size.width;
#else
        
        stringWidth = [self.text sizeWithFont:self.font
                            constrainedToSize:size
                                lineBreakMode:NSLineBreakByCharWrapping].width;
#endif
    }
    return stringWidth;
}
@end

創(chuàng)建一個label并實現計算label寬度的方法。

7. willMoveToSuperview

上面說了,MJRefreshComponent.m文件方法調用順序是:
alloc(prepare) -> setState -> willMoveToSuperView -> layoutSubViews(placeSubviews) -> drawRect

- (void)willMoveToSuperview:(UIView *)newSuperview
{
    [super willMoveToSuperview:newSuperview];
    
    // 如果不是UIScrollView,不做任何事情
    if (newSuperview && ![newSuperview isKindOfClass:[UIScrollView class]]) return;
    
    // 舊的父控件移除監(jiān)聽
    [self removeObservers];
    
    if (newSuperview) { // 新的父控件
        //mj_x mj_w 的設置出現在了MJRefreshComponent的willMoveToSuperView方法中,因為這兩個值始終是不會去變的。雖然可能會橫豎屏切換,但是autoresizingMask的設置就解決了這個問題,MJRefresh的水平方向的布局始終是定下來了。
        // 設置寬度
        self.mj_w = newSuperview.mj_w;
        // 設置位置
        self.mj_x = -_scrollView.mj_insetL;
        
        // 記錄UIScrollView
        _scrollView = (UIScrollView *)newSuperview;
        // 設置永遠支持垂直彈簧效果
        _scrollView.alwaysBounceVertical = YES;
        // 記錄UIScrollView最開始的contentInset
        _scrollViewOriginalInset = _scrollView.mj_inset;
        
        // 添加監(jiān)聽
        [self addObservers];
    }
}

- (void)drawRect:(CGRect)rect
{
    [super drawRect:rect];
    //到底這個MJRefreshStateWillRefresh標記會有什么樣的影響?drawRect是在最后才回去調用的,此時視圖已經被添加到父視圖了。通過這種方法,延緩了MJRefresh的刷新時間,從而保證了父視圖的存在。
    if (self.state == MJRefreshStateWillRefresh) {
        // 預防view還沒顯示出來就調用了beginRefreshing
        self.state = MJRefreshStateRefreshing;
    }
}

這個方法在UIView的整個生命周期中是會調用兩次,一次是子視圖即將添加到父視圖上的時候,還有一次是子視圖即將從父視圖移除的時候(他們的區(qū)別就是添加的時候newSuperview是有值的,移除的時候newSuperview沒有值)。

可能有的小伙伴會對這個地方產生疑惑,為什么要把這些初始化操作放在這個里面?不能直接放在初始化方法中嗎?
其實只要想一下MJRerfesh的服務對象就知道了,這里是判斷父視圖是不是scrollView以及其子類的最佳位置,放在初始化方法中沒法判斷父視圖,放在layoutSubViews中則太晚了,而且會調用多次。

8. KVO監(jiān)聽

#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];
    }
}

- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{}
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change{}
- (void)scrollViewPanStateDidChange:(NSDictionary *)change{}

這里監(jiān)聽了contentOffset和contentSize以及scrollView的pan手勢的狀態(tài),監(jiān)聽到改變會調用相應的方法,只不過MJRefreshComponent里面單純只是實現了這三個方法,相應的邏輯處理都在子類。

三. 下拉刷新

關于MJRefresh的繼承關系,可以看MJ老師自己畫的圖:
結構圖.png

MJRefreshComponent是不能直接做下拉刷新的,它的子類才可以。

1. MJRefreshHeader

直接繼承于MJRefreshComponent
下拉刷新控件,負責監(jiān)控用戶下拉的狀態(tài),這個控件沒添加子控件,直接使用是空白。

2. MJRefreshStateHeader

繼承于MJRefreshHeader
這個控件添加了兩個label,一個顯示刷新時間,一個顯示刷新狀態(tài),效果圖如下:

MJRefreshStateHeader.png

① MJRefreshNormalHeader

繼承于MJRefreshStateHeader,這個控件在兩個label的基礎上又添加了箭頭和菊花,效果圖如下:

MJRefreshNormalHeader.png

② MJRefreshGifHeader

也是繼承于MJRefreshStateHeader,這個控件在兩個label的基礎上又添加了Gif圖片,使用的時候需要子類化這個控件重寫prepare方法。

- (void)prepare
{
    [super prepare];
    
    // 設置普通狀態(tài)的動畫圖片
    NSMutableArray *idleImages = [NSMutableArray array];
    for (NSUInteger i = 1; i<=60; i++) {
        UIImage *image = [UIImage imageNamed:[NSString stringWithFormat:@"dropdown_anim__000%zd", I]];
        [idleImages addObject:image];
    }
     [self setImages:idleImages forState:MJRefreshStateIdle];
    
    // 設置即將刷新狀態(tài)的動畫圖片(一松開就會刷新的狀態(tài))
    NSMutableArray *refreshingImages = [NSMutableArray array];
    for (NSUInteger i = 1; i<=3; i++) {
        UIImage *image = [UIImage imageNamed:[NSString stringWithFormat:@"dropdown_loading_0%zd", I]];
        [refreshingImages addObject:image];
    }
    [self setImages:refreshingImages forState:MJRefreshStatePulling];
    
    // 設置正在刷新狀態(tài)的動畫圖片
    [self setImages:refreshingImages forState:MJRefreshStateRefreshing];
}

效果圖如下:
MJRefreshGifHeader.png

可以看出,如果我們想自定義下拉刷新,可以根據需求自定義控件繼承于MJRefreshStateHeader、MJRefreshNormalHeader、MJRefreshGifHeader。

四. 上拉刷新

關于MJRefresh的繼承關系,可以看MJ老師自己畫的圖:
結構圖.png

MJRefreshComponent是不能直接做下拉刷新的,它的子類才可以。

1. MJRefreshFooter

繼承于MJRefreshComponent
上拉刷新控件的根控件,實現了創(chuàng)建上拉控件的方法以及抽取了上拉控件必須的方法。

2. MJRefreshAutoFooter

繼承于MJRefreshFooter
會自動刷新的上拉刷新控件,不需要手動釋放才刷新,不會回彈到底部,沒添加子控件直接使用是空白。

MJRefreshAutoStateFooter

繼承于MJRefreshAutoFooter
添加了一個label的上拉刷新,示意圖如下:

MJRefreshAutoStateFooter.png

① MJRefreshAutoNormalFooter

繼承于MJRefreshAutoStateFooter
在一個label的基礎上又添加了菊花的上拉刷新,示意圖如下:

MJRefreshAutoNormalFooter.png

② MJRefreshAutoGifFooter

繼承于MJRefreshAutoStateFooter
在一個label的基礎上又添加imageV動圖的上拉刷新,示意圖如下:

MJRefreshAutoGifFooter.png

3. MJRefreshBackFooter

繼承于MJRefreshFooter
上拉需要手動釋放才會刷新的上拉刷新控件,會回彈到底部,沒添加子控件直接使用是空白。

MJRefreshBackStateFooter

繼承于MJRefreshBackFooter
添加了一個label的上拉刷新,示意圖如下:

MJRefreshBackStateFooter.png

① MJRefreshBackNormalFooter

繼承于MJRefreshBackStateFooter
在一個label的基礎上又添加箭頭和菊花的上拉刷新,示意圖如下:

MJRefreshBackNormalFooter.png

② MJRefreshBackGifFooter

也是繼承于MJRefreshBackStateFooter
在一個label的基礎上又添加了imageV動圖的上拉刷新,示意圖如下:

MJRefreshBackGifFooter.png

如果我們想自定義上拉刷新,可以根據需求自定義上拉控件繼承于StateFooter、NormalFooter、GifFooter。

github地址:MJRefresh

待完整...

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

友情鏈接更多精彩內容