任他風(fēng)吹雨打,我自巋然不動瘪吏!
當(dāng)我們實時往UITableView
中插入數(shù)據(jù)并刷新列表的時候癣防,會發(fā)現(xiàn)列表是有抖動的。比如在微信聊天頁面掌眠,你滑動到某一個位置保持住蕾盯,然后收到一個或者若干人的微信(這幾個人不在當(dāng)前聊天列表中)。你會發(fā)現(xiàn)每收到一個人的信息蓝丙,列表向下沉级遭,就是有一個“抖動”的過程。當(dāng)然渺尘,并不是說微信體驗不好挫鸽,只是拋磚引玉。
言歸正傳鸥跟,我要討論的場景如下:
當(dāng)前列表展示了很多新聞丢郊,同時后臺在加載第三方廣告。廣告加載完成后需要按照規(guī)定的位置順序循環(huán)地插入到列表中,比如第5蚂夕,12迅诬,19,26...婿牍,要求插入廣告后當(dāng)前展示的頁面沒有下沉抖動現(xiàn)象,避免剛剛看的新聞跳到不可知的位置去了惩歉。
由于這里廣告不是直接附加在列表末尾等脂,也不是一次性插入到相鄰的位置,而是離散地分布在整個列表中撑蚌,所以不好用
insertRowsAtIndexPaths:withRowAnimation:
或者
reloadRowsAtIndexPaths:withRowAnimation:
局部刷新上遥,必須對整個列表ReloadData。顯然這會導(dǎo)致列表下沉抖動争涌,最壞的情況是當(dāng)前展示的整個頁面下沉粉楚,這對于新聞客戶端來說體驗很不好。
首先亮垫,我會想到scrollToRowAtIndexPath:atScrollPosition:animated:
這個方法模软。在我刷新完整個列表之后,再將UITableView
滾動到之前記錄的位置饮潦。大致思路看代碼:
//刷新列表之前找到當(dāng)前屏最頂部的新聞Id
- (NSString *)topNewsId {
NSArray *visibleCells = [self.tableView visibleCells];
UITableViewCell *cell = [visibleCells firstObject];
NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
NewsModel *topNews = [self.dataArr objectAtIndex:indexPath.row];
NSString *newsId = = topNews.newsId;
return newsId;
}
//刷新之后再將之前頂部的新聞滾動到頂部 避免頁面抖動
- (void)keepTopNews:(NSString *)topNewsId {
int topNewsRow = 0;
for (int i = 0; i <[self.dataArr count] ; i ++) {
id data = [self.dataArr objectAtIndex:i];
if ([data isKindOfClass:[NewsModel class]]) {
NewsModel *model = data;
if ([model.newsId isEqualToString:topNewsId]) {
topNewsRow = i;
break;
}
}
}
if (topNewsRow) {
NSIndexPath *toIndex = [NSIndexPath indexPathForRow:topNewsRow inSection:0];
[self.tableView scrollToRowAtIndexPath:toIndex atScrollPosition:UITableViewScrollPositionTop animated:NO];
}
}
乍一看燃异,這種方法挺優(yōu)美的,也好像能達到我們的目的继蜡。但實際上還是有問題的回俐,問題出在visibleCells
這個方法。先來看看這個方法的定義:
Returns an array of visible cells currently displayed by the collection view.
即返回當(dāng)前展示的可見cell數(shù)組稀并。
不過仅颇,這個方法并不是"眼見為實的",有時候我們?nèi)庋劭床坏降腸ell它卻認為是可見的碘举,或者只部分可見的它也會返回給我們的忘瓦。比如圖中網(wǎng)易新聞最上面的新聞 “...夫人鏡頭里的民國世相”就只見到一部分,如果用它來置頂也是會有下沉抖動問題的殴俱。
那么還有沒有更優(yōu)雅的方式呢政冻?Absolutely!!!
既然用cell做單位來滾動太粗糙,我們可以用像素級別滾動來優(yōu)雅地保持置頂新聞巋然不動线欲。
首先我們要知道ReloadData的一個特性:
When you call this method, the collection view discards any currently visible items and views and redisplays them. For efficiency, the collection view displays only the items and supplementary views that are visible after reloading the data. If the collection view’s size changes as a result of reloading the data, the collection view adjusts its scrolling offsets accordingly.
關(guān)于ContentOffset明场、ContentSize、ContentInset的區(qū)別這里就不贅述了李丰,可以參考這里苦锨。
就是說ReloadData只刷新當(dāng)前屏幕可見的哪些cell,只會對visibleCells調(diào)用
tableView:cellForRowAtIndexPath:
。contentOffset是保持不變的舟舒,所以我們才看到了“抖動現(xiàn)象”拉庶,就像新聞被擠下去了。
圖中灰色部分表示iPhone的屏幕秃励,粉紅色表示所有數(shù)據(jù)的布局大小氏仗,白色單元是隱藏在屏幕上方的數(shù)據(jù),綠色表示目標(biāo)廣告單于格夺鲜。
左圖的當(dāng)前屏幕最上面的新聞是news 11皆尔,UITableview的contentOffset是200,我們可以計算出news 11之前所有新聞單元格的高度總和得出現(xiàn)在news 11的偏移量preOffset币励。
右圖是在第三個位置插入一個廣告后的布局慷蠕。UITableview的contentOffset還是200,但是news 11被“擠下去”了食呻。我們同樣可以計算news 11之前所有新聞單元格和廣告單元格的高度總和得出現(xiàn)在news 11的偏移量afterOffset流炕。
有了preOffset和afterOffset之后就可以知道news 11被“擠下去”多少距離
deltaOffset = afterOffset - preOffset;
那么,為了保證news 11還是展示在當(dāng)初的位置仅胞,我們只要手動更新ContentOffset的值就可以了每辟,相當(dāng)于將粉紅色部分上移deltaOffset的距離。
看代碼:
- (void)insertAds:(NSArray *)ads {
NSString *topNewsId = [self topNewsId];
CGFloat preOffset = [self offSetOfTopNews:topNewsId];
/*
插入廣告...
*/
[self.tableView reloadData];
CGFloat afterOffset = [self offSetOfTopNews:topNewsId];
CGFloat deltaOffset = afterOffset - preOffset;
CGPoint contentOffet = [self.tableView contentOffset];
contentOffet.y += deltaOffset;
self.tableView .contentOffset = contentOffet;
}
//計算newsId對應(yīng)新聞的偏移量
- (CGFloat)offSetOfTopNews:(NSString *)newsId {
CGFloat offset = 0;
for (int i = 0; i < [self.dataArr count]; i ++) {
id data = [self.dataArr objectAtIndex:i];
if ([data isKindOfClass:[NewsModel class]]) {
NewsModel *model = data;
if ([model.newsId isEqualToString:newsId]) {
break;
}
}
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
CGFloat height = [self heightForRowAtIndexPath:indexPath];
offset += height;
}
return offset;
}
如此饼问,就可以真正做到當(dāng)前屏幕一點都不下沉了影兽。如果廣告插在當(dāng)前屏幕之外,用戶是感覺不到的莱革,等滑動列表才能在相應(yīng)位置看到廣告峻堰;如果插入到當(dāng)前屏幕中,用戶在課間區(qū)域看到插入一個新聞盅视,但是置頂?shù)男侣勎恢檬潜3植粍拥摹?/p>
盡享絲滑~
最后稍微提一下計算偏移量中用到的一個小技巧捐名。
如果所有的新聞和廣告單元的高度是固定的,那么heightForRowAtIndexPath:
是很方便計算的闹击。如果是動態(tài)的镶蹋,就需要用到一點技巧了。
比如廣告的數(shù)據(jù)用AdModel
表示赏半。為了讓廣告單元的高度隨廣告內(nèi)容動態(tài)調(diào)整贺归,我們一般習(xí)慣在AdModel
里用一個cellHeight
字段。
@interface AdModel:NSObject
@property (nonatomic, assign) NSInteger adId;
...
@property (nonatomic, assign) CGFloat cellHeight;
@end
在我們填充內(nèi)容渲染廣告位的時候算出高度再賦值給cellHeight
断箫。
在上面的場景下拂酣,前面雖然插入了廣告,但是ReloadData的時候仲义,UITableView并不會刷新不可見的廣告位婶熬,因此cellHeight
始終為0剑勾,這就導(dǎo)致heightForRowAtIndexPath:
不能計算出正確的結(jié)果。
巧妙地赵颅,我們在廣告插入self.dataArr
的時候定義一個臨時的廣告單元變量AdCell
虽另,并主動調(diào)用渲染的接口來給cellHeight
賦值。
AdCell *tmpCell = [AdCell new];
[tmpCell setAdsContent:model];//這里會渲染廣告位并計算出cellHeight