下拉刷新和上拉載入的原理
非常多App中,新聞或者展示類都存在下拉刷新和上拉載入的效果,網(wǎng)上提供了實(shí)現(xiàn)這樣的效果的第三方類(詳情請(qǐng)見(jiàn)MJRefresh和EGOTableViewPullRefresh),用起來(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