亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb

首頁 > 學院 > 開發設計 > 正文

iOS開發日記42-UIScrollView與UITableview

2019-11-14 18:01:19
字體:
來源:轉載
供稿:網友

今天博主有一個UIScrollView的需求,遇到了一些困難點,在此和大家分享,希望能夠共同進步.

UIScrollView(包括它的子類 UITableView 和 UICollectionView)是 iOS 開發中最常用也是最有意思的 UI 組件,大部分 App 的核心界面都是基于三者之一或三者的組合實現。

UIScrollView 是 UIKit 中為數不多能響應滑動手勢的 view,相比自己用 UipanGestureRecognizer 實現一些基于滑動手勢的效果,用 UIScrollView 的優勢在于 bounce 和 decelerate 等特性可以讓 App 的用戶體驗與 iOS 系統的用戶體驗保持一致。本文通過一些實例講解 UIScrollView 的特性和實際使用中的經驗。

UIScrollView 和 Auto Layout

iPhone 5 剛出來的時候,大部分不支持橫屏的 App 都不需要做太多的適配工作,因為屏幕寬度沒有變,table view 多個 cell 也不需要加 code。

但是在 iPhone 6 和 iPhone 6 Plus 發布以后,多分辨率適配終于不再是 Android 開發的專利了。于是,從 iOS 6 起就存在的 Auto Layout 終于有了用武之地。

關于 Auto Layout 的基本用法不再贅述,可以參考 Ray Wenderlich 上的教程Part 2)。

但 UIScrollView 在 Auto Layout 是一個很特殊的 view,對于 UIScrollView 的 subview 來說,它的 leading/trailing/top/bottom space 是相對于 UIScrollView 的 contentSize 而不是 bounds 來確定的,所以當你嘗試用 UIScrollView 和它 subview 的 leading/trailing/top/bottom 來互相決定大小的時候,就會出現「Has ambiguous scrollable content width/height」的 warning。

正確的姿勢是用 UIScrollView 外部的 view 或 UIScrollView 本身的 width/height 確定 subview 的尺寸,進而確定 contentSize。

因為 UIScrollView 本身的 leading/trailing/top/bottom 變得不好用,所以我習慣的做法是在 UIScrollView 和它原來的 subviews 之間增加一個 content view,這樣做的好處有:

  • 不會在 storyboard 里留下 error/warning
  • 為 subview 提供 leading/trailing/top/bottom,方便 subview 的布局
  • 通過調整 content view 的 size(可以是 constraint 的 IBOutlet)來調整 contentSize
  • 不需要 hard code 與屏幕尺寸相關的代碼
  • 更好地支持 rotation

Sample 中的 AutoLayout 演示了 UIScrollView + Auto Layout 的例子。

UIScrollViewDelegate

UIScrollViewDelegate 是 UIScrollView 的 delegate PRotocol,UIScrollView 有意思的功能都是通過它的 delegate 方法實現的。

了解這些方法被觸發的條件及調用的順序對于使用 UIScrollView 是很有必要的,本文主要講拖動相關的效果,所以 zoom 相關的方法跳過不提,拖動相關的 delegate 方法按調用順序分別是:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView

這個方法在任何方式觸發 contentOffset 變化的時候都會被調用(包括用戶拖動,減速過程,直接通過代碼設置等),可以用于監控 contentOffset 的變化,并根據當前的 contentOffset 對其他 view 做出隨動調整。

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView

用戶開始拖動 scroll view 的時候被調用。

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset

該方法從 iOS 5 引入,在 didEndDragging 前被調用,當 willEndDragging 方法中 velocity 為 CGPointZero(結束拖動時兩個方向都沒有速度)時,didEndDragging 中的 decelerate 為 NO,即沒有減速過程,willBeginDecelerating 和 didEndDecelerating 也就不會被調用。

反之,當 velocity 不為 CGPointZero 時,scroll view 會以 velocity 為初速度,減速直到 targetContentOffset。

值得注意的是,這里的 targetContentOffset 是個指針,沒錯,你可以改變減速運動的目的地,這在一些效果的實現時十分有用,實例章節中會具體提到它的用法,并和其他實現方式作比較。

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate

在用戶結束拖動后被調用,decelerate 為 YES 時,結束拖動后會有減速過程。

注,在 didEndDragging 之后,如果有減速過程,scroll view 的 dragging 并不會立即置為 NO,而是要等到減速結束之后,所以這個 dragging 屬性的實際語義更接近 scrolling。

- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView

減速動畫開始前被調用。

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView

減速動畫結束時被調用,這里有一種特殊情況:當一次減速動畫尚未結束的時候再次 drag scroll view,didEndDecelerating 不會被調用,并且這時 scroll view 的 dragging 和 decelerating 屬性都是 YES。

新的 dragging 如果有加速度,那么 willBeginDecelerating 會再一次被調用,然后才是 didEndDecelerating;如果沒有加速度,雖然 willBeginDecelerating 不會被調用,但前一次留下的 didEndDecelerating 會被調用,所以連續快速滾動一個 scroll view 時,delegate 方法被調用的順序(不含 didScroll)可能是這樣的:

scrollViewWillBeginDragging:  scrollViewWillEndDragging: withVelocity: targetContentOffset:  scrollViewDidEndDragging: willDecelerate:  scrollViewWillBeginDecelerating:  scrollViewWillBeginDragging:  scrollViewWillEndDragging: withVelocity: targetContentOffset:  scrollViewDidEndDragging: willDecelerate:  scrollViewWillBeginDecelerating:  ...scrollViewWillBeginDragging:  scrollViewWillEndDragging: withVelocity: targetContentOffset:  scrollViewDidEndDragging: willDecelerate:  scrollViewWillBeginDecelerating:  scrollViewDidEndDecelerating:  

雖然很少有因為這個導致的 bug,但是你需要知道這種很常見的用戶操作會導致的中間狀態。例如你嘗試在 UITableViewDataSource 的 tableView:cellForRowAtIndexPath: 方法中基于 tableView 的 dragging 和 decelerating 屬性判斷是在用戶拖拽還是減速過程中的話可能會誤判(見例 1)。

Sample 中的 Delegate 簡單輸出了一些 Log,你可以快速了解這些方法的調用順序。

實例

下面通過一些實例,更詳細地演示和描述以上各 delegate 方法的用途。

1. Table View 中圖片加載邏輯的優化

雖然這種優化方式在現在的機能和網絡環境下可能看似不那么必要,但在我最初看到這個方法是的 09 年(印象中是 Tweetie 作者在 08 年寫的 Blog,可能有誤),遙想 iPhone 3G/3GS 的機能,這個方法為多圖的 table view 的性能帶來很大的提升,也成了我的秘密武器。而現在,在移動網絡環境下,你依然值得這么做來為用戶節省流量。

先說一下原文的思路:

  • 當用戶手動 drag table view 的時候,會加載 cell 中的圖片;
  • 在用戶快速滑動的減速過程中,不加載過程中 cell 中的圖片(但文字信息還是會被加載,只是減少減速過程中的網絡開銷和圖片加載的開銷);
  • 在減速結束后,加載所有可見 cell 的圖片(如果需要的話);

問題 1:

前面提到,剛開始拖動的時候,dragging 為 YESdecelerating 為 NO;decelerate 過程中,dragging 和 decelerating 都為 YES;decelerate 未結束時開始下一次拖動,dragging 和 decelerating 依然都為 YES。所以無法簡單通過 table view 的 dragging 和 decelerating 判斷是在用戶拖動還是減速過程。

解決這個問題很簡單,添加一個變量如 userDragging,在 willBeginDragging 中設為 YES,didEndDragging 中設為 NO。那么 tableView: cellForRowAtIndexPath: 方法中,是否 load 圖片的邏輯就是:

if (!self.userDragging && tableView.decelerating) {      cell.imageView.image = nil;} else {    // code for loading image from network or disk}

問題 2:

這么做的話,decelerate 結束后,屏幕上的 cell 都是不帶圖片的,解決這個問題也不難,你需要一個形如 loadImageForVisibleCells 的方法,加載可見 cell 的圖片:

- (void)loadImageForVisibleCells{    NSArray *cells = [self.tableView visibleCells];    for (GLImageCell *cell in cells) {        NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];        [self setupCell:cell withIndexPath:indexPath];    }}

問題 3:

這個問題可能不容易被發現,在減速過程中如果用戶開始新的拖動,當前屏幕的 cell 并不會被加載(前文提到的調用順序問題導致),而且問題 1 的方案并不能解決問題 3,因為這些 cell 已經在屏上,不會再次經過 cellForRowAtIndexPath 方法。雖然不容易發現,但解決很簡單,只需要在 scrollViewWillBeginDragging: 方法里也調用一次 loadImageForVisibleCells 即可。

再優化

上述方法在那個年代的確提升了 table view 的 performance,但是你會發現在減速過程最后最慢的那零點幾秒時間,其實還是會讓人等得有些心急,尤其如果你的 App 只有圖片沒有文字。在 iOS 5 引入了 scrollViewWillEndDragging: withVelocity: targetContentOffset: 方法后,配合 SDWebImage,我嘗試再優化了一下這個方法以提升用戶體驗:

  • 如果內存中有圖片的緩存,減速過程中也會加載該圖片
  • 如果圖片屬于 targetContentOffset 能看到的 cell,正常加載,這樣一來,快速滾動的最后一屏出來的的過程中,用戶就能看到目標區域的圖片逐漸加載
  • 你可以嘗試用類似 fade in 或者 flip 的效果緩解生硬的突然出現(尤其是像本例這樣只有圖片的 App)

核心代碼:

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{    self.targetRect = nil;    [self loadImageForVisibleCells];}- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{    CGRect targetRect = CGRectMake(targetContentOffset->x, targetContentOffset->y, scrollView.frame.size.width, scrollView.frame.size.height);    self.targetRect = [NSValue valueWithCGRect:targetRect];}- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{    self.targetRect = nil;    [self loadImageForVisibleCells];}

是否需要加載圖片的邏輯:

BOOL shouldLoadImage = YES;  if (self.targetRect && !CGRectIntersectsRect([self.targetRect CGRectValue], cellFrame)) {      SDImageCache *cache = [manager imageCache];    NSString *key = [manager cacheKeyForURL:targetURL];    if (![cache imageFromMemoryCacheForKey:key]) {        shouldLoadImage = NO;    }}if (shouldLoadImage) {      // load image}

更值得高興的是,通過判斷是否 nil,targetRect 同時起到了原來 userDragging 的作用。本例完整的代碼見 Sample 中的 LazyLoad

2. 分頁的幾種實現方式

利用 UIScrollView 有多種方法實現分頁,但是各自的效果和用途不盡相同,其中方法 2 和方法 3 的區別也正是一些同類 App 在模仿 Glow 的首頁 Bubble 翻轉效果時跟 Glow 體驗上的的差距所在(但愿他們不會看到本文并且調整他們的實現方式)。本例通過三種方法實現相似的一個場景,你可以通過安裝到手機上來感受三種實現方式的不同用戶體驗。為了區分每個例子的重點,本例沒有重用機制,重用相關內容見例 3。

2.1 pagingEnabled

這是系統提供的分頁方式,最簡單,但是有一些局限性:

  • 只能以 frame size 為單位翻頁,減速動畫阻尼大,減速過程不超過一頁
  • 需要一些 hacking 實現 bleeding 和 padding(即頁與頁之間有 padding,在當前頁可以看到前后頁的部分內容)

Sample 中 Pagination 有簡單實現 bleeding 和 padding 效果的代碼,主要的思路是:

  • 讓 scroll view 的寬度為 page 寬度 + padding,并且設置 clipsToBounds 為 NO
  • 這樣雖然能看到前后頁的內容,但是無法響應 touch,所以需要另一個覆蓋期望的可觸摸區域的 view 來實現類似 touch bridging 的功能

適用場景:上述局限性同時也是這種實現方式的優點,比如一般 App 的引導頁(教程),Calendar 里的月視圖,都可以用這種方法實現。

2.2 Snap

這種方法就是在 didEndDragging 且無減速動畫,或在減速動畫完成時,snap 到一個整數頁。核心算法是通過當前 contentOffset 計算最近的整數頁及其對應的 contentOffset,通過動畫 snap 到該頁。這個方法實現的效果都有個通病,就是最后的 snap 會在 decelerate 結束以后才發生,總感覺很突兀。

2.3 修改 targetContentOffset

通過修改 scrollViewWillEndDragging: withVelocity: targetContentOffset: 方法中的 targetContentOffset 直接修改目標 offset 為整數頁位置。其中核心代碼:

- (CGPoint)nearestTargetOffsetForOffset:(CGPoint)offset{    CGFloat pageSize = BUBBLE_DIAMETER + BUBBLE_PADDING;    NSInteger page = roundf(offset.x / pageSize);    CGFloat targetX = pageSize * page;    return CGPointMake(targetX, offset.y);}- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{    CGPoint targetOffset = [self nearestTargetOffsetForOffset:*targetContentOffset];    targetContentOffset->x = targetOffset.x;    targetContentOffset->y = targetOffset.y;}

適用場景:方法 2 和 方法 3 的原理近似,效果也相近,適用場景也基本相同,但方法 3 的體驗會好很多,snap 到整數頁的過程很自然,或者說用戶完全感知不到 snap 過程的存在。這兩種方法的減速過程流暢,適用于一屏有多頁,但需要按整數頁滑動的場景;也適用于如圖表中自動 snap 到整數天的場景;還適用于每頁大小不同的情況下 snap 到整數頁的場景(不做舉例,自行發揮,其實只需要修改計算目標 offset 的方法)。

完整代碼參見 Pagination

3. 重用

大部分的 iOS 開發應該都清楚 UITableView 的 cell 重用機制,這種重用機制減少了內存開銷也提高了 performance,UIScrollView 作為 UITableView 的父類,在很多場景中也很適合應用重用機制(其實不只是 UIScrollView,任何場景中會反復出現的元素都應該適當地引入重用機制)。

你可以參照 UITableView 的 cell 重用機制,總結重用機制如下:

  • 維護一個重用隊列
  • 當元素離開可見范圍時,removeFromSuperview 并加入重用隊列(enqueue)
  • 當需要加入新的元素時,先嘗試從重用隊列獲取可重用元素(dequeue)并且從重用隊列移除
  • 如果隊列為空,新建元素
  • 這些一般都在 scrollViewDidScroll: 方法中完成

實際使用中,需要注意的點是:

  • 當重用對象為 view controller 時,記得 addChildeViewController
  • 當 view 或 view controller 被重用但其對應 model 發生變化的時候,需要及時清理重用前留下的內容
  • 數據可以適當做緩存,在重用的時候嘗試從緩存中讀取數據甚至之前的狀態(如 table view 的 contentOffset),以得到更好的用戶體驗
  • 當 on screen 的元素數量可確定的時候,有時候可以提前 init 這些元素,不會在 scroll 過程中遇到因為 init 開銷帶來的卡頓(尤其是以 view controller 為重用對象的時候)

例 2 中的場景很適合以 view 為重用單位,本例新增一個以 view controller 為重用對象的例子,該例子同時演示了聯動效果,具體見下個例子。

完整代碼參見 Reuse

4. 聯動/視差滾動

上一個例子里 main scroll view 和 title view 里的 scroll view 就是一個聯動的例子,所謂聯動,就是當 A 滾動的時候,在 scrollViewDidScroll: 里根據 A 的 contentOffset 動態計算 B 的 contentOffset 并設給 B。同樣對于非 scroll view 的 C,也可以動態計算 C 的 frame 或是 transform(Glow 的氣泡為例)實現視差滾動或者其他高級動畫,這在現在許多應用的引導頁面里會被用到。

聯動/視差滾動部分原理上其實比較簡單,不再贅述,寫了個簡單的例子 Parallax。

寫在最后

不知不覺就寫了很多關于 UIScrollView 的內容,其實還有很多可寫,由于時間關系只好停筆。在我看來,UIScrollView 就好像提供了一個跳脫二維空間束縛的途徑,如果你有足夠的想象力,它能幫你實現更豐富的跳出平面束縛的用戶體驗。本來還準備寫一個綜合性的例子,但是由于時間關系還沒完成,后面有時間會繼續更新。

此外,例子中可能會有錯誤或可以改進的地方,歡迎在 GitHub 直接提 Issue

 


發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb
韩国欧美亚洲国产| 国产一区二区黑人欧美xxxx| 精品国产电影一区| 精品动漫一区二区| 成人国产精品久久久| 亚洲欧洲中文天堂| 精品亚洲国产视频| 91系列在线播放| 日韩av在线网页| 精品久久久一区| 亚洲欧洲成视频免费观看| 一区二区三区视频免费在线观看| 中文字幕精品久久久久| 精品亚洲一区二区三区在线观看| 亚洲午夜精品久久久久久久久久久久| 欧美激情一区二区三级高清视频| 全亚洲最色的网站在线观看| 亚洲跨种族黑人xxx| 欧美性videos高清精品| 日本高清视频一区| 精品国产91久久久| 久久久久久久久中文字幕| 国产成人精品免高潮在线观看| 欧美老女人性视频| 成人h视频在线| 国产精品久久久久久超碰| 精品亚洲一区二区三区在线观看| 91网在线免费观看| 一个人看的www久久| 91中文在线视频| 国产在线一区二区三区| 欧美日韩国产一区二区三区| 中文字幕欧美专区| 国产午夜精品全部视频播放| 亚洲精品aⅴ中文字幕乱码| 久久国产精品电影| 国产91精品久久久久久| 久久人人看视频| 国色天香2019中文字幕在线观看| 尤物99国产成人精品视频| 国产成人啪精品视频免费网| 亚洲白拍色综合图区| 中文字幕在线国产精品| 91福利视频网| 亚洲成人精品久久久| 国产成人免费av电影| 欧美精品免费在线观看| 欧美色播在线播放| 亚洲精品中文字幕有码专区| 欧美制服第一页| 丝袜亚洲欧美日韩综合| 爱福利视频一区| 国产日产亚洲精品| 最近2019中文字幕一页二页| 国产欧美一区二区三区在线| 欧美成人在线网站| 自拍偷拍免费精品| 欧美性猛交xxxx偷拍洗澡| 欧美乱妇高清无乱码| 亚洲欧美日韩网| 92看片淫黄大片欧美看国产片| 欧美丰满老妇厨房牲生活| 岛国av一区二区在线在线观看| 亚洲老头同性xxxxx| 超碰91人人草人人干| 久久av在线播放| 国产一区二区三区免费视频| 欧美自拍视频在线| 亚洲性69xxxbbb| y97精品国产97久久久久久| 亚洲国产婷婷香蕉久久久久久| 欧美日韩亚洲网| 亚洲人成欧美中文字幕| 91中文在线观看| 亚洲人成网在线播放| 亚洲激情在线观看| 成人激情在线观看| 热re99久久精品国产66热| 欧美最猛黑人xxxx黑人猛叫黄| 欧美亚洲另类在线| 久久久www成人免费精品| 青青久久av北条麻妃黑人| 成人激情视频网| 综合136福利视频在线| 国产成人aa精品一区在线播放| 国产精品久久久久久久久久久新郎| 久久精品久久精品亚洲人| 日韩在线观看高清| 欧美在线日韩在线| 亚洲精品国产综合久久| 国产精品久久久久久久久久东京| 亚洲视频在线观看视频| 色偷偷偷综合中文字幕;dd| 欧美最猛性xxxxx(亚洲精品)| 欧美中文在线观看| 国产原创欧美精品| 欧美理论在线观看| 中文字幕最新精品| 日韩精品丝袜在线| 久久精品一本久久99精品| 久久久久久久久久国产精品| 欧美激情在线视频二区| 久久99精品久久久久久琪琪| 亚洲图片在线综合| 97国产真实伦对白精彩视频8| 国产精品揄拍500视频| 国产精品久久久久秋霞鲁丝| 欧美电影免费在线观看| 国产精品久久av| 精品国产一区二区三区久久狼5月| 久久在线免费视频| 亚洲欧美精品一区二区| 亚洲人成在线免费观看| 精品视频在线播放| 97av视频在线| 国产精品h在线观看| 久久久久国产精品免费网站| 日韩欧美第一页| 国产精选久久久久久| 欧美另类高清videos| 国产99视频在线观看| 在线日韩第一页| 亚洲乱码一区二区| 国产91精品久| 国产精品久久久久久久久久三级| 国产精品爱久久久久久久| 自拍偷拍亚洲精品| 九色精品美女在线| 国产日韩精品入口| 亚洲性日韩精品一区二区| 亚洲最新视频在线| 亚洲一区精品电影| 欧美日韩午夜视频在线观看| 色妞欧美日韩在线| 国产精品免费久久久久影院| 亚洲欧美另类国产| 性欧美xxxx视频在线观看| 欧美国产日韩一区| 久久精品国产一区| 亚洲自拍偷拍视频| 日韩av免费看网站| 亚洲精品国产精品国自产观看浪潮| 日韩va亚洲va欧洲va国产| 成人精品一区二区三区| 国产精品综合网站| 亚洲人在线观看| 日韩精品中文在线观看| 91国在线精品国内播放| 久久精品国产v日韩v亚洲| 中文字幕无线精品亚洲乱码一区| 国产日韩av高清| 国产欧美韩国高清| 国产一区二区三区日韩欧美| 国产成人jvid在线播放| 亚洲精品ady| 在线观看欧美成人| 亚洲欧美成人一区二区在线电影| 久久亚洲精品国产亚洲老地址| 欧美高跟鞋交xxxxhd| 日本一区二区三区四区视频| 狠狠久久五月精品中文字幕| 在线色欧美三级视频| 日韩黄色高清视频| 亚洲aⅴ日韩av电影在线观看|