作者:@靛青K 授權本站轉載。作者特地為本站將文章進行了潤色,在此致謝。
最近做了一個輪子 DQKFreezeWindowView ,這裡我們一起探討一下這個輪子中凍結效果的簡單實現思路,也就是我的思考過程。
不廢話,直接開始~
初步分析
既然是一個凍結的效果,那至少要用一個UIScrollView來做主要顯示的部分。還是先來看幾張已經實現了的:
在三者的使用當中,個人認為體驗最好就是 MS 的 Excel 了,流暢、各方向都可以滑動。這裡先用 Reveal 查看一下三者的布局,使用的 View 都是什麼。
可以看到某課程表 App 是選擇了一個 UIScrollView 加兩個 Bar (UIView)實現該功能。簡單實用,顯示少量視圖尚可。
為了顯示更多數據/視圖, Excel 就采用了三個UIScrollView。
這裡有一些奇怪的現象引起了我的注意,為什麼三者左邊都不支持滑動?明明邊欄視圖都是在UIScrollView裡,怎麼想都是一個不合理的情況。所以我們的目標是這樣的幾個功能實現:
主要視圖支持多方位滾動
邊欄視圖一樣支持滾動
bounces效果實現
像 Excel 一樣支持更多數據顯示
既然需要實現邊緣的滑動,就要三個UIScrollView。為什麼一個不行,因為當你滑動的時候,頂欄和側欄最好是留在邊緣的。
注意:
這裡我考慮過使用UITableView或者UICollectionView實現,因為它已經很好的解決了delegate和dataSource等眾多問題。 XCMultiSortTableView 這個開源項目的方案是UITableView裡面的UITableViewCell套一個UITableView,很好的實現邊緣滾動問題。贊!但是並不能滿足咱的要求,像 Excel 那樣任意方向滾動。 博主沒有想到用UITableView或者UICollectionView實現的方案所依如果你在這方面有什麼好方案,快來和我交流。
關於以上幾款 App 的研究有興趣可以學習逆向相關問題 class-dump 和 IDA ,這裡不再贅述。
開始實現
我們需要三個UIScrollView,首要問題就是實現同步滾動,也是整個問題的關鍵部分:
為了方便定位視圖位置,先來自定義一個UIView,聲明三個UIScrollView類,mainScrollView 、 sectionScrollView 、 rowScrollView。將該UIVIew添加到一個UIViewController中,三者在UIView位置如下圖:
為什麼這樣起名字,因為像日歷、課程表之類都是豎著一排在一天,(好吧、其實是想不到合適的了,如果你想到更好的,快來告訴我)。
開始實現同步滾動問題:
普通情況的滾動
這個簡單,使用- (void)scrollViewDidScroll:(nonnull UIScrollView *)scrollView加上- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated;即可。比如 mainScrollView x 方向滾動多少, sectionScrollView x 方向滾動多少。
滾動到邊緣情況
這個稍微復雜一些,這裡先談一個觸摸邊緣滾動的情況,並且沒有bounces,這種情況不涉及滑動越過邊界問題。滾動 SectionScrollView 時, RowScrollView 不動,反之亦然。當前代碼如下:
- (void)scrollViewDidScroll:(nonnull UIScrollView *)scrollView { if ([scrollView isEqual:self.mainScrollView]) { //滾動 mainScrollView self.sectionScrollView.delegate = nil; self.rowScrollView.delegate = nil; [self.sectionScrollView setContentOffset:CGPointMake(self.mainScrollView.contentOffset.x, 0)]; [self.rowScrollView setContentOffset:CGPointMake(0, self.mainScrollView.contentOffset.y)]; self.sectionScrollView.delegate = self; self.rowScrollView.delegate = self; } else if ([scrollView isEqual:self.sectionScrollView]) { // 滾動 sectionScrollView self.mainScrollView.delegate = nil; self.rowScrollView.delegate = nil; [self.mainScrollView setContentOffset:CGPointMake(self.sectionScrollView.contentOffset.x, self.mainScrollView.contentOffset.y)]; self.mainScrollView.delegate = self; self.rowScrollView.delegate = self; } else if ([scrollView isEqual:self.rowScrollView]) { // 滾動 rowScrollView self.mainScrollView.delegate = nil; self.sectionScrollView.delegate = nil; [self.mainScrollView setContentOffset:CGPointMake(self.mainScrollView.contentOffset.x, self.rowScrollView.contentOffset.y)]; self.mainScrollView.delegate = self; self.sectionScrollView.delegate = self; } }
诶?為什麼我們要設置其他 scrollView 的delegate = nil。如果不設置,當視圖滾動時,再次滑動會出現視圖卡頓、移動混亂情況。
為什麼?
因為要實現三個 scrollView 都支持滾動,那就都要設置其delegate屬性,那麼滾動時,三個視圖都會委托這個函數來執行,如果不分開他們的滾動情況討論,並在情況裡面設置其他 scrollView delegate 為 nil。
其實到這裡需要的功能已經實現了。但是我在使用的時候發現一個 Bug ,比如,我們現在滑動的是 mainScrollView ,視圖停止滾動之前,去滑動 sectionScrollView 。诶!!!視圖一下子就錯位了,而且再滑動 mainScrollView 也不會同步了。這裡我的理解是,- (void)scrollViewDidScroll:(nonnull UIScrollView *)scrollView,這個委托方法是在視圖只要有滾動就會一直執行,那麼其對應的delegate也是不停的在nil和self之間不停的切換,滾動 mainScrollView 時,又去滾動另一個 scrollView ,將會執行self.mainScrollView.delegate = nil;, 同時執行另一個- (void)scrollViewDidScroll:(nonnull UIScrollView *)scrollView,出現錯位,如果不去滑動其他 scrollView ,self.mainScrollView.delegate就永遠是nil了,沒有了委托對象,自然不會繼續同步。
兩步解決問題,以滾動 mainScrollView 舉例,第一步,在情況開始添加:
[self.sectionScrollView setContentOffset:self.sectionScrollView.contentOffset animated:NO]; [self.rowScrollView setContentOffset:self.rowScrollView.contentOffset animated:NO];
為什麼這樣做?理由很簡單,立刻停止其他兩個 scrollView 的滾動,其他情況的計算也就立即停止了。只計算一個滾動 mainScrollView 情況。
第二步,在委托方法最後重新設置回三者的delegate對象。這樣一定不會在某次執行完出現nil情況了。
編譯運行,Gut~隨意滑動,三個位置任意滑動,隨時切換滑動對象。
那麼bounces效果怎麼辦?沒有這個效果,滾動到邊緣就會出現立即停止的不自然感覺。這裡想過幾種方案,不好的就不在這裡談了,浪費篇章,只提現在想到的最佳方案。先來考慮 Excel 的效果實現,你這麼聰明,其實早就發現根本不需要改代碼,對,沒錯。那麼現在只需要考慮DQKFreezeWindowViewBounceStyleAll情況,滾動到邊緣時,看起來只是一個UIScrollView。這裡需要考慮的就是 sectionScrollView 向下和 rowScrollView 向右移動的問題。當然這裡也有多種方案:
方案一:增加 View 的frame.size大小,這樣視圖就不會丟失了;
方案二:改變 View 的位置。
采取方案二,方案一有視圖重疊問題,不好不好。在原基礎上增加代碼,因為對於 sectionScrollView 視圖水平滾動(contentOffset)已經完成,那麼視圖位置只需要垂直移動即可。還是滾動 mainScrollView 情況,直接貼代碼:
if (self.bounceStyle == DQKFreezeWindowViewBounceStyleAll) { if (self.mainScrollView.contentOffset.y <= 0) { [self.sectionScrollView setFrame:CGRectMake(self.sectionScrollView.frame.origin.x, - self.mainScrollView.contentOffset.y, self.sectionScrollView.frame.size.width, self.sectionScrollView.frame.size.height)]; } if (self.mainScrollView.contentOffset.x <= 0) { [self.rowScrollView setFrame:CGRectMake(- self.mainScrollView.contentOffset.x, self.rowScrollView.frame.origin.y, self.rowScrollView.frame.size.width, self.rowScrollView.frame.size.height)]; } }
最初的方案是考慮四個情況的(>>,><,<>,<<),同時還進行各種計算,現在發現,沒有必要,sectionScrollView 的 y 坐標是 0 。那麼滾動多少,移動多少就可以了。(取相反數,為什麼?)
你應該注意到我在相關實現文件裡還加了一個 signView ,也就是左上角的小視圖。至於這個如何跟隨著移動,請參考源文件或者留著你來思考,類似 sectionScrollView 和 rowScrollView 。
視圖滾動問題解決!
效果已經實現,接下來的問題就是:
數據/視圖加載問題(也就是 Cell 的重用機制問題)
為了支持更好的使用內存,支持更多的數據滾動顯示,那麼如何計算那些視圖在什麼時候移除,又在什麼時候加載,什麼時候釋放成了一個關鍵的難題。
類似 UITableView 的 delegate 和 dateSource 的實現。
這裡看過一些 GitHub 項目,發現其最終還是基於 UITableView 來實現的,失望ing
這兩個問題,筆者實現的並不好,甚至很爛,同時限於篇幅,不再贅述這些問題。如果你在這裡有什麼好的經驗或者見解,希望來與我分享,非常感激。當然,如果你對我對這兩個問題的解決方案感興趣,可以查看源碼,隨時私信我。歡迎。我的微博: @靛青K
最後寫兩個不相關的兩件事以及一個相關的
最近很想有個實習的機會來鍛煉,如果你願意幫我,非常感激,坐標北京;
如果你覺得我們水平差不多,希望可以一起學習(自己孤零零的泡圖書館很辛苦),坐標北京;
接下來筆者可能嘗試編寫一個 tweak 使 Excel 支持邊緣滾動,如果在這方面你有什麼經驗,歡迎來與我探討。