概述
UIScrollView
(滾動視圖)是一個在日常開發(fā)中使用頻率極高的容器視圖控件, 它允許用戶通過滾動和縮放的方式查看超出屏幕區(qū)域大小的內(nèi)容, 在應(yīng)用程序開發(fā)中經(jīng)常使用到的UITableView
(列表視圖)、UICollectionView
(集合視圖)和UITextView
(文本視圖)都是它的子類.
下面將從用戶界面和事件處理兩個方面對UIScrollView
做一次詳細(xì)的使用介紹和簡要的實現(xiàn)原理分析.
用戶界面相關(guān)
內(nèi)容區(qū)域相關(guān)API
介紹
該屬性用于標(biāo)識內(nèi)容區(qū)域的起點相對于scrollView
的起點的偏移量, 默認(rèn)值為CGPointZero
@property(nonatomic) CGPoint contentOffset;
- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated;
該屬性用于標(biāo)識內(nèi)容區(qū)域的尺寸, 默認(rèn)值為CGSizeZero
@property(nonatomic) CGSize contentSize;
該屬性用于標(biāo)識為內(nèi)容區(qū)域周圍增加的可滾動區(qū)域, 默認(rèn)值為UIEdgeInsetsZero
@property(nonatomic) UIEdgeInsets contentInset;
該屬性用于標(biāo)識為內(nèi)容區(qū)域周圍增加的總的可滾動區(qū)域, 該屬性值的最終結(jié)果取決于contentInsetAdjustmentBehavior
屬性的值
@property(nonatomic, readonly) UIEdgeInsets adjustedContentInset API_AVAILABLE(ios(11.0));
- (void)adjustedContentInsetDidChange API_AVAILABLE(ios(11.0)) NS_REQUIRES_SUPER;
該屬性用于配置safeAreaInsets
如何影響adjustedContentInset
屬性的值, 該屬性可設(shè)置四個枚舉值:
-
UIScrollViewContentInsetAdjustmentAutomatic
: 默認(rèn), 在UIScrollViewContentInsetAdjustmentScrollableAxes
的基礎(chǔ)上添加了向前兼容. 不論是否可以滾動, 如果scrollView
所在的控制器位于導(dǎo)航控制器中且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));
該屬性用于標(biāo)識內(nèi)容區(qū)域和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));
指示器相關(guān)API
介紹
該屬性用于配置指示器樣式, 該屬性可設(shè)置三個枚舉值:
-
UIScrollViewIndicatorStyleDefault
: 默認(rèn), 黑內(nèi)容白邊框, 適用于任何背景 -
UIScrollViewIndicatorStyleBlack
: 全黑, 較小, 適用于白色背景 -
UIScrollViewIndicatorStyleWhite
: 全白, 較小, 適用于黑色背景
@property(nonatomic) UIScrollViewIndicatorStyle indicatorStyle;
該屬性用于標(biāo)識為指示器周圍增加的可滾動區(qū)域, 默認(rèn)值為UIEdgeInsetsZero
@property(nonatomic) UIEdgeInsets scrollIndicatorInsets;
該屬性用于標(biāo)識是否在滾動時指示器可見, 默認(rèn)為值YES
@property(nonatomic) BOOL showsHorizontalScrollIndicator;
@property(nonatomic) BOOL showsVerticalScrollIndicator;
該方法用于閃動一下指示器. 建議在將scrollView
展示給用戶時調(diào)用一下, 以提醒用戶該控件可以滾動
- (void)flashScrollIndicators;
滾動相關(guān)API
介紹
該屬性用于標(biāo)識是否允許滾動, 默認(rèn)值為YES
@property(nonatomic,getter=isScrollEnabled) BOOL scrollEnabled;
該屬性用于標(biāo)識是否只允許同時滾動一個方向, 默認(rèn)值為NO
. 如果設(shè)置為YES
, 則用戶在水平/豎直方向上開始進(jìn)行滾動操作, 便禁止同時在豎直/水平方向上進(jìn)行滾動
注: 當(dāng)用戶在對角線方向上開始進(jìn)行滾動操作, 則本次滾動可以同時在兩個方向上進(jìn)行滾動
@property(nonatomic, getter=isDirectionalLockEnabled) BOOL directionalLockEnabled;
該屬性用于標(biāo)識是否允許通過點擊狀態(tài)欄讓距離狀態(tài)欄最近的scrollView
滾動到頂部, 默認(rèn)值為YES
注: 當(dāng)同時存在多個將該屬性設(shè)置為
YES
的scrollView
, 則該屬性在iPhone
中無效; 在iPad
中將距離狀態(tài)欄最近的scrollView
滾動到頂部
@property(nonatomic) BOOL scrollsToTop;
該屬性用于標(biāo)識是否按頁數(shù)進(jìn)行滾動, 默認(rèn)值為NO
. 如果設(shè)置為YES
, 則在滾動時只會停止在scrollView
的bounds
的整數(shù)倍處
@property(nonatomic, getter=isPagingEnabled) BOOL pagingEnabled;
該屬性用于標(biāo)識是否有觸底反彈效果, 默認(rèn)值為YES
@property(nonatomic) BOOL bounces;
該屬性用于標(biāo)識是否總是有觸底反彈效果(即使contentSize
小于scrollView
的尺寸), 默認(rèn)值為NO
注: 該屬性生效的前提條件為
bounces = YES
@property(nonatomic) BOOL alwaysBounceHorizontal;
@property(nonatomic) BOOL alwaysBounceVertical;
該屬性用于配置當(dāng)用戶手指離開屏幕后滾動減速的速率, 該屬性可設(shè)置兩個常量:
-
UIScrollViewDecelerationRateNormal
: 默認(rèn), 慢慢停止 -
UIScrollViewDecelerationRateFast
: 快速停止
@property(nonatomic) CGFloat decelerationRate NS_AVAILABLE_IOS(3_0);
該方法用于將指定區(qū)域滾動到剛好可見處
- (void)scrollRectToVisible:(CGRect)rect animated:(BOOL)animated;
縮放相關(guān)API
介紹
該屬性用于標(biāo)識最小縮放比例, 默認(rèn)值為1.0
@property(nonatomic) CGFloat minimumZoomScale;
該屬性用于標(biāo)識最大縮放比例, 默認(rèn)值為1.0
注: 該屬性值必須大于minimumZoomScale才能進(jìn)行縮放
@property(nonatomic) CGFloat maximumZoomScale;
該屬性用于標(biāo)識縮放比例, 默認(rèn)值為1.0
@property(nonatomic) CGFloat zoomScale NS_AVAILABLE_IOS(3_0);
- (void)setZoomScale:(CGFloat)scale animated:(BOOL)animated NS_AVAILABLE_IOS(3_0);
該方法用于將內(nèi)容縮放到指定區(qū)域
- (void)zoomToRect:(CGRect)rect animated:(BOOL)animated NS_AVAILABLE_IOS(3_0);
該屬性用于標(biāo)識是否允許觸底反彈, 默認(rèn)值為YES
@property(nonatomic) BOOL bouncesZoom;
該屬性用于標(biāo)識是否正在縮放
@property(nonatomic,readonly,getter=isZooming) BOOL zooming;
該屬性用于標(biāo)識是否正在觸底反彈
@property(nonatomic,readonly,getter=isZoomBouncing) BOOL zoomBouncing;
用戶界面實現(xiàn)原理
frame
和bounds
這部分內(nèi)容將會簡單介紹一下UIView
的兩個屬性: frame
和bounds
, 這將有助于理解UIScrollView
用戶界面的實現(xiàn)原理.
在iOS
系統(tǒng)中, 視圖的坐標(biāo)系統(tǒng)的原點默認(rèn)位于視圖的左上角, 右方向為x
軸的正方向, 下方向為y
軸的正方向. 其中, frame
用于描述視圖在父視圖坐標(biāo)系統(tǒng)中的位置和尺寸; bounds
用于描述視圖在自身坐標(biāo)系統(tǒng)中的位置和尺寸. 下面通過兩個代碼片段來具體說明:
// 代碼片段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
發(fā)生變化并不影響其自身所處的位置, 但是卻會影響到subView
的位置. 這是因為superView
的bounds.origin
發(fā)生變化直接導(dǎo)致了自身坐標(biāo)系統(tǒng)的原點發(fā)生了改變, 即通過bounds.origin
設(shè)置的值便是superView
的左上角在自身坐標(biāo)系統(tǒng)中的位置, 而superView
則會根據(jù)自身新的坐標(biāo)系統(tǒng)更新其subView
的位置.
注: 本文在此僅涉及
bounds
屬性的變化對位置的影響, 如果想了解其對尺寸的影響煩請自行
實現(xiàn)原理
通過上一部分內(nèi)容的介紹, 理解UIScrollView
用戶界面的實現(xiàn)原理將不再有困難. 其實UIScrollView
只是在用戶滾動的時候動態(tài)修改其bounds.origin
的值, 這樣便會相應(yīng)地影響子視圖的位置變化, 而其他滑動相關(guān)屬性則均用于約束bounds.origin
的變化范圍. 以常用的四個屬性為例:
-
contentOffset
: 當(dāng)用戶在scrollView
中向上滑動時, 設(shè)置bounds.origin
的值逐漸增加, 此時所有的子視圖便會相應(yīng)地向上移動. 其實contentOffset = bounds.origin
. -
contentSize
: 由于bounds.origin
的值可以隨意變化, 因此scrollView
便可以無限制地向四周滾動. 其實contentSize
的值便是可滾動范圍的抽象. -
contentInset
和adjustedContentInset
: 在不改變contentSize
的前提下對可滾動范圍進(jìn)行擴展.
iOS11
中的新變化
在iOS10
及以前, 當(dāng)scrollView
所在的控制器位于導(dǎo)航控制器的最頂層時, 系統(tǒng)會通過contentInset
屬性自動為scrollView
上方增加64pt
的可滾動區(qū)域以防內(nèi)容區(qū)域被導(dǎo)航欄遮擋. 該種優(yōu)化方式可以通過設(shè)置控制器的automaticallyAdjustsScrollViewInsets = NO
來禁用.
注: 系統(tǒng)只在
UIScrollView
是控制器視圖的第0
個子視圖時才會自動修改其contentInset
屬性和scrollIndicatorInsets
屬性
在iOS11
中, 上述優(yōu)化方式被廢棄. 系統(tǒng)通過adjustedContentInset
屬性配合contentInsetAdjustmentBehavior
屬性來處理scrollView
的內(nèi)容區(qū)域超出安全區(qū)域以外的情況, 這是一種對原有優(yōu)化方式的升級, 避免了原有的一刀切的優(yōu)化方式.
注: 不要被圖片誤導(dǎo),
adjustedContentInset
屬性的值是包含contentInset
屬性的值的
事件處理相關(guān)
觸摸相關(guān)API
介紹
該屬性用于標(biāo)識用戶是否已經(jīng)觸摸了內(nèi)容區(qū)域并準(zhǔn)備進(jìn)行滑動
注: 該屬性值被設(shè)置為
YES
的時候用戶可能只是觸摸了內(nèi)容區(qū)域, 但是并沒有開始進(jìn)行滑動
@property(nonatomic,readonly,getter=isTracking) BOOL tracking;
該屬性用于標(biāo)識用戶是否已經(jīng)開始滑動內(nèi)容區(qū)域
注: 該屬性值被設(shè)置為
YES
之前用戶可能需要先滑動一段時間或距離
@property(nonatomic,readonly,getter=isDragging) BOOL dragging;
該屬性用于標(biāo)識是否正在處于減速狀態(tài)(即手指已經(jīng)離開屏幕, 但scrollView
仍然處于滑動中)
@property(nonatomic,readonly,getter=isDecelerating) BOOL decelerating;
該屬性用于標(biāo)識是否延遲內(nèi)容區(qū)域的事件傳遞, 默認(rèn)值為YES
. 如果設(shè)置為NO
, 則scrollView
會立即調(diào)用-touchesShouldBegin:withEvent:inContentView:
方法以進(jìn)行下一步操作
@property(nonatomic) BOOL delaysContentTouches;
當(dāng)已經(jīng)將事件傳遞給子視圖后是否可以取消, 默認(rèn)值為YES
. 如果設(shè)置為NO
, 則一旦開始跟蹤事件, 即使手指進(jìn)行移動也不會取消已經(jīng)傳遞給子視圖的事件
@property(nonatomic) BOOL canCancelContentTouches;
該方法用于在UIScrollView
的子類中重寫, 返回是否將事件傳遞給對應(yīng)的子視圖, 默認(rèn)返回YES
. 如果返回NO
, 則該事件不會傳遞給對應(yīng)的子視圖
- (BOOL)touchesShouldBegin:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event inContentView:(UIView *)view;
該方法用于在UIScrollView
的子類中重寫, 返回當(dāng)已經(jīng)將事件傳遞給子視圖后是否可以取消. 默認(rèn)當(dāng)子視圖是UIControl
時返回NO
, 即不再繼續(xù)跟蹤用戶的觸摸事件; 否則返回YES
, 即仍然繼續(xù)跟蹤用戶的觸摸事件
注: 該方法被調(diào)用的前提是
canCancelContentTouches = YES
- (BOOL)touchesShouldCancelInContentView:(UIView *)view;
其他相關(guān)API
介紹
該屬性用于配置隱藏鍵盤的模式, 該屬性可設(shè)置三個枚舉值:
-
UIScrollViewKeyboardDismissModeNone
: 默認(rèn)值, 不隱藏鍵盤 -
UIScrollViewKeyboardDismissModeOnDrag
: 當(dāng)拖拽時隱藏鍵盤 -
UIScrollViewKeyboardDismissModeInteractive
: 當(dāng)拖拽鍵盤上方時隱藏鍵盤, 如果反向拖拽鍵盤會取消隱藏
@property(nonatomic) UIScrollViewKeyboardDismissMode keyboardDismissMode NS_AVAILABLE_IOS(7_0);
該屬性用于標(biāo)識內(nèi)建的拖動手勢和捏合手勢, 可在此對其進(jìn)行配置
@property(nonatomic, readonly) UIPanGestureRecognizer *panGestureRecognizer NS_AVAILABLE_IOS(5_0);
@property(nonatomic, readonly) UIPinchGestureRecognizer *pinchGestureRecognizer NS_AVAILABLE_IOS(5_0);
該屬性用于標(biāo)識內(nèi)建的下拉刷新控件, 可在此實現(xiàn)下拉刷新功能
@property (nonatomic, strong, nullable) UIRefreshControl *refreshControl NS_AVAILABLE_IOS(10_0);
事件處理實現(xiàn)原理
由于scrollView
并沒有用于直接操控的滾動條, 因此用戶只能通過直接操作scrollView
的內(nèi)容區(qū)域以便進(jìn)行滾動操作. 但是當(dāng)用戶觸碰到屏幕上時, scrollView
并不清楚該用戶的目的是想要進(jìn)行滾動操作還是單純地想要點擊某一個視圖. 為了處理這種情況, 當(dāng)用戶觸碰屏幕時, scrollView
首先攔截到該觸摸事件并啟用一個150s
的定時器, 同時觀察用戶的下一步行為.
- 當(dāng)定時器結(jié)束前, 如果用戶的觸摸點發(fā)生足夠的移動, 則直接滾動內(nèi)容區(qū)域, 并且不會繼續(xù)將該觸摸事件傳遞給子視圖.
- 當(dāng)定時器結(jié)束后, 如果用戶的觸摸點并沒有發(fā)生足夠的移動, 則調(diào)用
-touchesShouldBegin:withEvent:inContentView:
方法詢問是否將事件傳遞給對應(yīng)的子視圖. 如果返回NO
, 則該事件不會傳遞給對應(yīng)的子視圖; 如果返回YES
, 則該事件會傳遞給對應(yīng)的子視圖, 默認(rèn)為YES
. - 當(dāng)觸摸事件被傳遞給子視圖后, 如果
canCancelContentTouches=YES
, 則會立即調(diào)用-touchesShouldCancelInContentView:
方法詢問是否可以取消已經(jīng)傳遞給子視圖的事件. 如果返回NO
, 則不再進(jìn)一步跟蹤用戶的觸摸事件; 如果返回YES
, 則當(dāng)用戶的觸摸點又發(fā)生足夠的移動時, 系統(tǒng)會向該子視圖發(fā)送-touchesCancelled:withEvent:
消息并進(jìn)行滑動.
代理相關(guān)
該方法在contentOffset
發(fā)生變化時調(diào)用
- (void)scrollViewDidScroll:(UIScrollView *)scrollView;
該方法在將要開始拖拽時調(diào)用
注: 該方法可能需要先滑動一段時間或距離才會被調(diào)用
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView;
該方法在用戶停止拖拽時調(diào)用
注: 應(yīng)用程序可以通過修改
targetContentOffset
參數(shù)的值來調(diào)整停止的位置
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset NS_AVAILABLE_IOS(5_0);
該方法在用戶停止拖拽時調(diào)用
注: 如果在停止拖拽后繼續(xù)移動, 則
decelerate
參數(shù)為YES
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;
該方法在將要開始減速時調(diào)用
注: 僅當(dāng)停止拖拽后繼續(xù)移動時才會被調(diào)用
- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView;
該方法在已經(jīng)結(jié)束減速時調(diào)用
注: 僅當(dāng)停止拖拽后繼續(xù)移動時才會被調(diào)用
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;
該方法用于返回是否允許點擊狀態(tài)欄讓scrollView
滑動到頂部, 默認(rèn)值為YES
注: 僅當(dāng)
scrollsToTop
屬性值為YES
時才調(diào)用
- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView;
該方法在scrollView
已經(jīng)滑動到頂部時調(diào)用
注: 僅當(dāng)通過點擊狀態(tài)欄讓
scrollView
滑動到頂部才調(diào)用
- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView;
該方法在-setContentOffset:animated:/-scrollRectVisible:animated:
方法動畫結(jié)束時調(diào)用
注: 僅當(dāng)
animated
設(shè)置為YES
時才調(diào)用
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView;
該方法在縮放比例發(fā)生變化時調(diào)用
- (void)scrollViewDidZoom:(UIScrollView *)scrollView NS_AVAILABLE_IOS(3_2);
該方法用于返回參與縮放的子視圖
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView;
該方法在將要開始縮放時調(diào)用
- (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view NS_AVAILABLE_IOS(3_2);
該方法在已經(jīng)結(jié)束縮放時調(diào)用
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale;
該方法在adjustedContentInset
發(fā)生變化時調(diào)用
- (void)scrollViewDidChangeAdjustedContentInset:(UIScrollView *)scrollView API_AVAILABLE(ios(11.0));