概述
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));