背景
- 在現(xiàn)今的開發(fā)中,UITableView和UICollectionView算是最最流行的控件了,基本上每一個應用內部都會多次使用到這類控件,它們統(tǒng)一都繼承自UIScrollView
- 平時我們都會用它們來展示數(shù)據(jù),但是也有很多情況,比如頁面上下拖動的時候導航欄透明度改變、一個工具欄在頁面向上拖動到一定程度后懸浮到一個位置不動了、再比如簡書的個人頁面,頭像在導航欄上,頭像會隨著頁面的上下拖動變大變小、再比如上拉下拉刷新的實現(xiàn)、再比如點擊狀態(tài)欄,tabbleView如果不是在最頂部,那么它會立即滾動到頁面最頂部等等等等,這些效果全部都是通過UIScrollView的一些屬性實現(xiàn)的
- 而且這類控件也算是我們開發(fā)中最基本的控件了,基本我們天天都需要和它們打交道,所以說,掌握它們的使用不僅僅是對我們最基本的要求,也可以讓我們實現(xiàn)很多效果,但是這些都需要建立在對它們一些易混淆屬性理解的基礎之上
- 本文將以UITableView為例說明下它的一些關鍵屬性,并結合一些屬性簡單實現(xiàn)下拉刷新
- 先來看下這篇文章最后結合相關屬性講解的一個小Demo效果

refresh.gif
知識點
1)contenSize -- 滾動視圖內容的尺寸
- 這個屬性是CGSize類型的,它決定了你的滾動視圖是否能滾動,能夠滾動多遠
- 這個屬性對于實現(xiàn)我們本文要講解的下拉刷新是至關重要的,我們需要明確contenSize包含了tabbleview的什么內容,也就是什么內容才算是contenSize的一部分
- 除了UITableviewCell屬于contenSize的一部分之外,tabbleview的頭部和尾部視圖也算是它的contenSize的一部分
- 如果設置了內邊距,那么內邊距是不算contenSize的一部分的
- 如果給tabbleview添加子控件,這個子控件也不屬于contenSize的一部分
- 而且給scrollview內部子控件添加約束時,最頭疼的其實就是這個contenSize,它的contenSize需要根據(jù)子控件的尺寸以及子控件與scrollview之間的間距計算

偏移量.png
2)contentOffset -- 偏移量
- 是一個CGPoint類型的屬性,一般而言,上下滑動,我們需要它的contentOffset.y,左右滑動我們需要它的contentOffset.x
- 這是一個滾動視圖最最基本的屬性,也是最重要的屬性,基本上,上述所有效果都是基于它來實現(xiàn)的
- 當tabbleview是在一個導航條下面的話,那么系統(tǒng)會自動將tabbleview內容增加64的頂部內邊距,因此它的偏移量y值默認是-64
- 我們可以這樣去理解它,以一個上下滾動的tabbleview為例,
contentOffset.y = tabbleview的frame的左上角的y值 - tabbleview的內容的左上角的y值,而且這個坐標系以tabbleview的內容的左上角為坐標原點

坐標系.png
3)showsHorizontalScrollIndicator和showsVerticalScrollIndicator
- 是BOOL類型的,決定了是否隱藏水平或者垂直方法滾動條,之所以提及這2個屬性是要說一個注意點,比如我們在使用scrollview時,如果我們需要遍歷scrollview的子控件數(shù)組,我們就需要注意這2個控件,它也算scrollview的子控件
4)scrollsToTop
- 一個BOOL類型的屬性,可以控制點擊狀態(tài)欄,是否讓不在頂部的滾動視圖回到最頂部
- 屬性默認為YES,也就是系統(tǒng)默認幫我們實現(xiàn)了這個效果
- 需要說明的是如果頁面有2個及以上滾動視圖的時候,你需要控制哪一個滾動視圖支持scrollsToTop,哪些不支持
5)estimatedHeight --- 估算高度
這里針對tabbleview說一下這個屬性,因為tabbleview不需要我們自己計算contensize,所以默認情況下,tabbleview都是先調用heightForRow方法然后再去調用cellForRow方法,而且heightForRow會根據(jù)當前tabbleview有多少行就去調用多少次,無論是否cell當前被展示
如果我們給了tabbleview一個估算高度,那么它可以減少heightForRow方法的調用頻率,延遲計算目前不需要展示cell的高度,需要展示再去計算,這個也算是這個屬性的優(yōu)點
它也是有一定缺點的,既然是估算高度,那我們給多少算合適的,其實給多給少都不太好,給少了,它調用heightForRow方法就會多,給少了,雖然調用heightForRow方法少,但是tabbleview計算它的contensize的誤差就越大,明顯效果就是滾動條的高度很奇怪(跳躍性很大),所以還是根據(jù)cell平均值給一個估算高度
6)cellForRowAtIndexPath和indexPathForSelectedRow
-
cellForRowAtIndexPath這個方法根據(jù)傳入的indexPath可以獲得顯示在tableView上的某一行cell -
indexPathForSelectedRow這個方法可以在系統(tǒng)沒有傳入indexPath的方法或者自定義的方法中,通過該方法獲得被選中的cell的indexPath,可以得到section,row,也是非常實用的
利用偏移量簡單實現(xiàn)一下下拉刷新(仿新浪微博)
- 其實一開始說的那些效果實現(xiàn)起來都是不難的,主要是我們需要對這些基本且關鍵屬性非常理解,知己知彼,百戰(zhàn)不殆嘛
- 下面的這些代碼主要是關于下拉過程中,更新下拉刷新控件文字以及箭頭狀態(tài)的描述,關于發(fā)送網絡請求,這里只是模擬一下發(fā)送,新添加的數(shù)據(jù)也是假數(shù)據(jù)
- 關于下拉刷新控件是否作為tabbleview的頭部視圖,這個我是建議不要這樣做的,因為頭部視圖一般最好是用來作為輪播器或者廣告使用,我們一般最好把刷新控件使用addSubview的方法添加到tabbleview上面最好

下拉刷新.png
/**
* 停止拖拽,需要在這個方法里面監(jiān)聽停止拖拽的時候偏移量,根據(jù)偏移量判斷刷新控件是否完全顯示,是的話進入刷新,不是的話直接返回
*/
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
// 如果正在刷新, 直接返回
if (self.isHeaderRefreshing) return;
// 當偏移量 <= offsetY時(注意正負值), 刷新控件就完全出現(xiàn)了
CGFloat offsetY = - (self.tableView.contentInset.top + self.headerBtn.lb_height);
[self.loadingView stopAnimating];
self.headerBtn.imageView.hidden = NO;
if (self.tableView.contentOffset.y <= offsetY) { // 刷新header完全出現(xiàn)了
// 進入刷新狀態(tài)
[self headerBeginRefresh];
}
}
- 還需要在
scrollViewDidScroll方法中實時根據(jù)偏移量控制下拉刷新控件文字以及箭頭的狀態(tài),這個監(jiān)控狀態(tài)的代碼這里不貼出來了,放在代碼里面有,注釋也是很詳細的 - 開始刷新方法實現(xiàn)
#pragma mark - 開始刷新
- (void)headerBeginRefresh
{
//如果當前正在刷新,那么不往下繼續(xù)執(zhí)行
if (self.isHeaderRefreshing) return;
//否則,進入刷新狀態(tài)
self.headerRefreshing = YES;
//顯示菊花
self.loadingView.alpha = 1.0;
[self.loadingView startAnimating];
[self.headerBtn setTitle:@"加載中..." forState:UIControlStateNormal];
self.headerBtn.imageView.transform = CGAffineTransformIdentity;
self.headerBtn.imageView.hidden = YES;
// 顯示加載中...,這個時候這個刷新控件是會自己懸浮在導航欄下面,不需要人為拽著不松手
//這個效果,我們可以通過增大tabbleview的內邊距來達到這個效果
[UIView animateWithDuration:0.25f animations:^{
//因為刷新控件的y值就是-50,它自己高度也是50,所以只需要讓tabbleview內邊距向下走50,那么刷新控件就會完全顯示了
UIEdgeInsets inset = self.tableView.contentInset;
inset.top += self.headerBtn.lb_height;
self.tableView.contentInset = inset;
}];
//由于是模擬發(fā)送網絡請求,所以延遲1.5秒后再去加載數(shù)據(jù)
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 發(fā)送請求給服務器
[self loadNewData];
});
}
- 結束刷新方法實現(xiàn)
#pragma mark - 結束刷新
- (void)headerEndRefresh
{
self.headerRefreshing = NO;
self.headerBtn.hidden = YES;
// 減小內邊距
// 刷新已經停止,不需要刷新控件顯示在用戶能看到的范圍,所以需要減少tabbleview的內邊距
[UIView animateWithDuration:0.25 animations:^{
UIEdgeInsets inset = self.tableView.contentInset;
inset.top -= self.headerBtn.lb_height;
self.tableView.contentInset = inset;
}completion:^(BOOL finished) {//刷新控件縮回到用戶看不到的位置后,更新刷新控件以及內部子控件的狀態(tài)
self.headerBtn.hidden = NO;
self.loadingView.alpha = 0.0;
[self.loadingView stopAnimating];
}];
}