UIScrollView的详细使用介绍和实现原理分析[2018.06.20更新]

UIScrollView的详细使用介绍和实现原理分析[2018.06.20更新]_第1张图片
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}}
UIScrollView的详细使用介绍和实现原理分析[2018.06.20更新]_第2张图片
父视图坐标系统
// 代码片段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}}
UIScrollView的详细使用介绍和实现原理分析[2018.06.20更新]_第3张图片
自身坐标系统

通过以上两个代码片段可以看出, 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的内容区域超出安全区域以外的情况, 这是一种对原有优化方式的升级, 避免了原有的一刀切的优化方式.

UIScrollView的详细使用介绍和实现原理分析[2018.06.20更新]_第4张图片
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 *)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));

你可能感兴趣的:(UIScrollView的详细使用介绍和实现原理分析[2018.06.20更新])