李明杰的MJRefresh應(yīng)該也算是iOS中使用最廣泛的一個框架了,而且MJ的框架也用了好多中文注釋成福,這點讓我感覺到很親切碾局,網(wǎng)上也有好多分析的文章砌溺,但是別人的畢竟沒自己的印象深刻锡搜,現(xiàn)在分析一下MJRefresh每一句代碼是干什么的贾虽?為什么要這么寫挟伙?
想要寫一個好的框架需要注意兩點:1. 易用性強 2. 可定制性強 3. 設(shè)計合理
易用性強才會有人愿意使用豁陆,可定制性強才會有更多的場景可以使用研侣,設(shè)計合理以后修改的時候才不會太麻煩矢劲,別人也很容易理解蒲拉。
一. 如何實現(xiàn)下拉刷新
- 利用contentOffset
首先如何實現(xiàn)下拉刷新潭苞,很顯然下拉的時候scrollView 的contentOffset會改變埋合,我們可以監(jiān)聽這個值的變化來給scrollView添加一個mj_header并實現(xiàn)相應(yīng)的動畫效果。 - 如何添加mj_header
現(xiàn)在目標是給scrollView添加一個MJRefreshHeader萄传,只要給scrollView添加MJRefreshHeader甚颂,其他tableView和collectionView就都有了,但是系統(tǒng)的scrollView沒有這個屬性秀菱,這時候我們可以通過給scrollView的分類添加關(guān)聯(lián)對象的方式振诬,來實現(xiàn)給scrollView添加一個屬性,具體可參考UIScrollView+MJRefresh代碼衍菱。 - mj_header添加在contentInset.top的位置赶么。
- 為什么要有MJRefreshComponent
接下來我們就想如何去寫一個MJRefreshHeader,顯然脊串,我們不但有下拉刷新還有上拉刷新辫呻,這時候我們就需要一個baseView,這個baseView就是MJRefreshComponent琼锋,我們的上拉和下拉刷新控件都繼承于這個MJRefreshComponent放闺,MJRefreshComponent繼承于UIView是最基礎(chǔ)的基類,所以關(guān)于上拉下拉所有唯一共用的東西我們都可以寫在這里面缕坎。
接下來的事情就很簡單了怖侦,我們可以層層繼承,在合適的類添加合適的控件實現(xiàn)合適的方法谜叹。
二. MJRefreshComponent
1. 定義的東西和成員變量
/** 刷新控件的狀態(tài) */
typedef NS_ENUM(NSInteger, MJRefreshState) {
/** 普通閑置狀態(tài) */
MJRefreshStateIdle = 1,
/** 松開就可以進行刷新的狀態(tài) */
MJRefreshStatePulling,
/** 正在刷新中的狀態(tài) */
MJRefreshStateRefreshing,
/** 即將刷新的狀態(tài) */
MJRefreshStateWillRefresh,
/** 所有數(shù)據(jù)加載完畢匾寝,沒有更多的數(shù)據(jù)了 */
MJRefreshStateNoMoreData
};
/** 進入刷新狀態(tài)的回調(diào) */
typedef void (^MJRefreshComponentRefreshingBlock)(void);
/** 開始刷新后的回調(diào)(進入刷新狀態(tài)后的回調(diào)) */
typedef void (^MJRefreshComponentbeginRefreshingCompletionBlock)(void);
/** 結(jié)束刷新后的回調(diào) */
typedef void (^MJRefreshComponentEndRefreshingCompletionBlock)(void);
/** 刷新控件的基類 */
@interface MJRefreshComponent : UIView
{
/** 記錄scrollView剛開始的inset */
UIEdgeInsets _scrollViewOriginalInset;
/** 父控件 */
__weak UIScrollView *_scrollView;
}
- 首先定義了刷新狀態(tài)MJRefreshState和三個刷新狀態(tài)的block,這個很容易理解荷腊,每個刷新控件一定有這些東西艳悔。
- 另外還定義了兩個成員變量
① _scrollViewOriginalInset這個值記錄scrollView剛開始的inset,這個值會在scrollViewContentOffsetDidChange方法里面使用到女仰,用來設(shè)置mj_header的位置猜年。
② _scrollView就是父控件香府,弱引用。
2. 刷新回調(diào)
#pragma mark - 刷新回調(diào)
/** 正在刷新的回調(diào) */
@property (copy, nonatomic) MJRefreshComponentRefreshingBlock refreshingBlock;
/** 設(shè)置回調(diào)對象和回調(diào)方法 */
- (void)setRefreshingTarget:(id)target refreshingAction:(SEL)action;
/** 回調(diào)對象 */
@property (weak, nonatomic) id refreshingTarget;
/** 回調(diào)方法 */
@property (assign, nonatomic) SEL refreshingAction;
/** 觸發(fā)回調(diào)(交給子類去調(diào)用) */
- (void)executeRefreshingCallback;
這里定義了刷新回調(diào)码倦,以及回調(diào)方法和回調(diào)對象企孩,主要介紹executeRefreshingCallback方法:
#pragma mark - 內(nèi)部方法
//執(zhí)行刷新回調(diào),不同子類都會調(diào)用,所以抽取到父類
- (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í)行刷新回調(diào),不同子類都會調(diào)用袁稽,所以抽取到父類里面勿璃。
3. 刷新狀態(tài)控制
#pragma mark - 刷新狀態(tài)控制
/** 進入刷新狀態(tài) */
- (void)beginRefreshing;
- (void)beginRefreshingWithCompletionBlock:(void (^)(void))completionBlock;
/** 開始刷新后的回調(diào)(進入刷新狀態(tài)后的回調(diào)) */
@property (copy, nonatomic) MJRefreshComponentbeginRefreshingCompletionBlock beginRefreshingCompletionBlock;
/** 結(jié)束刷新的回調(diào) */
@property (copy, nonatomic) MJRefreshComponentEndRefreshingCompletionBlock endRefreshingCompletionBlock;
/** 結(jié)束刷新狀態(tài) */
- (void)endRefreshing;
- (void)endRefreshingWithCompletionBlock:(void (^)(void))completionBlock;
/** 是否正在刷新 */
@property (assign, nonatomic, readonly, getter=isRefreshing) BOOL refreshing;
//- (BOOL)isRefreshing;
/** 刷新狀態(tài) 一般交給子類內(nèi)部實現(xiàn) */
@property (assign, nonatomic) MJRefreshState state;
這里定義了開始結(jié)束刷新的方法以及開始結(jié)束刷新的block,定義了刷新狀態(tài)以及是否正在刷新的BOOL值來控制刷新狀態(tài)推汽。
① setState:
一般交給子類內(nèi)部實現(xiàn)补疑,不同狀態(tài)做不同的事情。
- (void)setState:(MJRefreshState)state
{
_state = state;
// 加入主隊列的目的是等setState:方法調(diào)用完畢歹撒、設(shè)置完文字后再去布局子控件
//因為文字的變化會引起左側(cè)箭頭位置的變化莲组,這時候需要刷新來重制位置。
MJRefreshDispatchAsyncOnMainQueue([self setNeedsLayout];)
}
② beginRefreshing:
他其實主要就是把state標記為MJRefreshStateRefreshing暖夭。但是它還做了另外一層判斷:window的有無锹杈。MJ 也做了備注,說明了為什么要有這個判斷迈着,主要是因為預(yù)防用戶過早的調(diào)用了beginRefresh方法竭望,然而這時候自身還并沒有顯示出來,所以巧妙的先將state標記為了MJRefreshStateWillRefresh裕菠。
- (void)beginRefreshing
{
[UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
self.alpha = 1.0;
}];
self.pullingPercent = 1.0;
// 只要正在刷新咬清,就完全顯示
if (self.window) {
self.state = MJRefreshStateRefreshing;
} else {
// 預(yù)防正在刷新中時,調(diào)用本方法使得header inset回置失敗
if (self.state != MJRefreshStateRefreshing) {
self.state = MJRefreshStateWillRefresh;
// 刷新(預(yù)防從另一個控制器回到這個控制器的情況奴潘,回來要重新刷新一下)
[self setNeedsDisplay];
}
}
}
4. 交給子類實現(xiàn)
#pragma mark - 交給子類們?nèi)崿F(xiàn)
/** 初始化 */
- (void)prepare NS_REQUIRES_SUPER;
/** 擺放子控件frame */
- (void)placeSubviews NS_REQUIRES_SUPER;
/** 當scrollView的contentOffset發(fā)生改變的時候調(diào)用 */
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;
/** 當scrollView的contentSize發(fā)生改變的時候調(diào)用 */
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;
/** 當scrollView的拖拽狀態(tài)發(fā)生改變的時候調(diào)用 */
- (void)scrollViewPanStateDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;
這些方法主要是交給子類來實現(xiàn)旧烧,這里只實現(xiàn)了prepare方法:
- (void)prepare
{
// 基本屬性
//保證在橫豎屏切換的時候能夠保證自身相對于父視圖的左右邊距保持不變,這個方法是每個子類都必須的画髓,所以放在了基類MJRefreshComponent中掘剪。
self.autoresizingMask = UIViewAutoresizingFlexibleWidth;
self.backgroundColor = [UIColor clearColor];
}
方法的調(diào)用順序是:
alloc(prepare) -> setState -> willMoveToSuperView -> layoutSubViews(placeSubviews) -> drawRect
5. 拖拽百分比和透明度
#pragma mark - 其他
/** 拉拽的百分比(交給子類重寫) */
@property (assign, nonatomic) CGFloat pullingPercent;
/** 根據(jù)拖拽比例自動切換透明度 */
@property (assign, nonatomic, getter=isAutoChangeAlpha) BOOL autoChangeAlpha MJRefreshDeprecated("請使用automaticallyChangeAlpha屬性");
/** 根據(jù)拖拽比例自動切換透明度 */
@property (assign, nonatomic, getter=isAutomaticallyChangeAlpha) BOOL automaticallyChangeAlpha;
@end
這里有拖拽百分比和自動切換透明度,pullingPercent一般交給子類來實現(xiàn)雀扶,根據(jù)拖拽的比例來實現(xiàn)個性定制杖小,默認是根據(jù)拖拽百分比自動切換透明度,如下:
- (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
實現(xiàn)了兩個方法:
@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并實現(xiàn)計算label寬度的方法愚墓。
7. willMoveToSuperview
上面說了,MJRefreshComponent.m文件方法調(diào)用順序是:
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 的設(shè)置出現(xiàn)在了MJRefreshComponent的willMoveToSuperView方法中浪册,因為這兩個值始終是不會去變的。雖然可能會橫豎屏切換岗照,但是autoresizingMask的設(shè)置就解決了這個問題村象,MJRefresh的水平方向的布局始終是定下來了笆环。
// 設(shè)置寬度
self.mj_w = newSuperview.mj_w;
// 設(shè)置位置
self.mj_x = -_scrollView.mj_insetL;
// 記錄UIScrollView
_scrollView = (UIScrollView *)newSuperview;
// 設(shè)置永遠支持垂直彈簧效果
_scrollView.alwaysBounceVertical = YES;
// 記錄UIScrollView最開始的contentInset
_scrollViewOriginalInset = _scrollView.mj_inset;
// 添加監(jiān)聽
[self addObservers];
}
}
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
//到底這個MJRefreshStateWillRefresh標記會有什么樣的影響?drawRect是在最后才回去調(diào)用的厚者,此時視圖已經(jīng)被添加到父視圖了躁劣。通過這種方法,延緩了MJRefresh的刷新時間库菲,從而保證了父視圖的存在账忘。
if (self.state == MJRefreshStateWillRefresh) {
// 預(yù)防view還沒顯示出來就調(diào)用了beginRefreshing
self.state = MJRefreshStateRefreshing;
}
}
這個方法在UIView的整個生命周期中是會調(diào)用兩次,一次是子視圖即將添加到父視圖上的時候熙宇,還有一次是子視圖即將從父視圖移除的時候(他們的區(qū)別就是添加的時候newSuperview是有值的鳖擒,移除的時候newSuperview沒有值)。
可能有的小伙伴會對這個地方產(chǎn)生疑惑烫止,為什么要把這些初始化操作放在這個里面蒋荚?不能直接放在初始化方法中嗎?
其實只要想一下MJRerfesh的服務(wù)對象就知道了馆蠕,這里是判斷父視圖是不是scrollView以及其子類的最佳位置期升,放在初始化方法中沒法判斷父視圖,放在layoutSubViews中則太晚了互躬,而且會調(diào)用多次吓妆。
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)聽到改變會調(diào)用相應(yīng)的方法吨铸,只不過MJRefreshComponent里面單純只是實現(xiàn)了這三個方法行拢,相應(yīng)的邏輯處理都在子類。
三. 下拉刷新
關(guān)于MJRefresh的繼承關(guān)系诞吱,可以看MJ老師自己畫的圖:MJRefreshComponent是不能直接做下拉刷新的舟奠,它的子類才可以。
1. MJRefreshHeader
直接繼承于MJRefreshComponent
下拉刷新控件房维,負責(zé)監(jiān)控用戶下拉的狀態(tài)沼瘫,這個控件沒添加子控件,直接使用是空白咙俩。
2. MJRefreshStateHeader
繼承于MJRefreshHeader
這個控件添加了兩個label耿戚,一個顯示刷新時間,一個顯示刷新狀態(tài)阿趁,效果圖如下:
① MJRefreshNormalHeader
繼承于MJRefreshStateHeader膜蛔,這個控件在兩個label的基礎(chǔ)上又添加了箭頭和菊花,效果圖如下:
② MJRefreshGifHeader
也是繼承于MJRefreshStateHeader脖阵,這個控件在兩個label的基礎(chǔ)上又添加了Gif圖片皂股,使用的時候需要子類化這個控件重寫prepare方法。
- (void)prepare
{
[super prepare];
// 設(shè)置普通狀態(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];
// 設(shè)置即將刷新狀態(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];
// 設(shè)置正在刷新狀態(tài)的動畫圖片
[self setImages:refreshingImages forState:MJRefreshStateRefreshing];
}
效果圖如下:可以看出命黔,如果我們想自定義下拉刷新呜呐,可以根據(jù)需求自定義控件繼承于MJRefreshStateHeader就斤、MJRefreshNormalHeader、MJRefreshGifHeader蘑辑。
四. 上拉刷新
關(guān)于MJRefresh的繼承關(guān)系洋机,可以看MJ老師自己畫的圖:MJRefreshComponent是不能直接做下拉刷新的,它的子類才可以洋魂。
1. MJRefreshFooter
繼承于MJRefreshComponent
上拉刷新控件的根控件绷旗,實現(xiàn)了創(chuàng)建上拉控件的方法以及抽取了上拉控件必須的方法。
2. MJRefreshAutoFooter
繼承于MJRefreshFooter
會自動刷新的上拉刷新控件忧设,不需要手動釋放才刷新刁标,不會回彈到底部,沒添加子控件直接使用是空白址晕。
MJRefreshAutoStateFooter
繼承于MJRefreshAutoFooter
添加了一個label的上拉刷新膀懈,示意圖如下:
① MJRefreshAutoNormalFooter
繼承于MJRefreshAutoStateFooter
在一個label的基礎(chǔ)上又添加了菊花的上拉刷新,示意圖如下:
② MJRefreshAutoGifFooter
繼承于MJRefreshAutoStateFooter
在一個label的基礎(chǔ)上又添加imageV動圖的上拉刷新谨垃,示意圖如下:
3. MJRefreshBackFooter
繼承于MJRefreshFooter
上拉需要手動釋放才會刷新的上拉刷新控件启搂,會回彈到底部,沒添加子控件直接使用是空白刘陶。
MJRefreshBackStateFooter
繼承于MJRefreshBackFooter
添加了一個label的上拉刷新胳赌,示意圖如下:
① MJRefreshBackNormalFooter
繼承于MJRefreshBackStateFooter
在一個label的基礎(chǔ)上又添加箭頭和菊花的上拉刷新,示意圖如下:
② MJRefreshBackGifFooter
也是繼承于MJRefreshBackStateFooter
在一個label的基礎(chǔ)上又添加了imageV動圖的上拉刷新匙隔,示意圖如下:
如果我們想自定義上拉刷新疑苫,可以根據(jù)需求自定義上拉控件繼承于StateFooter、NormalFooter纷责、GifFooter捍掺。
github地址:MJRefresh
待完整...