MJRefresh
是李明傑大神的開源框架,這是一款十分優雅的刷新組件庫,這開源組件不管從代碼風格,可用性,易讀性仍是兼容性來說都十分優秀。本文就最新MJRefresh
版原本講解。耐心看下去,本文和純解讀源碼的文章不一樣。本文碼字幾天,若是對您有幫助,給個鼓勵,謝謝你們!bash
#import <UIKit/UIKit.h>
#import "MJRefreshConst.h"
#import "UIView+MJExtension.h"
#import "UIScrollView+MJExtension.h"
#import "UIScrollView+MJRefresh.h"
#import "NSBundle+MJRefresh.h"
複製代碼
/** 刷新控件的狀態 */
typedef NS_ENUM(NSInteger, MJRefreshState) {
/** 普通閒置狀態 */
MJRefreshStateIdle = 1,
/** 鬆開就能夠進行刷新的狀態 */
MJRefreshStatePulling,
/** 正在刷新中的狀態 */
MJRefreshStateRefreshing,
/** 即將刷新的狀態 */
MJRefreshStateWillRefresh,
/** 全部數據加載完畢,沒有更多的數據了 */
MJRefreshStateNoMoreData
};
複製代碼
#pragma mark - 刷新回調
/** 正在刷新的回調 */
@property (copy, nonatomic) MJRefreshComponentRefreshingBlock refreshingBlock;
/** 設置回調對象和回調方法 */
- (void)setRefreshingTarget:(id)target refreshingAction:(SEL)action;
/** 回調對象 */
@property (weak, nonatomic) id refreshingTarget;
/** 回調方法 */
@property (assign, nonatomic) SEL refreshingAction;
/** 觸發回調(交給子類去調用) */
- (void)executeRefreshingCallback;
複製代碼
#pragma mark - 刷新狀態控制
/** 進入刷新狀態 */
- (void)beginRefreshing;
- (void)beginRefreshingWithCompletionBlock:(void (^)(void))completionBlock;
/** 開始刷新後的回調(進入刷新狀態後的回調) */
@property (copy, nonatomic) MJRefreshComponentbeginRefreshingCompletionBlock beginRefreshingCompletionBlock;
/** 結束刷新的回調 */
@property (copy, nonatomic) MJRefreshComponentEndRefreshingCompletionBlock endRefreshingCompletionBlock;
/** 結束刷新狀態 */
- (void)endRefreshing;
- (void)endRefreshingWithCompletionBlock:(void (^)(void))completionBlock;
/** 是否正在刷新 */
@property (assign, nonatomic, readonly, getter=isRefreshing) BOOL refreshing;
//- (BOOL)isRefreshing;
/** 刷新狀態 通常交給子類內部實現 */
@property (assign, nonatomic) MJRefreshState state;
複製代碼
具體方法分析:app
#pragma mark 進入刷新狀態
- (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];
}
}
}
複製代碼
上面作了一個動畫效果,多加了一個
willRefresh
的狀態,個人理解是爲了防止self.window爲空的時候,忽然刷新崩潰(從另外一個頁面返回的時候),因此須要一個狀態來過渡。框架
設置
state
會調用setNeedsLayout
方法;若是self.window
爲空,把狀態改爲即將刷新,並調用setNeedsDisplay
iphone
- 首先
UIView
的setNeedsDisplay
和setNeedsLayout
方法都是異步執行的。而setNeedsDisplay
會調用自動調用drawRect
方法,這樣能夠拿到UIGraphicsGetCurrentContext
,就能夠繪製了,而setNeedsLayout
會默認調用layoutSubViews
,就能夠處理子視圖中的一些數據。- 綜上所訴,
setNeedsDisplay
方便繪圖,而layoutSubViews
方便出來數據。
//結束刷新
- (void)endRefreshing
{
dispatch_async(dispatch_get_main_queue(), ^{
self.state = MJRefreshStateIdle;
});
}
複製代碼
在主線程結束刷新,把刷新狀態改成普通閒置狀態異步
#pragma mark - KVO監聽
- (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];
}
}
複製代碼
監聽
ContentOffset
、ContentSize
、手勢的State
async
#pragma mark - 內部方法
- (void)executeRefreshingCallback
{
dispatch_async(dispatch_get_main_queue(), ^{
if (self.refreshingBlock) {
self.refreshingBlock();
}
if ([self.refreshingTarget respondsToSelector:self.refreshingAction]) {
MJRefreshMsgSend(MJRefreshMsgTarget(self.refreshingTarget), self.refreshingAction, self);
}
if (self.beginRefreshingCompletionBlock) {
self.beginRefreshingCompletionBlock();
}
});
}
複製代碼
MJRefreshMsgSend
是時運行時objc_msgSend,第一個參數表明接收者,第二個參數表明選擇子(SEL是選擇子的類型),後續參數就是消息中的那些參數,其順序不變。選擇子指的就是方法的名字。ide
#pragma mark - 構造方法
+ (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;
}
複製代碼
- (void)prepare
{
[super prepare];
// 設置key
self.lastUpdatedTimeKey = MJRefreshHeaderLastUpdatedTimeKey;
// 設置高度
self.mj_h = MJRefreshHeaderHeight;
}
- (void)placeSubviews
{
[super placeSubviews];
// 設置y值(當本身(頭部)的高度發生改變了,確定要從新調整Y值,因此放到placeSubviews方法中設置y值)
self.mj_y = - self.mj_h - self.ignoredScrollViewContentInsetTop;
}
複製代碼
prepare
設置一下初始化值數據。而placeSubViews
更新一下UI。函數
//當scrollView的contentOffset發生改變的時候調用
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
[super scrollViewContentOffsetDidChange:change];
// 在刷新的refreshing狀態
if (self.state == MJRefreshStateRefreshing) {
// 暫時保留
if (self.window == nil) return;
// sectionheader停留解決
//刷新的時候:偏移量(self.scrollView.mj_offsetY) = 狀態欄 + 導航欄 + header的高度(54+64=118 iphoneX則爲54+88=142)
//內邊距高度(_scrollViewOriginalInset.top)= 狀態欄 + 導航欄 = 64
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;
}
// 跳轉到下一個控制器時,contentInset可能會變
_scrollViewOriginalInset = self.scrollView.mj_inset;
// 當前的contentOffset
CGFloat offsetY = self.scrollView.mj_offsetY;
// 頭部控件恰好出現的offsetY
CGFloat happenOffsetY = - self.scrollViewOriginalInset.top;
// 若是是向上滾動到看不見頭部控件,直接返回
// >= -> >
if (offsetY > happenOffsetY) return;
// 普通閒置 即將刷新 的臨界點
//我的以爲normal2pullingOffsetY應該是頭部徹底出來時的Y軸偏移值
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) { //手指拖拽中,狀態是默認狀態以及下拉距離(偏移值)大於臨界點距離
// 轉爲能夠進行刷新狀態
self.state = MJRefreshStatePulling;
} else if (self.state == MJRefreshStatePulling && offsetY >= normal2pullingOffsetY) {
//手指拖拽中,狀態是默認狀態以及下拉距離(偏移值)小於臨界點距離,也就是拖得比較下
// 轉爲普通狀態
self.state = MJRefreshStateIdle;
}
} else if (self.state == MJRefreshStatePulling) {// 即將刷新 && 手鬆開
// 開始刷新
[self beginRefreshing];
} else if (pullingPercent < 1) {
self.pullingPercent = pullingPercent;//手鬆開後,默認狀態時,恢復self.pullingPercent
}
}
複製代碼
狀態切換的因素有兩個:一個是下拉的距離是否超過臨界值,另外一個是 手指是否離開屏幕。佈局
手指還貼在屏幕的時候是不能進行刷新的。即便在下拉的距離超過了臨界距離(狀態欄 + 導航欄 + header高度),若是手指沒有離開屏幕,那麼也不能立刻進行刷新,而是將狀態切換爲:能夠刷新。一旦手指離開了屏幕,立刻將狀態切換爲正在刷新。post
普通閒置與即將刷新的分界點,看下圖,一目瞭然
- (void)setState:(MJRefreshState)state
{
MJRefreshCheckState
//MJRefreshCheckState是宏,其實也就是下面語句,爲了檢測狀態是否相同,相同則return
// MJRefreshState oldState = self.state;
// if (state == oldState) {
// NSLog(@"相同");
// return;
// }
// [super setState:state];
// 根據狀態作事情
if (state == MJRefreshStateIdle) {
if (oldState != MJRefreshStateRefreshing) return;
// 保存刷新時間
[[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:self.lastUpdatedTimeKey];
[[NSUserDefaults standardUserDefaults] synchronize];
// 恢復inset和offset
[UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
//此時要加上scrollView刷新時跟普通閒置時的偏移差值(刷新時偏移值爲118或者142,self.insetTDelta值爲header高度-54),恢復後self.scrollView.mj_insetT = 64(或者88)
self.scrollView.mj_insetT += self.insetTDelta;
// 自動調整透明度
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;
// 增長滾動區域top
self.scrollView.mj_insetT = top;
//增長滾動區域top(賦值給scrollView.inset.top)
CGPoint offset = self.scrollView.contentOffset;
offset.y = -top;
[self.scrollView setContentOffset:offset animated:NO];
} completion:^(BOOL finished) {
//執行正在刷新的回調
[self executeRefreshingCallback];
}];
});
}
}
複製代碼
注意
[super setState:state]
的位置,等基類的state
賦值給oldState
,再跟新狀態對比,對比完後,再[super setState:state]
調用基類,從而賦值基類state
該方法主要要注意狀態在普通閒置狀態以及刷新狀態的scrollView.contentOffset變化
該類是MJRefreshHeader
的子類,主要用來設置顯示上一次刷新時間的label:lastUpdatedTimeLabel
和顯示刷新狀態的label:stateLabel
屬性等
- (void)setTitle:(NSString *)title forState:(MJRefreshState)state
{
if (title == nil) return;
self.stateTitles[@(state)] = title;
self.stateLabel.text = self.stateTitles[@(self.state)];
}
#pragma mark - 覆蓋父類的方法
- (void)prepare
{
[super prepare];
// 初始化間距
self.labelLeftInset = MJRefreshLabelLeftInset;
// 初始化文字
[self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderIdleText] forState:MJRefreshStateIdle];
[self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderPullingText] forState:MJRefreshStatePulling];
[self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderRefreshingText] forState:MJRefreshStateRefreshing];
}
複製代碼
prepare
初始化方法,實現本地化(不一樣字體),並根據不一樣狀態賦值給stateLabel
#pragma mark key的處理
- (void)setLastUpdatedTimeKey:(NSString *)lastUpdatedTimeKey
{
[super setLastUpdatedTimeKey:lastUpdatedTimeKey];
// 若是label隱藏了,就不用再處理
if (self.lastUpdatedTimeLabel.hidden) return;
NSDate *lastUpdatedTime = [[NSUserDefaults standardUserDefaults] objectForKey:lastUpdatedTimeKey];
// 若是有block
//用戶定義的時間格式
if (self.lastUpdatedTimeText) {
self.lastUpdatedTimeLabel.text = self.lastUpdatedTimeText(lastUpdatedTime);
return;
}
if (lastUpdatedTime) {
// 1.得到年月日
NSCalendar *calendar = [self currentCalendar];
NSUInteger unitFlags = NSCalendarUnitYear| NSCalendarUnitMonth | NSCalendarUnitDay |NSCalendarUnitHour |NSCalendarUnitMinute;
NSDateComponents *cmp1 = [calendar components:unitFlags fromDate:lastUpdatedTime];
NSDateComponents *cmp2 = [calendar components:unitFlags fromDate:[NSDate date]];
// 2.格式化日期
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
BOOL isToday = NO;
if ([cmp1 day] == [cmp2 day]) { // 今天
formatter.dateFormat = @" HH:mm"; //返回11:11樣式
isToday = YES;
} else if ([cmp1 year] == [cmp2 year]) { // 今年
formatter.dateFormat = @"MM-dd HH:mm"; //返回02-08 11:11樣式
} else {
formatter.dateFormat = @"yyyy-MM-dd HH:mm"; //返回2018-02-08 11:11樣式
}
NSString *time = [formatter stringFromDate:lastUpdatedTime];
// 3.顯示日期
//[NSBundle mj_localizedStringForKey:MJRefreshHeaderLastTimeText] 會返回簡體(英文、繁體)的 【最後更新:】
//isToday ? [NSBundle mj_localizedStringForKey:MJRefreshHeaderDateTodayText] : @"" 若是上一次刷新也是今天,則返回簡體(英文、繁體)的 【今天】,不是則返回空字符串
self.lastUpdatedTimeLabel.text = [NSString stringWithFormat:@"%@%@%@",
[NSBundle mj_localizedStringForKey:MJRefreshHeaderLastTimeText],
isToday ? [NSBundle mj_localizedStringForKey:MJRefreshHeaderDateTodayText] : @"",
time];
} else {
//沒有得到上次更新時間
self.lastUpdatedTimeLabel.text = [NSString stringWithFormat:@"%@%@",
[NSBundle mj_localizedStringForKey:MJRefreshHeaderLastTimeText],
[NSBundle mj_localizedStringForKey:MJRefreshHeaderNoneLastDateText]];
}
}
複製代碼
注意一下時間格式,本地化以及不一樣上次刷新時間的
lastUpdatedTimeLabel
顯示 上面代碼還給用戶自定義時間格式,沒有才使用默認,默認的格式邏輯顯示,我已在上面註釋清楚
MJRefreshNormalHeader
和MJRefreshGifHeader
都是MJRefreshStateHeader
的子類,前者和後者的佈局同樣,不一樣的就是heade
r左邊一個是菊花的樣式,另一個是gif,詳看下圖:
header
都有相同的共性,咱們在作相似的功能時,若是有幾個控件或者幾個類共性同樣,好比說,一個保險類(InsuranceClass),一個房地產類(RealEstateClass),他們能夠有一個基類銷售類(SalesClass),SalesClass擁有銷售員工、顧客、金額、銷售日期等 保險類 和 房地產類 須要的共同屬性
一、在
MJRefreshStateHeader
上添加了箭頭和菊花
二、佈局這兩種樣式
View
,且在狀態切換時更改樣式切換
- (void)placeSubviews
{
[super placeSubviews];
// 箭頭的中心點
CGFloat arrowCenterX = self.mj_w * 0.5;
if (!self.stateLabel.hidden) {
CGFloat stateWidth = self.stateLabel.mj_textWith; //狀態label文字的寬度
CGFloat timeWidth = 0.0;
if (!self.lastUpdatedTimeLabel.hidden) {
timeWidth = self.lastUpdatedTimeLabel.mj_textWith; //時間label文字的寬度
}
CGFloat textWidth = MAX(stateWidth, timeWidth); //求出一個最寬的文字寬度
arrowCenterX -= textWidth / 2 + self.labelLeftInset; //箭頭(菊花)中心點x還要減去(最寬的文字寬度/2 + 文字距離圈圈、箭頭的距離)
}
//中心點y設置爲header的高度的一半
CGFloat arrowCenterY = self.mj_h * 0.5;
CGPoint arrowCenter = CGPointMake(arrowCenterX, arrowCenterY);
// 箭頭
if (self.arrowView.constraints.count == 0) { //箭頭沒有其餘佈局約束
self.arrowView.mj_size = self.arrowView.image.size; //箭頭大小跟提供的arrowView圖片大小一致
self.arrowView.center = arrowCenter;
}
// 圈圈
if (self.loadingView.constraints.count == 0) { //圈圈(菊花)沒有其餘佈局約束
self.loadingView.center = arrowCenter;
}
self.arrowView.tintColor = self.stateLabel.textColor;
}
複製代碼
上面代碼主要實現了圈圈(菊花)和箭頭的佈局,須要注意的是讓箭頭菊花緊跟刷新文字或者狀態文字居中的邏輯,我已在註釋寫明
- (void)setState:(MJRefreshState)state
{
MJRefreshCheckState
// 根據狀態作事情
if (state == MJRefreshStateIdle) {
if (oldState == MJRefreshStateRefreshing) { //上次狀態是正在刷新,準備改變成普通閒置狀態
self.arrowView.transform = CGAffineTransformIdentity; //仿射變換初始化
[UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
self.loadingView.alpha = 0.0; //把菊花變成徹底透明
} completion:^(BOOL finished) {
// 若是執行完動畫發現不是idle狀態,就直接返回,進入其餘狀態
if (self.state != MJRefreshStateIdle) return;
// self.loadingView.backgroundColor = [UIColor greenColor];
self.loadingView.alpha = 1.0; //菊花變成徹底顯示 (爲何要這樣?求大佬告訴)
[self.loadingView stopAnimating]; //菊花中止轉動,同時會隱藏菊花(loadingView.hidesWhenStopped = YES;)
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);//(改變箭頭的方向,可是爲何要0.000001 - M_PI?)
}];
} else if (state == MJRefreshStateRefreshing) { //正在刷新狀態:菊花徹底顯示而且開始轉動,箭頭隱藏
self.loadingView.alpha = 1.0; // 防止refreshing -> idle的動畫完畢動做沒有被執行
[self.loadingView startAnimating];
self.arrowView.hidden = YES;
}
}
複製代碼
經過不一樣的狀態控制菊花和箭頭的隱藏和消失,及他們的動畫效果,如箭頭的朝上朝下,和菊花的轉與不轉
一、加載不一樣狀態對應的動畫圖片 二、設置不一樣狀態對應的動畫時間
#pragma mark - 懶加載
//gigView顯示gif
- (UIImageView *)gifView
{
if (!_gifView) {
UIImageView *gifView = [[UIImageView alloc] init];
[self addSubview:_gifView = gifView];
}
return _gifView;
}
- (NSMutableDictionary *)stateImages
{
if (!_stateImages) {
self.stateImages = [NSMutableDictionary dictionary];
}
return _stateImages;
}
- (NSMutableDictionary *)stateDurations
{
if (!_stateDurations) {
self.stateDurations = [NSMutableDictionary dictionary];
}
return _stateDurations;
}
複製代碼
#pragma mark - 公共方法
- (void)setImages:(NSArray *)images duration:(NSTimeInterval)duration forState:(MJRefreshState)state
{
if (images == nil) return;
self.stateImages[@(state)] = images;
self.stateDurations[@(state)] = @(duration);
/* 根據圖片設置控件的高度 */
UIImage *image = [images firstObject];
if (image.size.height > self.mj_h) {
self.mj_h = image.size.height;
}
}
- (void)setImages:(NSArray *)images forState:(MJRefreshState)state
{
[self setImages:images duration:images.count * 0.1 forState:state];
}
複製代碼
#pragma mark - 實現父類的方法
- (void)prepare
{
[super prepare];
// 初始化間距
self.labelLeftInset = 20;
}
//根據拖拽進度設置透明度
- (void)setPullingPercent:(CGFloat)pullingPercent
{
[super setPullingPercent:pullingPercent];
NSArray *images = self.stateImages[@(MJRefreshStateIdle)]; //選擇閒置狀態下的圖片組
if (self.state != MJRefreshStateIdle || images.count == 0) return; //狀態不是閒置或者圖片爲空,則直接返回
// 中止動畫
[self.gifView stopAnimating];
// 設置當前須要顯示的圖片
NSUInteger index = images.count * pullingPercent;
if (index >= images.count) index = images.count - 1;
self.gifView.image = images[index];
}
- (void)placeSubviews
{
[super placeSubviews];
if (self.gifView.constraints.count) return; //gifView沒有約束,直接返回
self.gifView.frame = self.bounds;
if (self.stateLabel.hidden && self.lastUpdatedTimeLabel.hidden) { //上次刷新時間和狀態文字都隱藏了,圖片內容就居ifView中間顯示
self.gifView.contentMode = UIViewContentModeCenter;
} else { //圖片居gifView右邊顯示
self.gifView.contentMode = UIViewContentModeRight;
//下面代碼一樣也是爲了讓gifView緊挨着文字居中顯示。算出最長的文字,經過減去文字的通常寬度,調整gifView的x值,跟NormalHeader的方法同樣,詳細請看normalHeader
CGFloat stateWidth = self.stateLabel.mj_textWith;
CGFloat timeWidth = 0.0;
if (!self.lastUpdatedTimeLabel.hidden) {
timeWidth = self.lastUpdatedTimeLabel.mj_textWith;
}
CGFloat textWidth = MAX(stateWidth, timeWidth);
self.gifView.mj_w = self.mj_w * 0.5 - textWidth * 0.5 - self.labelLeftInset;
}
}
- (void)setState:(MJRefreshState)state
{
MJRefreshCheckState
// 根據狀態作事情
if (state == MJRefreshStatePulling || state == MJRefreshStateRefreshing) { //狀態變爲拖拽或者正在刷新,獲取各自狀態該顯示的圖片組
NSArray *images = self.stateImages[@(state)];
if (images.count == 0) return;
[self.gifView stopAnimating];
if (images.count == 1) { // 單張圖片
self.gifView.image = [images lastObject];
} else { // 多張圖片
self.gifView.animationImages = images;
self.gifView.animationDuration = [self.stateDurations[@(state)] doubleValue];
[self.gifView startAnimating];
}
} else if (state == MJRefreshStateIdle) {
[self.gifView stopAnimating];
}
}
複製代碼
到此,我對MJRefreshHeader
那一塊的源碼已經讀完,剩下MJRefreshFooter
,但因爲實現邏輯基本一致,故在此再也不詳說。遲點,發現MJRefreshFooter
有其餘特殊之處,我會更新此文,謝謝你們!
咱們可能見到一些開發者會在didSelectRowAtIndexPath
協議方法裏面這樣寫
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
MJExample *exam = self.examples[indexPath.section];
UIViewController *vc = [[exam.vcClass alloc] init];
vc.title = exam.titles[indexPath.row];
[vc setValue:exam.methods[indexPath.row] forKeyPath:@"method"];
[self.navigationController pushViewController:vc animated:YES];
if (indexPath.row == 0) {
UIViewController *test1 = [UIViewController new];
test1.title = @"test1";
[self.navigationController pushViewController:test1 animated:YES];
}else if (indexPath.row == 1) {
UIViewController *test2 = [UIViewController new];
test2.title = @"test2";
[self.navigationController pushViewController:test2 animated:YES];
}else if (indexPath.row == 2) {
UIViewController *test3 = [UIViewController new];
test3.title = @"test3";
[self.navigationController pushViewController:test3 animated:YES];
}else {
;
}
}
複製代碼
這樣會形成
didSelectRowAtIndexPath
方法過於臃腫,且重複代碼過多,太多if else 或者 switch,咱們能夠用Model很好的解決這個問題,代碼以下:
- (NSArray *)examples
{
if (!_examples) {
MJExample *exam0 = [[MJExample alloc] init];
exam0.header = MJExample00;
exam0.vcClass = [MJTableViewController class];
exam0.titles = @[@"默認", @"動畫圖片", @"隱藏時間", @"隱藏狀態和時間", @"自定義文字", @"自定義刷新控件"];
exam0.methods = @[@"example01", @"example02", @"example03", @"example04", @"example05", @"example06"];
MJExample *exam1 = [[MJExample alloc] init];
exam1.header = MJExample10;
exam1.vcClass = [MJTableViewController class];
exam1.titles = @[@"默認", @"動畫圖片", @"隱藏刷新狀態的文字", @"所有加載完畢", @"禁止自動加載", @"自定義文字", @"加載後隱藏", @"自動回彈的上拉01", @"自動回彈的上拉02", @"自定義刷新控件(自動刷新)", @"自定義刷新控件(自動回彈)"];
exam1.methods = @[@"example11", @"example12", @"example13", @"example14", @"example15", @"example16", @"example17", @"example18", @"example19", @"example20", @"example21"];
MJExample *exam2 = [[MJExample alloc] init];
exam2.header = MJExample20;
exam2.vcClass = [MJCollectionViewController class];
exam2.titles = @[@"上下拉刷新"];
exam2.methods = @[@"example21"];
MJExample *exam3 = [[MJExample alloc] init];
exam3.header = MJExample30;
exam3.vcClass = [MJWebViewViewController class];
exam3.titles = @[@"下拉刷新"];
exam3.methods = @[@"example31"];
self.examples = @[exam0, exam1, exam2, exam3];
}
return _examples;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
MJExample *exam = self.examples[indexPath.section];
UIViewController *vc = [[exam.vcClass alloc] init];
vc.title = exam.titles[indexPath.row];
[vc setValue:exam.methods[indexPath.row] forKeyPath:@"method"];
[self.navigationController pushViewController:vc animated:YES];
}
複製代碼
ViewController.h
- (IBAction)tappdeBtn:(id)sender {
UIViewController *vc = [[BViewController alloc] init];
vc.title = @"example01";
[vc setValue:@"example01" forKeyPath:@"method"];
[self.navigationController pushViewController:vc animated:YES];
}
複製代碼
上面是跳轉方法,請留意
[vc setValue:@"example01" forKeyPath:@"method"];
這句代碼,下面會詳解
BViewController.h
#import "BViewController.h"
#import "UIViewController+Example.h"
#define MJPerformSelectorLeakWarning(Stuff) \
do { \
_Pragma("clang diagnostic push") \
_Pragma("clang diagnostic ignored \"-Warc-performSelector-leaks\"") \
Stuff; \
_Pragma("clang diagnostic pop") \
} while (0)
@implementation BViewController
- (void)viewDidLoad {
[super viewDidLoad];
MJPerformSelectorLeakWarning(
[self performSelector:NSSelectorFromString(self.method) withObject:nil];
);
}
- (void)example01
{
NSLog(@"進入此方法");
}
複製代碼
結果:
一、由上能夠看到
[self performSelector:NSSelectorFromString(self.method) withObject:nil];
沒有指明方法名,仍能夠調用- (void)example01()
,這是運用了runtime
的黑魔法,定義了UIViewController+Example
分類方法,runtime
的使用能夠看我以前的文章-->iOS進階之runtime做用
二、
MJPerformSelectorLeakWarning( );
若是selector是在運行時才肯定的,performSelector時,若先把selector保存起來,等到某事件發生後再調用,至關於在動態綁定之上再使用動態綁定,不過這是編譯器不知道要執行的selector是什麼,由於這必須到了運行時才能肯定,使用這種特性的代價是,若是在ARC下編譯代碼,編譯器會發生警告,可用#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
忽略警告
#import <UIKit/UIKit.h>
@interface UIViewController (Example)
@property (copy, nonatomic) NSString *method;
@end
----------------------------
#import "UIViewController+Example.h"
#import <objc/runtime.h>
@implementation UIViewController (Example)
static char MethodKey;
- (void)setMethod:(NSString *)method
{
objc_setAssociatedObject(self, &MethodKey, method, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)method
{
return objc_getAssociatedObject(self, &MethodKey);
}
複製代碼
這是runtime中爲分類添加屬性的經典用法,把上面跳轉方法中的
[vc setValue:@"example01" forKeyPath:@"method"];
賦值的example01
利用runtime
關聯,這樣分類中的method
屬性值就爲example01
解析一下 static char
好比有這樣一個函數
exp()
{
char a[] = "Hello!" ;
static char b[] = "Hello!" ;
}
複製代碼
當調用這個函數完後,a[]就不存在了,而b[]依然存在,而且值爲hello;
performSelector系列方法編譯器警告-Warc-performSelector-leaks