概述
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));
該屬性用于標識內容區域和scrollView
的Auto 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
注: 當同時存在多個將該屬性設置為
YES
的scrollView
, 則該屬性在iPhone
中無效; 在iPad
中將距離狀態欄最近的scrollView
滾動到頂部
@property(nonatomic) BOOL scrollsToTop;
該屬性用于標識是否按頁數進行滾動, 默認值為NO
. 如果設置為YES
, 則在滾動時只會停止在scrollView
的bounds
的整數倍處
@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;
用戶界面實現原理
frame
和bounds
這部分內容將會簡單介紹一下UIView
的兩個屬性: frame
和bounds
, 這將有助于理解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}}
通過以上兩個代碼片段可以看出, superView
的bounds.origin
發生變化并不影響其自身所處的位置, 但是卻會影響到subView
的位置. 這是因為superView
的bounds.origin
發生變化直接導致了自身坐標系統的原點發生了改變, 即通過bounds.origin
設置的值便是superView
的左上角在自身坐標系統中的位置, 而superView
則會根據自身新的坐標系統更新其subView
的位置.
注: 本文在此僅涉及
bounds
屬性的變化對位置的影響, 如果想了解其對尺寸的影響煩請自行
實現原理
通過上一部分內容的介紹, 理解UIScrollView
用戶界面的實現原理將不再有困難. 其實UIScrollView
只是在用戶滾動的時候動態修改其bounds.origin
的值, 這樣便會相應地影響子視圖的位置變化, 而其他滑動相關屬性則均用于約束bounds.origin
的變化范圍. 以常用的四個屬性為例:
-
contentOffset
: 當用戶在scrollView
中向上滑動時, 設置bounds.origin
的值逐漸增加, 此時所有的子視圖便會相應地向上移動. 其實contentOffset = bounds.origin
. -
contentSize
: 由于bounds.origin
的值可以隨意變化, 因此scrollView
便可以無限制地向四周滾動. 其實contentSize
的值便是可滾動范圍的抽象. -
contentInset
和adjustedContentInset
: 在不改變contentSize
的前提下對可滾動范圍進行擴展.
iOS11
中的新變化
在iOS10
及以前, 當scrollView
所在的控制器位于導航控制器的最頂層時, 系統會通過contentInset
屬性自動為scrollView
上方增加64pt
的可滾動區域以防內容區域被導航欄遮擋. 該種優化方式可以通過設置控制器的automaticallyAdjustsScrollViewInsets = NO
來禁用.
注: 系統只在
UIScrollView
是控制器視圖的第0
個子視圖時才會自動修改其contentInset
屬性和scrollIndicatorInsets
屬性
在iOS11
中, 上述優化方式被廢棄. 系統通過adjustedContentInset
屬性配合contentInsetAdjustmentBehavior
屬性來處理scrollView
的內容區域超出安全區域以外的情況, 這是一種對原有優化方式的升級, 避免了原有的一刀切的優化方式.
注: 不要被圖片誤導,
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));