【轉載請註明出處】
本文將說明讓UIScrollView支援"下拉重新整理"和"上拉載入更多"的實現機制,並實現一個可用的tableView子類,以下主要以"下拉重新整理"進行說明。
工程地址在帖子最下方,只需要程式碼的直拉到底即可。
【目錄】
1、contentInset和下拉重新整理;
2、動畫、動態文字和重新整理時間;
3、其他;
4、工程地址。
【added at 2013.11.28】
下拉重新整理和section headerView衝突原因分析及解決辦法。
1、contentInset和下拉重新整理
contentInset是UIScrollView的屬性,它描述了UIScrollView的內容View的內邊距,具體可見官方文件:
Scroll View programming Guide for iOS
目前幾乎所有"下拉重新整理"的第三方庫都是依賴它實現的。
【為便於討論,將下拉重新整理/上拉載入時顯示的檢視稱為refresh panel,如下圖】
在使用者手指向下滑動到最終更新介面的過程中,經歷了4個步驟:
(1)隨著使用者下拉逐漸顯示UITableView頂部的refresh panel;
(2a)下拉達到預設位置,狀態文字變為"鬆開可以重新整理";
(2b)下拉未達到預設位置,使用者手指離開螢幕,ScrollView彈回,refresh panel重新隱藏起來,結束。
(3)使用者手指離開螢幕,refresh panel保持顯示。狀態文字變為"載入中",在後臺執行更新資料的操作;
(4)資料更新完成,返回主執行緒,重新隱藏refresh panel,結束。
可以看到,如果不考慮重新整理時間、狀態文字等,實現"下拉重新整理"實際上只需要做到2件事:
(1)隱藏refresh panel(初始時和重新整理後)
隱藏refresh panel,即使其居於UITableView的上方且不可見,如下
1 - (void)addDragHeaderView 2 { 3 if (self.shouldShowDragHeader && !dragHeaderView) 4 { 5 CGRect frame = CGRectMake(0, -self.dragHeaderHeight, 6 self.bounds.size.width, self.dragHeaderHeight); 7 dragHeaderView = [[Pull2RefreshView alloc] 8 initWithFrame:frame type:kPull2RefreshViewTypeHeader]; 9 [self addSubview:dragHeaderView]; 10 } 11 }
【注意:不應使用UITableView的tableHeaderView來作為refresh panel,一來會使得下拉重新整理和自定義tableHeaderView無法共存,二來UITableView的內容檢視是包含tableHeaderView的,即
tableView.contentSize.height == tableView.tableHeaderView.height
+ n * sectionHeaderView.height
+ m * cell.height
+ tableView.tableFooterView.height
因此想讓tableHeaderView預設不可見,需要修改contentOffset的初始值並在使用者滑動時控制滑動範圍,比較麻煩。】
(2)顯示refresh panel
當使用者手指下拉達到預設值並離開螢幕,立即修改contentInset,使refresh panel保持顯示,如下
1 - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate 2 { 3 if (scrollView.contentOffset.y < -self.dragHeaderHeight - 10.0f 4 { 5 //使refresh panel保持顯示 6 self.contentInset = UIEdgeInsetsMake(self.dragHeaderHeight, 0, 0, 0); 7 } 8 }
如此,更新資料後再次隱藏refresh panel的方式也很明瞭
1 self.contentInset = UIEdgeInsetsZero;
2、動畫、動態文字和重新整理時間
一個標準的refresh panel(如新浪微博的),包含指示箭頭、載入菊花、狀態文字和更新時間四部分,如下
1 @implementation Pull2RefreshView 2 { 3 UILabel *hintLabel; 4 UILabel *timeLabel; 5 6 UIImageView *arrowImageView; 7 UIActivityIndicatorView *indicatorView; 8 9 Pull2RefreshViewType refreshType; 10 }
在1中已經做到了refresh panel的顯示的隱藏,2中只需要在合適的時候改變refresh panel的顯示內容即可。
(1)"下拉可以重新整理"—>"鬆開立即更新"
在UIScrollView的委託函式scrollViewDidScroll:中檢測使用者下拉的程度,達到預設值後就改變狀態,如下:
1 - (void)scrollViewDidScroll:(UIScrollView *)scrollView 2 { 3 //拉動足夠距離,狀態變更為“鬆開....” 4 if (self.shouldShowDragHeader && dragHeaderView) 5 { 6 if (dragHeaderView.state == kPull2RefreshViewStateDragToRefresh 7 && scrollView.contentOffset.y < -self.dragHeaderHeight - 10.f 8 && !headerRefreshing 9 && !footerRefreshing) 10 { 11 [dragHeaderView flipImageAnimated:YES]; 12 [dragHeaderView setState:kPull2RefreshViewStateLooseToRefresh]; 13 } 14 } 15 }
修改指示箭頭方向為向上,在setState中修改狀態文字為"鬆開立即重新整理"。
(2)"鬆開立即重新整理"—>"載入中..."
在UIScrollView的委託函式scrollViewDidEndDragging: willDecelerate:中檢測使用者手指離開螢幕時的情況:
1 - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate 2 { 3 //拉動足夠距離,鬆開後,狀態變更為“載入中...” 4 if (dragHeaderView.state == kPull2RefreshViewStateLooseToRefresh 5 && scrollView.contentOffset.y < -self.dragHeaderHeight - 10.0f) 6 { 7 //使refresh panel保持顯示 8 self.contentInset = UIEdgeInsetsMake(self.dragHeaderHeight, 0, 0, 0); 9 [dragHeaderView flipImageAnimated:YES]; 10 [dragHeaderView setState:kPull2RefreshViewStateRefreshing]; 11 } 12 }
相比1中新增了修改指示箭頭方向,在setState中修改狀態文字為"載入中..."。
(3)"載入中..."—>"下拉可以重新整理"
這一步需要由外部(通常是ViewController)判斷何時執行,提供一個方法供外部呼叫,如下:
1 - (void)completeDragRefresh 2 { 3 [UIView beginAnimations:nil context:NULL]; 4 [UIView setAnimationDuration:0.3f]; 5 self.contentInset = UIEdgeInsetsZero; 6 [UIView commitAnimations]; 7 8 [dragView flipImageAnimated:NO]; 9 [dragView setState:kPull2RefreshViewStateDragToRefresh]; 10 }
指示箭頭方向和狀態文字恢復為初始狀態,更新時間變為當前時間。
3、其他
(1)"下拉重新整理"和"上拉載入更多"的不同
"下拉重新整理"的refresh panel的位置始終不變,而"上拉載入更多"的refresh panel則需要隨著tableView.contentSize的變化而變化。一個比較簡單的方案是:
1 tableView.tableFooterView = dragFooterView;
在某些第三方實現中便是如此處理的,好處是簡單到只需要一行程式碼,壞處是tableFooterView被佔用了。考慮到tableFooterView在"上拉載入更多"的情境下不太需要自定義,影響不大。
另一個方案是在初始化時和資料更新後,設定refresh panel的frame使其始終保持正確位置。
demo中用了第1種方法,SVPullToRefresh則採用了第2種方法。
此外,在觸發重新整理的條件上,二者也是不同的。"下拉重新整理"時,為防止重新整理"太過靈敏",需要設定一個閥值來控制,所以才有"鬆開立即重新整理"。而"上拉載入更多"是在使用者往下不斷瀏覽內容的過程中觸發的,因此只需滑動到內容底部就立即觸發載入。
(2)如何封裝
UITableView作為派生類,是和基類UIScrollView共享一個delegate屬性的,即UITableViewDeleagte和UIScrollViewDelegate是同時指定的。這帶來的問題是,想要封裝一個支援"下拉重新整理"和"上拉載入更多"的UITableView子類,恐怕不得不增加一層委託,將UITableViewDelegate中的各種方法都轉到外部進行實現,如
1 - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath 2 { 3 if (self.pullDelegate && [self.pullDelegate respondsToSelector:@selector(pull2RefreshTableView:didSelectRowAtIndexPath:)]) 4 { 5 [self.pullDelegate pull2RefreshTableView:self didSelectRowAtIndexPath:indexPath]; 6 } 7 }
可謂麻煩至極。
Added 2014.7.24:SVPullToRefresh中,提供了一種很好的思路,通過KVO監視UIScrollView的contentOffset.y的變化,來判斷是否會觸發下拉重新整理或上拉載入,有興趣的可以直接看它的原始碼:
https://github.com/samvermette/SVPullToRefresh
(3)下拉重新整理和UITableView的section headerView衝突
由於內容較多,單獨開一帖進行說明,地址:
http://www.cnblogs.com/lexingyu/p/3448532.html
4中工程已進行相應修改。
4、封裝的Pull2RefreshTableView Demo工程
使用iOS 6.1 SDK編譯,使用ARC。
地址:https://github.com/cDigger/CDPullToRefreshDemo
【參考】
1、Scroll View Programming Guide for iOS
2、SVPullToRefresh
https://github.com/samvermette/SVPullToRefresh