UIScrollView的詳細使用介紹和實現原理分析[2018.06.20更新]

UIScrollView.png

概述

UIScrollView(滾動視圖)是一個在日常開發中使用頻率極高的容器視圖控件, 它允許用戶通過滾動和縮放的方式查看超出屏幕區域大小的內容, 在應用程序開發中經常使用到的UITableView(列表視圖)、UICollectionView(集合視圖)和UITextView(文本視圖)都是它的子類.

下面將從用戶界面和事件處理兩個方面對UIScrollView做一次詳細的使用介紹和簡要的實現原理分析.

用戶界面相關

內容區域相關API介紹

該屬性用于標識內容區域的起點相對于scrollView的起點的偏移量, 默認值為CGPointZero

@property(nonatomic) CGPoint contentOffset;

- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated;

該屬性用于標識內容區域的尺寸, 默認值為CGSizeZero

@property(nonatomic) CGSize contentSize;

該屬性用于標識為內容區域周圍增加的可滾動區域, 默認值為UIEdgeInsetsZero

@property(nonatomic) UIEdgeInsets contentInset;

該屬性用于標識為內容區域周圍增加的總的可滾動區域, 該屬性值的最終結果取決于contentInsetAdjustmentBehavior屬性的值

@property(nonatomic, readonly) UIEdgeInsets adjustedContentInset API_AVAILABLE(ios(11.0));

- (void)adjustedContentInsetDidChange API_AVAILABLE(ios(11.0)) NS_REQUIRES_SUPER;

該屬性用于配置safeAreaInsets如何影響adjustedContentInset屬性的值, 該屬性可設置四個枚舉值:

  • UIScrollViewContentInsetAdjustmentAutomatic: 默認, 在UIScrollViewContentInsetAdjustmentScrollableAxes的基礎上添加了向前兼容. 不論是否可以滾動, 如果scrollView所在的控制器位于導航控制器中且automaticallyAdjustsScrollViewInsets = YES, 則在上下兩個方向上adjustedContentInset = contentInset + safeAreaInsets成立
  • UIScrollViewContentInsetAdjustmentScrollableAxes: 在可滾動方向上adjustedContentInset = contentInset + safeAreaInsets成立. 比如: contentSize.width/height > frame.size.width/height或者alwaysBounceHorizontal/Vertical = YES
  • UIScrollViewContentInsetAdjustmentNever: 在任何情況下adjustedContentInset = contentInset成立
  • UIScrollViewContentInsetAdjustmentAlways: 在任何情況下adjustedContentInset = contentInset + safeAreaInsets成立
@property(nonatomic) UIScrollViewContentInsetAdjustmentBehavior contentInsetAdjustmentBehavior API_AVAILABLE(ios(11.0));

該屬性用于標識內容區域和scrollViewAuto Layout參考線

@property(nonatomic,readonly,strong) UILayoutGuide *contentLayoutGuide API_AVAILABLE(ios(11.0));

@property(nonatomic,readonly,strong) UILayoutGuide *frameLayoutGuide API_AVAILABLE(ios(11.0));
指示器相關API介紹

該屬性用于配置指示器樣式, 該屬性可設置三個枚舉值:

  • UIScrollViewIndicatorStyleDefault: 默認, 黑內容白邊框, 適用于任何背景
  • UIScrollViewIndicatorStyleBlack: 全黑, 較小, 適用于白色背景
  • UIScrollViewIndicatorStyleWhite: 全白, 較小, 適用于黑色背景
@property(nonatomic) UIScrollViewIndicatorStyle indicatorStyle;

該屬性用于標識為指示器周圍增加的可滾動區域, 默認值為UIEdgeInsetsZero

@property(nonatomic) UIEdgeInsets scrollIndicatorInsets;

該屬性用于標識是否在滾動時指示器可見, 默認為值YES

@property(nonatomic) BOOL showsHorizontalScrollIndicator;
@property(nonatomic) BOOL showsVerticalScrollIndicator;

該方法用于閃動一下指示器. 建議在將scrollView展示給用戶時調用一下, 以提醒用戶該控件可以滾動

- (void)flashScrollIndicators;
滾動相關API介紹

該屬性用于標識是否允許滾動, 默認值為YES

@property(nonatomic,getter=isScrollEnabled) BOOL scrollEnabled;

該屬性用于標識是否只允許同時滾動一個方向, 默認值為NO. 如果設置為YES, 則用戶在水平/豎直方向上開始進行滾動操作, 便禁止同時在豎直/水平方向上進行滾動

注: 當用戶在對角線方向上開始進行滾動操作, 則本次滾動可以同時在兩個方向上進行滾動

@property(nonatomic, getter=isDirectionalLockEnabled) BOOL directionalLockEnabled;

該屬性用于標識是否允許通過點擊狀態欄讓距離狀態欄最近的scrollView滾動到頂部, 默認值為YES

注: 當同時存在多個將該屬性設置為YESscrollView, 則該屬性在iPhone中無效; 在iPad中將距離狀態欄最近的scrollView滾動到頂部

@property(nonatomic) BOOL scrollsToTop;

該屬性用于標識是否按頁數進行滾動, 默認值為NO. 如果設置為YES, 則在滾動時只會停止在scrollViewbounds的整數倍處

@property(nonatomic, getter=isPagingEnabled) BOOL pagingEnabled;

該屬性用于標識是否有觸底反彈效果, 默認值為YES

@property(nonatomic) BOOL bounces;

該屬性用于標識是否總是有觸底反彈效果(即使contentSize小于scrollView的尺寸), 默認值為NO

注: 該屬性生效的前提條件為bounces = YES

@property(nonatomic) BOOL alwaysBounceHorizontal;
@property(nonatomic) BOOL alwaysBounceVertical;

該屬性用于配置當用戶手指離開屏幕后滾動減速的速率, 該屬性可設置兩個常量:

  • UIScrollViewDecelerationRateNormal: 默認, 慢慢停止
  • UIScrollViewDecelerationRateFast: 快速停止
@property(nonatomic) CGFloat decelerationRate NS_AVAILABLE_IOS(3_0);

該方法用于將指定區域滾動到剛好可見處

- (void)scrollRectToVisible:(CGRect)rect animated:(BOOL)animated;
縮放相關API介紹

該屬性用于標識最小縮放比例, 默認值為1.0

@property(nonatomic) CGFloat minimumZoomScale;

該屬性用于標識最大縮放比例, 默認值為1.0

注: 該屬性值必須大于minimumZoomScale才能進行縮放

@property(nonatomic) CGFloat maximumZoomScale;

該屬性用于標識縮放比例, 默認值為1.0

@property(nonatomic) CGFloat zoomScale NS_AVAILABLE_IOS(3_0);

- (void)setZoomScale:(CGFloat)scale animated:(BOOL)animated NS_AVAILABLE_IOS(3_0);

該方法用于將內容縮放到指定區域

- (void)zoomToRect:(CGRect)rect animated:(BOOL)animated NS_AVAILABLE_IOS(3_0);

該屬性用于標識是否允許觸底反彈, 默認值為YES

@property(nonatomic) BOOL bouncesZoom;

該屬性用于標識是否正在縮放

@property(nonatomic,readonly,getter=isZooming) BOOL zooming;

該屬性用于標識是否正在觸底反彈

@property(nonatomic,readonly,getter=isZoomBouncing) BOOL zoomBouncing;

用戶界面實現原理

framebounds

這部分內容將會簡單介紹一下UIView的兩個屬性: framebounds, 這將有助于理解UIScrollView用戶界面的實現原理.

iOS系統中, 視圖的坐標系統的原點默認位于視圖的左上角, 右方向為x軸的正方向, 下方向為y軸的正方向. 其中, frame用于描述視圖在父視圖坐標系統中的位置和尺寸; bounds用于描述視圖在自身坐標系統中的位置和尺寸. 下面通過兩個代碼片段來具體說明:

// 代碼片段1
UIView *superView = [[UIView alloc] initWithFrame:CGRectMake(20.f, 20.f, 100.f, 100.f)];
superView.backgroundColor = [UIColor redColor];
[self.view addSubview:superView];

UIView *subView = [[UIView alloc] initWithFrame:CGRectMake(20.f, 20.f, 60.f, 60.f)];
subView.backgroundColor = [UIColor yellowColor];
[superView addSubview:subView];

NSLog(@"superView.frame = %@, superView.bounds = %@", NSStringFromCGRect(superView.frame), NSStringFromCGRect(superView.bounds));
// 輸出: superView.frame = {{20, 20}, {100, 100}}, superView.bounds = {{0, 0}, {100, 100}}
NSLog(@"subView.frame = %@, subView.bounds = %@", NSStringFromCGRect(subView.frame), NSStringFromCGRect(subView.bounds));
// 輸出: subView.frame = {{20, 20}, {60, 60}}, subView.bounds = {{0, 0}, {60, 60}}
父視圖坐標系統
// 代碼片段2
UIView *superView = [[UIView alloc] initWithFrame:CGRectMake(20.f, 20.f, 100.f, 100.f)];
superView.backgroundColor = [UIColor redColor];
[self.view addSubview:superView];

UIView *subView = [[UIView alloc] initWithFrame:CGRectMake(20.f, 20.f, 60.f, 60.f)];
subView.backgroundColor = [UIColor yellowColor];
[superView addSubview:subView];

// 新增代碼
superView.bounds = CGRectMake(0, 20, 100, 100);

NSLog(@"superView.frame = %@, superView.bounds = %@", NSStringFromCGRect(superView.frame), NSStringFromCGRect(superView.bounds));
// 輸出: superView.frame = {{20, 20}, {100, 100}}, superView.bounds = {{0, 20}, {100, 100}}
NSLog(@"subView.frame = %@, subView.bounds = %@", NSStringFromCGRect(subView.frame), NSStringFromCGRect(subView.bounds));
// 輸出: subView.frame = {{20, 20}, {60, 60}}, subView.bounds = {{0, 0}, {60, 60}}
自身坐標系統

通過以上兩個代碼片段可以看出, superViewbounds.origin發生變化并不影響其自身所處的位置, 但是卻會影響到subView的位置. 這是因為superViewbounds.origin發生變化直接導致了自身坐標系統的原點發生了改變, 即通過bounds.origin設置的值便是superView的左上角在自身坐標系統中的位置, 而superView則會根據自身新的坐標系統更新其subView的位置.

注: 本文在此僅涉及bounds屬性的變化對位置的影響, 如果想了解其對尺寸的影響煩請自行Google.

實現原理

通過上一部分內容的介紹, 理解UIScrollView用戶界面的實現原理將不再有困難. 其實UIScrollView只是在用戶滾動的時候動態修改其bounds.origin的值, 這樣便會相應地影響子視圖的位置變化, 而其他滑動相關屬性則均用于約束bounds.origin的變化范圍. 以常用的四個屬性為例:

  • contentOffset: 當用戶在scrollView中向上滑動時, 設置bounds.origin的值逐漸增加, 此時所有的子視圖便會相應地向上移動. 其實contentOffset = bounds.origin.
  • contentSize: 由于bounds.origin的值可以隨意變化, 因此scrollView便可以無限制地向四周滾動. 其實contentSize的值便是可滾動范圍的抽象.
  • contentInsetadjustedContentInset: 在不改變contentSize的前提下對可滾動范圍進行擴展.
iOS11中的新變化

iOS10及以前, 當scrollView所在的控制器位于導航控制器的最頂層時, 系統會通過contentInset屬性自動為scrollView上方增加64pt的可滾動區域以防內容區域被導航欄遮擋. 該種優化方式可以通過設置控制器的automaticallyAdjustsScrollViewInsets = NO來禁用.

注: 系統只在UIScrollView是控制器視圖的第0個子視圖時才會自動修改其contentInset屬性和scrollIndicatorInsets屬性

iOS11中, 上述優化方式被廢棄. 系統通過adjustedContentInset屬性配合contentInsetAdjustmentBehavior屬性來處理scrollView的內容區域超出安全區域以外的情況, 這是一種對原有優化方式的升級, 避免了原有的一刀切的優化方式.

adjustedContentInset.png

注: 不要被圖片誤導, adjustedContentInset屬性的值是包含contentInset屬性的值的

事件處理相關

觸摸相關API介紹

該屬性用于標識用戶是否已經觸摸了內容區域并準備進行滑動

注: 該屬性值被設置為YES的時候用戶可能只是觸摸了內容區域, 但是并沒有開始進行滑動

@property(nonatomic,readonly,getter=isTracking) BOOL tracking;

該屬性用于標識用戶是否已經開始滑動內容區域

注: 該屬性值被設置為YES之前用戶可能需要先滑動一段時間或距離

@property(nonatomic,readonly,getter=isDragging) BOOL dragging;

該屬性用于標識是否正在處于減速狀態(即手指已經離開屏幕, 但scrollView仍然處于滑動中)

@property(nonatomic,readonly,getter=isDecelerating) BOOL decelerating;

該屬性用于標識是否延遲內容區域的事件傳遞, 默認值為YES. 如果設置為NO, 則scrollView會立即調用-touchesShouldBegin:withEvent:inContentView:方法以進行下一步操作

@property(nonatomic) BOOL delaysContentTouches;

當已經將事件傳遞給子視圖后是否可以取消, 默認值為YES. 如果設置為NO, 則一旦開始跟蹤事件, 即使手指進行移動也不會取消已經傳遞給子視圖的事件

@property(nonatomic) BOOL canCancelContentTouches;

該方法用于在UIScrollView的子類中重寫, 返回是否將事件傳遞給對應的子視圖, 默認返回YES. 如果返回NO, 則該事件不會傳遞給對應的子視圖

- (BOOL)touchesShouldBegin:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event inContentView:(UIView *)view;

該方法用于在UIScrollView的子類中重寫, 返回當已經將事件傳遞給子視圖后是否可以取消. 默認當子視圖是UIControl時返回NO, 即不再繼續跟蹤用戶的觸摸事件; 否則返回YES, 即仍然繼續跟蹤用戶的觸摸事件

注: 該方法被調用的前提是canCancelContentTouches = YES

- (BOOL)touchesShouldCancelInContentView:(UIView *)view;

其他相關API介紹

該屬性用于配置隱藏鍵盤的模式, 該屬性可設置三個枚舉值:

  • UIScrollViewKeyboardDismissModeNone: 默認值, 不隱藏鍵盤
  • UIScrollViewKeyboardDismissModeOnDrag: 當拖拽時隱藏鍵盤
  • UIScrollViewKeyboardDismissModeInteractive: 當拖拽鍵盤上方時隱藏鍵盤, 如果反向拖拽鍵盤會取消隱藏
@property(nonatomic) UIScrollViewKeyboardDismissMode keyboardDismissMode NS_AVAILABLE_IOS(7_0);

該屬性用于標識內建的拖動手勢和捏合手勢, 可在此對其進行配置

@property(nonatomic, readonly) UIPanGestureRecognizer *panGestureRecognizer NS_AVAILABLE_IOS(5_0);
@property(nonatomic, readonly) UIPinchGestureRecognizer *pinchGestureRecognizer NS_AVAILABLE_IOS(5_0);

該屬性用于標識內建的下拉刷新控件, 可在此實現下拉刷新功能

@property (nonatomic, strong, nullable) UIRefreshControl *refreshControl NS_AVAILABLE_IOS(10_0);

事件處理實現原理

由于scrollView并沒有用于直接操控的滾動條, 因此用戶只能通過直接操作scrollView的內容區域以便進行滾動操作. 但是當用戶觸碰到屏幕上時, scrollView并不清楚該用戶的目的是想要進行滾動操作還是單純地想要點擊某一個視圖. 為了處理這種情況, 當用戶觸碰屏幕時, scrollView首先攔截到該觸摸事件并啟用一個150s的定時器, 同時觀察用戶的下一步行為.

  • 當定時器結束前, 如果用戶的觸摸點發生足夠的移動, 則直接滾動內容區域, 并且不會繼續將該觸摸事件傳遞給子視圖.
  • 當定時器結束后, 如果用戶的觸摸點并沒有發生足夠的移動, 則調用-touchesShouldBegin:withEvent:inContentView:方法詢問是否將事件傳遞給對應的子視圖. 如果返回NO, 則該事件不會傳遞給對應的子視圖; 如果返回YES, 則該事件會傳遞給對應的子視圖, 默認為YES.
  • 當觸摸事件被傳遞給子視圖后, 如果canCancelContentTouches=YES, 則會立即調用-touchesShouldCancelInContentView:方法詢問是否可以取消已經傳遞給子視圖的事件. 如果返回NO, 則不再進一步跟蹤用戶的觸摸事件; 如果返回YES, 則當用戶的觸摸點又發生足夠的移動時, 系統會向該子視圖發送-touchesCancelled:withEvent:消息并進行滑動.

代理相關

該方法在contentOffset發生變化時調用

- (void)scrollViewDidScroll:(UIScrollView *)scrollView;

該方法在將要開始拖拽時調用

注: 該方法可能需要先滑動一段時間或距離才會被調用

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView;

該方法在用戶停止拖拽時調用

注: 應用程序可以通過修改targetContentOffset參數的值來調整停止的位置

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset NS_AVAILABLE_IOS(5_0);

該方法在用戶停止拖拽時調用

注: 如果在停止拖拽后繼續移動, 則decelerate參數為YES

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

該方法在將要開始減速時調用

注: 僅當停止拖拽后繼續移動時才會被調用

- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView;

該方法在已經結束減速時調用

注: 僅當停止拖拽后繼續移動時才會被調用

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;

該方法用于返回是否允許點擊狀態欄讓scrollView滑動到頂部, 默認值為YES

注: 僅當scrollsToTop屬性值為YES時才調用

- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView;

該方法在scrollView已經滑動到頂部時調用

注: 僅當通過點擊狀態欄讓scrollView滑動到頂部才調用

- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView;

該方法在-setContentOffset:animated:/-scrollRectVisible:animated:方法動畫結束時調用

注: 僅當animated設置為YES時才調用

- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView;

該方法在縮放比例發生變化時調用

- (void)scrollViewDidZoom:(UIScrollView *)scrollView NS_AVAILABLE_IOS(3_2);

該方法用于返回參與縮放的子視圖

- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView;

該方法在將要開始縮放時調用

- (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view NS_AVAILABLE_IOS(3_2);

該方法在已經結束縮放時調用

- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale;

該方法在adjustedContentInset發生變化時調用

- (void)scrollViewDidChangeAdjustedContentInset:(UIScrollView *)scrollView API_AVAILABLE(ios(11.0));
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容