记录寄己走过的路

iOS UI控件详解—「UIScrollView滚动视图」

引导


“ 本文不适合老司机… ”
Copyright © PBWln Unauthorized shall not be reproduced.

本文章将介绍 iOS UI控件 详解,最常使用的三大控件之一 UIScrollView,将会分成两篇文章完整的讲述 UIScrollView 的常用属性方法使用(包括优化方面)及注意点 和 实战开发使用场景案例,文章编写周期会长一些,用到那点实用的东西,会及时补充。

本篇文章主要从【 UIScrollView 属性方法注解】学习总结。
在「时间 & 知识 」有限内,总结的文章难免有「未全、不足 」的地方,还望各位好友指出,以提高文章质量。

目录:

  1. UIScrollView概念
  2. UIScrollView基本使用(滚动)
  3. UIScrollView属性与方法
    1.属性 & 方法
    2.使用格式 & 说明:
    3.简明注解:
  4. UIScrollView代理方法
    1.滑动代理方法相关
    2.缩放代理方法相关
    3.部分代理方法简明示例
  5. UIScrollView处理触摸事件原理
  6. 常见需求示例 & 原理解析
    1.导航栏半透明效果
    2.控件导航悬停
    3.下拉头部图片放大
    4.正反无限轮播(3张)重点
  7. UIScrollView底层实现
  8. 正反无限轮播(2张)
  9. UIScrollView.h属性和方法
  10. 期待 & 后续

UIScrollView概念


我还是喜欢先看官方的说明:

 SDKs iOS 2.0+

上图简明释义
可以展示比设备屏幕更大区域的内容,我们可以通过手指滑动来查看内容视图(content view)的每一部分内容,也可以通过手指捏合来对内容视图进行缩放操作。

UIScrollView 是一个非常重要的控件,UITableViewUICollectionViewUITextView等常用控件全部继承自UIScrollView,而UIWebView控件内部也是基于UIScrollView实现的。而UIScrollView继承自UIView,至于它可以滚动的原因我下面会有原理说明。

UIScrollView基本使用(滚动)


UIScrollView的使用很简单,基本使用方式如下 3点

1
2
3
4
5
6
7
8
9
10
// 1.创建滚动视图
UIScrollView *scrollView = [[UIScrollView alloc] init];
scrollView.frame = CGRectMake(0,0,375,150);
[self addSubview:scrollView];
// 2. 设置滚动范围(内容视图的大小)
scrollView.contentSize = CGSizeMake(ImageViewCount * self.bounds.size.width, 0);
// 3. 添加内容
[scrollView addSubview:[UIImageView new]];
  • 说明:如果想让UIScrollView进行滚动,必须设置可以滚动的范围

UIScrollView属性与方法


属性 & 方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
属性 & 方法:
#pragma mark ------------------
#pragma mark - 内容视图属性方法相关
/** 内容视图的原点相对于scrollView的原点的偏移量(上/左 方向偏移为正数,下/右 方向偏移为负数),默认为CGPointZero */
@property(nonatomic) CGPoint contentOffset; // default CGPointZero
/** 内容视图的大小,默认为CGSizeZero */
@property(nonatomic) CGSize contentSize; // default CGSizeZero
/** 为内容视图周围增加可滚动区域,默认为UIEdgeInsetsZero */
@property(nonatomic) UIEdgeInsets contentInset; // default UIEdgeInsetsZero. add additional scroll area around content
// 代理
@property(nullable,nonatomic,weak) id<UIScrollViewDelegate> delegate; // default nil. weak reference
/** 设置内容视图的原点相对于scrollView的原点的偏移量 */
- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated; // animate at constant velocity to new offset

使用格式 & 说明:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
使用格式 & 说明:
// 内容滚动大小(说明:设置的大小 > scrollView.bounds 才可以滚动,哪个方向不滚动可直接设置为0 如:width = 0 或是 height = 0)
self.scrollView.contentSize = CGSizeMake(3 * ScreenViewW, ScreenViewH);
// 内容偏移量(说明:内容视图的左上角到scrollView左上角的一个偏移量,计算时以内容视图的左上角为基准,默认(0,0)开始)
scrollView.contentOffset = CGPointMake(150, 0);
// 内边距(top +,left +,bottom -,right -)
scrollView.contentInset = UIEdgeInsetsMake(10, 10, 10, 10);
// 设置scorllView的代理,当滚动事件事件发生时,相应的代理方法被调用(说明:苹果设计的代理属性为什么是weak?注解在下面)
// 可以看到前面定义属性类型为 id,可知 任何OC对象都可以作为scrollView的代理,必须(1.遵守协议 2.实现协议方法)
scrollView.delegate = self;
// 方法:setContentOffset: 内容偏移量
// 说明:方法动画结束时调用 scrollViewDidEndScrollingAnimation:(仅当animated设置为YES时才调用)
[self.scrollView setContentOffset:offset animated:YES];
简明注解:

对于 UIScrollView 最重要的属性 contentSize contentOffset contentInset,要有正确的认知。

这个图很能说明,这三个属性的区别:
简明注解:UIScrollView


解答上面问题:苹果设计的代理属性为什么是weak?

上图注解:

  • 原因:防止造成循环引用

  • 说明:

    • rootViewController 指针指向控制器对象(每个控制器都有个View强引用–> UIView对象(强引用 内部 属性 subViews–> 子控件数组对象(强引用 假设scr对象在第0位)–> UIScrollView对象将ViewController设置为自己的代理(delegate),所以的用 弱引用 weak

    • 假设没有指针指向这个控制器,而delegate(weak弱引用) 没有强指针引用,控制器就会释放。

UIScrollView代理方法


滑动代理方法相关
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@protocol UIScrollViewDelegate<NSObject>
@optional (可选)
#pragma mark ------------------
#pragma mark - 滑动代理方法相关
/** 当scrollView的contentOffset发生变化时调用 */
- (void)scrollViewDidScroll:(UIScrollView *)scrollView; // any offset changes
// called on start of dragging (may require some time and or distance to move)
/** 用户即将开始拖拽scrollView是调用(注: 该方法可能需要先滑动一段时间或距离才会被调用) */
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView;
// called on finger up if the user dragged. velocity is in points/millisecond. targetContentOffset may be changed to adjust where the scroll view comes to rest
/** 用户即将停止拖拽时调用(注: 应用程序可以通过修改targetContentOffset参数的值来调整内容视图content view停止的位置) */
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset NS_AVAILABLE_IOS(5_0);
// called on finger up if the user dragged. decelerate is true if it will continue moving afterwards
/** 用户已经停止拖拽时调用(注: 如果内容视图content view在停止拖拽后但由于惯性会继续滚动,减速,则decelerate参数为YES) */
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;
/** 将要开始减速时调用(仅当停止拖拽后继续移动时才会被调用) */
- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView; // called on finger up as we are moving
/** 已经结束减速时调用(仅当停止拖拽后继续移动时才会被调用) */
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView; // called when scroll view grinds to a halt
/**
当-setContentOffset:animated:/-scrollRectVisible:animated:方法动画结束时调用(仅当animated设置为YES时才调用)
*/
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView; // called when setContentOffset/scrollRectVisible:animated: finishes. not called if not animating
/**
返回是否允许点击状态栏让scrollView滑动到顶部,如果未实现该方法,则默认为YES(仅当scrollsToTop属性为YES时才调用)
*/
- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView; // return a yes if you want to scroll to the top. if not defined, assumes YES
/**
当scrollView已经滑动到顶部时调用(仅当点击状态栏让scrollView滑动到顶部才调用)
*/
- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView; // called when scrolling animation finished. may be called immediately if already at top
缩放代理方法相关
1
2
3
4
5
6
7
8
9
10
11
12
13
#pragma mark ------------------
#pragma mark - 缩放代理方法相关
/** 当缩放比例更改时调用 */
- (void)scrollViewDidZoom:(UIScrollView *)scrollView NS_AVAILABLE_IOS(3_2); // any zoom scale changes
/** 参与缩放的子视图 */
- (nullable UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView; // return a view that will be scaled. if delegate returns nil, nothing happens
/** 将要开始缩放时调用 */
- (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(nullable UIView *)view NS_AVAILABLE_IOS(3_2); // called before the scroll view begins zooming its content
/** 已经结束缩放时调用 */
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(nullable UIView *)view atScale:(CGFloat)scale; // scale between minimum and maximum. called after any 'bounce' animations
@end
部分代理方法简明示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#pragma mark ------------------
#pragma mark - UIScrollViewDelegate
// 只要scrollView滚动就会调用该方法
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
//由于该方法在视图滚动中一直调用,所以不要在这里做耗时的计算
NSLog(@"视图正在滚动----");
}
// 用户即将开始拖动时,停止定时器
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
NSLog(@"滚动视图内容即将被拖动");
}
// 用户即将停止拖动时调用
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{
NSLog(@"滚动视图的内容即将停止拖动");
}
// 用户已经停止拖动时调用 (decelerate 指定是否有减速动作 Dragging 拖)
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
if (decelerate == NO) {
NSLog(@"用户已经停止拖动,停止滚动");
}else{
NSLog(@"用户已经停止拖动,但是由于惯性会减速滚动");
}
}
// 手动滚动 减速完毕会调用(停止滚动),开启定时器
// 只要设置了scrollView的分页显示,当手动(使用手指)滚动结束后,该代理方法会被调用
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
NSLog(@"减速结束,内容视图停止");
}
// 指定在scollView上哪一个视图被缩放
-(UIView*)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
//NSLog(@"346");
return [scrollView.subviews objectAtIndex:0];
}
// 只要scrollView缩放就会调用该方法
- (void)scrollViewDidZoom:(UIScrollView *)scrollView
{
//由于该方法在视图缩放中一直调用,所以不要在这里做耗时的计算
NSLog(@"视图正在缩放----");
}

UIScrollView处理触摸事件原理


当用户在UIScrollView的一个子视图上按下时,UIScrollView并不知道用户是想要滑动内容视图还是点击对应子视图,所以在按下的一瞬间,事件UIEventUIApplication传递到UIScrollView后,其会先将该事件拦截而不会立即传递给对应的子视图,同时开始一个150ms的倒计时,并监听用户接下来的行为

  • 当倒计时结束前,如果用户的手指发生了移动,则直接滚动内容视图,不会将该事件传递给对应的子视图;

  • 当倒计时结束时,如果用户的手指位置没有改变,则调用自身的-touchesShouldBegin:withEvent:inContentView:方法询问是否将事件传递给对应的子视图(如果返回NO,则该事件不会传递给对应的子视图,如果返回YES,则该事件会传递给对应的子视图,默认为YES)。

  • 当事件被传递给子视图后,如果手指位置又发生了移动,则调用自身的-touchesShouldCancelInContentView:方法询问是否取消已经传递给子视图的事件。

常见需求示例 & 原理解析


导航栏半透明效果

原理解析:

  • 默认情况下,在有UINavigationBar存在时,系统为了防止UIScrollView被遮挡,其contentInsetscrollIndicatorInsets属性都会被设置为UIEdgeInsetsMake(64, 0, 0, 0);在有UITabBar存在时,系统为了防止UIScrollView被遮挡,其contentInsetscrollIndicatorInsets属性都会被设置为UIEdgeInsetsMake(0, 0, 49, 0)

  • 因此,为了使用此种半透明效果,可以直接将UIScrollViewframe设置为整个屏幕的大小。

  • 注1: 系统只在UIScrollView是控制器视图的第0个子视图时才会自动修改contentInsetscrollIndicatorInsets属性。

  • 注2: 如果不想让系统自动修改contentInsetscrollIndicatorInsets属性,可以设置self.automaticallyAdjustsScrollViewInsets = NO;


控件导航悬停

原理解析:

  • 通过-scrollViewDidScroll:代理方法跟踪contentOffset的的变化,当不满足悬停条件时,待悬停控件属于UIScrollView的子视图,当满足悬停条件时,待悬停控件属于UIScrollView的父视图的子视图。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (scrollView.contentOffset.y >= 100)
{
CGRect rect = label.frame;
rect.origin.y = 0;
label.frame = rect;
[self.view addSubview:label];
}
else
{
CGRect rect = label.frame;
rect.origin.y = 100;
label.frame = rect;
[scrollView addSubview:label];
}
}

下拉头部图片放大

原理解析:

  • 通过-scrollViewDidScroll:代理方法跟踪contentOffset的的变化,根据contentOffset动态设置图片的缩放比例。
1
2
3
4
5
6
7
8
// 以"动态修改图片缩放比例于1倍和2倍之间"为例
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGFloat scale = 1 - (scrollView.contentOffset.y / 100);
scale = (scale >= 1) ? scale : 1;
scale = (scale <= 2) ? scale : 2;
imageView.transform = CGAffineTransformMakeScale(scale, scale);
}

正反无限轮播(3张)
原理解析:

正反无限轮播

  • 手动滚动:只创建三张图片: leftImageViewcenterImageViewrightImageView,定义中间显示页码为 centerPage ,设置初始self.centerPage = 0;,而左右图片页码都以 centerPage 表示,通过 scrollViewDidEndDecelerating: 方法判断contentOffsest.xscrollView.frame.size.width 比较,使改变 self.centerPage的值,然后对应页码滚动取数组中的图片,始终显示中间一张
  • 定时器滚动: 将定时器添加到 RunloopNSRunLoopCommonModes下,触发方法中设置setContentOffset: animated:YES 方法动画结束时调用 scrollViewDidEndScrollingAnimation:(仅当animated设置为YES时才调用),这个方法下 self.centerPage++;OK了

监听图片点击

当图片被点击的时候,我们往往需要执行某些操作,因此需要监听图片的点击,思路如下:



核心代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#pragma mark - 添加三张图片
- (void)createContentViews
{
// 把3张imageView对象添加到scrollView上
CGRect frame = self.bounds;
self.leftImageView = [[UIImageView alloc] initWithFrame:frame];
frame.origin.x += ScreenViewW;
self.centerImageView = [[UIImageView alloc] initWithFrame:frame];
frame.origin.x += ScreenViewW;
self.rightImageView = [[UIImageView alloc] initWithFrame:frame];
[self.scrollView addSubview:self.leftImageView];
[self.scrollView addSubview:self.centerImageView];
[self.scrollView addSubview:self.rightImageView];
}
#pragma mark - set方法赋值
- (void)setCenterPage:(NSInteger)centerPage {
_centerPage = centerPage;
if (_centerPage < 0) { // 向右滑动显示左面图片,值与0比较
_centerPage = self.imageArray.count - 1;
}
if (_centerPage > self.imageArray.count - 1) {
_centerPage = 0; // 向左滑动显示右面图片,值与count-1 比较
}
// Page: left和right用center表示
NSInteger leftPage = _centerPage - 1 < 0 ? self.imageArray.count -1 : _centerPage - 1;
NSInteger rightPage = _centerPage +1 > self.imageArray.count - 1 ? 0 : _centerPage + 1;
// 赋值
self.leftImageView.image = self.imageArray[leftPage];
self.centerImageView.image = self.imageArray[_centerPage];
self.rightImageView.image = self.imageArray[rightPage];
// 显示中间那一页(注:这里不要使用动画)
[self.scrollView setContentOffset:CGPointMake(self.scrollView.frame.size.width, 0)];
// 设置pageControl的页码
self.pageControl.currentPage = _centerPage;
}
#pragma mark - UIScrollViewDelegate
// 手动滚动 减速完毕会调用(停止滚动),开启定时器
// 只要设置了scrollView的分页显示,当手动(使用手指)滚动结束后,该代理方法会被调用
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
// 判断contentOffsest.x
if (scrollView.contentOffset.x > scrollView.frame.size.width) { // 下一张
self.centerPage++;
} else if (scrollView.contentOffset.x < scrollView.frame.size.width){ // 上一张
self.centerPage--;
}
[self startTimer];
NSLog(@"手动减速完毕,开启定时器 当前页 %ld",self.centerPage);
}

UIScrollView底层实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
NS_CLASS_AVAILABLE_IOS(2_0) @interface UIScrollView : UIView <NSCoding>
#pragma mark ------------------
#pragma mark - 内置手势识别器
// Change `panGestureRecognizer.allowedTouchTypes` to limit scrolling to a particular set of touch types.
/** 拖动手势 */
@property(nonatomic, readonly) UIPanGestureRecognizer *panGestureRecognizer NS_AVAILABLE_IOS(5_0);
// `pinchGestureRecognizer` will return nil when zooming is disabled.
/** 捏合手势 */
@property(nullable, nonatomic, readonly) UIPinchGestureRecognizer *pinchGestureRecognizer NS_AVAILABLE_IOS(5_0);
// `directionalPressGestureRecognizer` is disabled by default, but can be enabled to perform scrolling in response to up / down / left / right arrow button presses directly, instead of scrolling indirectly in response to focus updates.
/** 定向手势识别器 */
@property(nonatomic, readonly) UIGestureRecognizer *directionalPressGestureRecognizer UIKIT_AVAILABLE_TVOS_ONLY(9_0);

总结:
UIScrollView 继承自UIView内部加了手势,之所以可以滑动,是改变了bounds,若手指向上滑动,y++,内容就会向上滑动。这样我们也可以用 给UIView 添加个 Pan手势 实现滚动效果。

正反无限轮播(2张)

大致原理:
最底层是一个UIView,上面有一个UIScrollView以及UIPageControlscrollView上有两个UIImageViewimageView宽高 = scrollview宽高 = view宽高;

假设轮播控件的宽度为x高度为y,我们设置scrollviewcontentSize.width3x,并让scrollview的水平偏移量为x,既显示最中间内容。

具体细节请移步这里阅读:
轮播两个ImageView实现 http://www.jianshu.com/p/ef03ec7f23b2

参考:iOS控件详解之UIScrollView

正反无限轮播 效果图


UIScrollView 正反无限轮播详解

期待


  • 如果在阅读过程中遇到 error || new ideas,希望你能 messages 我,我会及时改正谢谢。
  • 点击右上角的 喜欢 和 订阅Rss 按钮,可以收藏本仓库,并在 Demo 更新时收到邮件通知。
❄︎ 本文结束    感谢简阅 ^_^. ❄︎

本文标题:iOS UI控件详解—「UIScrollView滚动视图」

文章作者:寄己的路

原始链接:https://sunyonghui.github.io/iOSUI/UIScrollView.html

版权声明: 署名-非商业性使用-禁止演绎 4.0 国际 本博客所有文章除特别声明外均为原创,转载务必请「注明出处链接(可点击) - 作者」,并通过E-mail等方式告知,谢谢合作!