MJRefresh原理

下拉刷新和上拉載入的原理
非常多App中,新聞或者展示類都存在下拉刷新和上拉載入的效果,網(wǎng)上提供了實(shí)現(xiàn)這樣的效果的第三方類(詳情請(qǐng)見(jiàn)MJRefreshEGOTableViewPullRefresh),用起來(lái)非常方便,可是閑暇之余,我們能夠思考下,這樣的效果實(shí)現(xiàn)的原理是什么,我曾經(jīng)說(shuō)過(guò),僅僅要是動(dòng)畫(huà)都是騙人的,僅僅要不是硬件問(wèn)題大部分效果都能在系統(tǒng)UI的基礎(chǔ)上做出來(lái).
@以下是關(guān)鍵代碼分析:

// 下拉刷新的原理

- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView{       if (scrollView.contentOffset.y < - 100) { 
[UIView animateWithDuration:1.0 animations:^{ 
// frame發(fā)生偏移,距離頂部150的距離(可自行設(shè)定) self.tableView.contentInset = UIEdgeInsetsMake(150.0f, 0.0f, 0.0f, 0.0f); 
} completion:^(BOOL finished) { 
/** * 發(fā)起網(wǎng)絡(luò)請(qǐng)求,請(qǐng)求刷新數(shù)據(jù) */ }]; 
}
}

// 上拉載入的原理

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{ 
NSLog(@"%f",scrollView.contentOffset.y); 
NSLog(@"%f",scrollView.frame.size.height); 
NSLog(@"%f",scrollView.contentSize.height); 

/** * 關(guān)鍵--> * scrollView一開(kāi)始并不存在偏移量,可是會(huì)設(shè)定contentSize的大小,所以contentSize.height永遠(yuǎn)都會(huì)比contentOffset.y高一個(gè)手機(jī)屏幕的 * 高度;上拉載入的效果就是每次滑動(dòng)究竟部時(shí),再往上拉的時(shí)候請(qǐng)求很多其它,那個(gè)時(shí)候產(chǎn)生的偏移量,就能讓contentOffset.y + 手機(jī)屏幕尺寸高大于這 * 個(gè)滾動(dòng)視圖的contentSize.height */

if (scrollView.contentOffset.y + scrollView.frame.size.height >= scrollView.contentSize.height) {
 NSLog(@"%d %s",__LINE__,__FUNCTION__); 
[UIView commitAnimations]; 
[UIView animateWithDuration:1.0 animations:^{ 
// frame發(fā)生的偏移量,距離底部往上提高60(可自行設(shè)定) self.tableView.contentInset = UIEdgeInsetsMake(0, 0, 60, 0); } completion:^(BOOL finished) { 

/** * 發(fā)起網(wǎng)絡(luò)請(qǐng)求,請(qǐng)求載入很多其它數(shù)據(jù) * 然后在數(shù)據(jù)請(qǐng)求回來(lái)的時(shí)候,將contentInset改為(0,0,0,0) */ }]; 
}
}

轉(zhuǎn)載于:http://www.cnblogs.com/fantasy3588/p/5486215.html

MJRefresh是流行的下拉刷新控件,前段時(shí)間為了修復(fù)一個(gè)BUG,讀了它的源碼,本文總結(jié)一下實(shí)現(xiàn)的原理
下拉刷新的基本原理
大部分的下拉刷新控件,都是用contentInset實(shí)現(xiàn)的。默認(rèn)情況下,如果一個(gè)UIScrollView的左上角在導(dǎo)航欄的正下方,那么它的contentInset是64,而contentOffset是-64。繼續(xù)下拉的話,contentOffset就會(huì)越來(lái)越小,如果上滑,contentOffset就會(huì)增大,直到左上角達(dá)到屏幕的左上角時(shí),contentOffset剛好為0
默認(rèn)情況下,如果下拉一個(gè)UIScrollView,在松手之后,會(huì)彈回初始的位置(導(dǎo)航欄下方)。而大部分的下拉刷新控件,都是將自己放在UIScrollView的上方,起始y設(shè)置成負(fù)數(shù),所以平時(shí)不會(huì)顯示出來(lái),只有下拉的時(shí)候才會(huì)出現(xiàn),放開(kāi)又會(huì)彈回去。然后在loading的時(shí)候,臨時(shí)把contentInset增大,相當(dāng)于把UIScrollView往下擠,于是下拉刷新的控件就會(huì)顯示出來(lái),然后刷新完成之后,再把contentInset改回原來(lái)的值,實(shí)現(xiàn)回彈的效果
基本上,MJRefresh也是這么實(shí)現(xiàn)的
創(chuàng)建下拉刷新控件實(shí)例
從創(chuàng)建實(shí)例的代碼開(kāi)始:

MJRefreshNormalHeader *header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
 [myController loadCollectionDataNeedReset:YES withBlock:^{ 
[self.header endRefreshing];
 [self reloadData];
 }];
}];

調(diào)用的是一個(gè)工廠方法headerWithRefreshingBlock,這個(gè)方法定義在各種header控件的基類MJRefreshHeader里:

 + (instancetype)headerWithRefreshingBlock:
(MJRefreshComponentRefreshingBlock)refreshingBlock{ 
MJRefreshHeader *cmp = [[self alloc] init]; 
cmp.refreshingBlock = refreshingBlock; 
return cmp;
}

然后會(huì)調(diào)用init方法,由于MJRefreshHeader里并沒(méi)有定義init方法,而它的基類MJRefreshComponent里定義了,所以會(huì)進(jìn)入到基類的初始化方法里:

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

這里的關(guān)鍵是prepare方法,這個(gè)方法是第一個(gè)擴(kuò)展點(diǎn),具體的header(包括庫(kù)提供的原生header,和用戶自定義的header)有哪些屬性,樣式是怎么樣,都是在這個(gè)方法里實(shí)現(xiàn)的。每個(gè)子類的prepare方法,都會(huì)調(diào)用父類的prepare方法。所以在擴(kuò)展的時(shí)候,公共的屬性寫(xiě)在父類的prepare方法里,特有的屬性寫(xiě)在子類的prepare方法里。比如,我們看一下MJRefreshStateHeader的:

- (void)prepare{ [super prepare];
 // 初始化文字 
[self setTitle:MJRefreshHeaderIdleText forState:MJRefreshStateIdle];
 [self setTitle:MJRefreshHeaderPullingText forState:MJRefreshStatePulling]; 
[self setTitle:MJRefreshHeaderRefreshingText forState:MJRefreshStateRefreshing];
}

總之,調(diào)用headerWithRefreshingBlock方法以后,就得到了一個(gè)UIView的實(shí)例,也就是下拉刷新的控件。但是現(xiàn)在它還沒(méi)有掛到任何superview上,也沒(méi)有任何行為
將下拉刷新控件,掛到UIScrollView上
接下來(lái)的調(diào)用:

  self.header = header;

這是利用了UIScrollView+MJRefresh里的一個(gè)category,為UIScrollView增加了屬性header和footer。這里用到了關(guān)聯(lián)對(duì)象的技巧(AssociatedObject),因?yàn)閏ategory通常情況下是不能直接添加實(shí)例變量的

- (void)setHeader:(MJRefreshHeader *)header{ 
if (header != self.header) { 
// 刪除舊的,添加新的
 [self.header removeFromSuperview];
 [self addSubview:header]; 
// 存儲(chǔ)新的 
[self willChangeValueForKey:@"header"]; 
// KVO 
objc_setAssociatedObject(self, &MJRefreshHeaderKey, header, OBJC_ASSOCIATION_ASSIGN);

 [self didChangeValueForKey:@"header"];
 // KVO 
}
}

通過(guò)上面的代碼,把header添加到了UIScrollView的subviews里,并保留了一個(gè)引用。但是這個(gè)header的frame還沒(méi)有確定,也沒(méi)有任何行為
設(shè)置header的位置和偵聽(tīng)行為
由于上面執(zhí)行了addSubview,接下來(lái)就會(huì)進(jìn)入header的生命周期方法willMoveToSuperview,這個(gè)方法是在公共的基類MJRefreshComponent里實(shí)現(xiàn)的。因?yàn)檫@是基礎(chǔ)的行為,所以寫(xiě)在公共的基類里,所有的子類都能共享:

- (void)willMoveToSuperview:(UIView *)newSuperview{
 [super willMoveToSuperview:newSuperview]; 
// 如果不是UIScrollView,不做任何事情
 if (newSuperview && ![newSuperview isKindOfClass:[UIScrollView class]]) return; 
// 舊的父控件移除監(jiān)聽(tīng) 
[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最開(kāi)始的contentInset 
_scrollViewOriginalInset = self.scrollView.contentInset; 
// 添加監(jiān)聽(tīng) 
[self addObservers]; 
}}

這里關(guān)鍵是設(shè)置了alwaysBounceVertical,這樣才能確保UIScrollView可以下拉,否則需要處理contentSize才能拉得動(dòng),就麻煩了很多。此外這里令header也持有UIScrollView的引用,后續(xù)可以從上面取到各種屬性
然后是添加監(jiān)聽(tīng)的方法addObservers,這里主要是用了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)observeValueForKeyPath:(NSString *)keyPath 
ofObject:(id)object change:(NSDictionary *)change 
context:(void *)context{ 
// 遇到這些情況就直接返回
 if (!self.userInteractionEnabled || self.hidden) return; 
if ([keyPath isEqualToString:MJRefreshKeyPathContentOffset]) { 
[self scrollViewContentOffsetDidChange:change]; 
} else if ([keyPath isEqualToString:MJRefreshKeyPathContentSize]) { 
[self scrollViewContentSizeDidChange:change]; 
} else if ([keyPath isEqualToString:MJRefreshKeyPathContentInset]) { 
[self scrollViewContentInsetDidChange:change]; 
} else if ([keyPath isEqualToString:MJRefreshKeyPathPanState]) { 
[self scrollViewPanStateDidChange:change]; }}

這里偵聽(tīng)了3個(gè)key的變化,UIScrollView的contentOffset和contentSize,以及滑動(dòng)手勢(shì)的狀態(tài)。然后在每個(gè)value發(fā)生變化的時(shí)候,調(diào)用幾個(gè)didChange方法。這些didChange方法都是hook,是第二個(gè)擴(kuò)展點(diǎn),實(shí)際上都是由子類來(lái)實(shí)現(xiàn)的
接下來(lái)會(huì)進(jìn)入生命周期方法layoutSubviews:
- (void)layoutSubviews{ [super layoutSubviews]; [self placeSubviews];}

這里的placeSubviews就是header應(yīng)該怎么擺,是第三個(gè)擴(kuò)展點(diǎn),把header的origin.y設(shè)置成負(fù)值,就是在MJRefreshHeader的這個(gè)方法里實(shí)現(xiàn)的:

  • (void)placeSubviews{ [super placeSubviews]; // 設(shè)置y值(當(dāng)自己的高度發(fā)生改變了,肯定要重新調(diào)整Y值,所以放到placeSubviews方法中設(shè)置y值) self.mj_y = - self.mj_h;}

每個(gè)子類的placeSubviews方法,都應(yīng)該先調(diào)用父類的這個(gè)方法
通過(guò)上述的代碼,確定了下拉刷新控件的位置,以及其中每個(gè)subview的位置。并且偵聽(tīng)了UIScrollView的contentOffset和contentSize變化
下拉時(shí)的實(shí)際行為
下拉會(huì)導(dǎo)致contentOffset變化,由于前面已經(jīng)添加了KVO偵聽(tīng),所以會(huì)執(zhí)行scrollViewContentOffsetDidChange方法:

- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{
[super scrollViewContentOffsetDidChange:change]; 
// 在刷新的refreshing狀態(tài) 
if (self.state == MJRefreshStateRefreshing) { 
// sectionheader停留解決 
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)到看不見(jiàn)頭部控件,直接返回 
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īng)顟B(tài)
self.state = MJRefreshStatePulling; 

} else if (self.state == MJRefreshStatePulling && offsetY >= 
normal2pullingOffsetY) { 

// 轉(zhuǎn)為普通狀態(tài) 
self.state = MJRefreshStateIdle;
}
}
else if (self.state == MJRefreshStatePulling) {
// 即將刷新 && 手松開(kāi) 
// 開(kāi)始刷新 
[self beginRefreshing]; 
} else if (pullingPercent < 1) {
self.pullingPercent = pullingPercent;
}}

這段代碼比較長(zhǎng),主要是判斷offset變化是否達(dá)到了臨界值,以及當(dāng)前的手勢(shì),切換header的state狀態(tài),然后根據(jù)state狀態(tài)變化,驅(qū)動(dòng)不同的行為:

- (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.mj_h; 
// 自動(dòng)調(diào)整透明度
 if (self.isAutoChangeAlpha) 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];
 }];
 }}

setState方法是第四個(gè)擴(kuò)展點(diǎn),這里的MJRefreshCheckState是個(gè)宏,也調(diào)用了父類的setState的方法。下拉的時(shí)候臨時(shí)增大contentInset,令header保留在屏幕上,然后調(diào)用callback block;結(jié)束之后還原contentInset

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,563評(píng)論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,694評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 178,672評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,965評(píng)論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,690評(píng)論 6 413
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 56,019評(píng)論 1 329
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,013評(píng)論 3 449
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 43,188評(píng)論 0 290
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,718評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,438評(píng)論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,667評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,149評(píng)論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,845評(píng)論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 35,252評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 36,590評(píng)論 1 295
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,384評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,635評(píng)論 2 380

推薦閱讀更多精彩內(nèi)容