UIScrollView - 滚动视图
UIScrollView可以用于显示多余一个屏幕的内容,超出屏幕范围的内容可以通过滑动或缩放进行查看。UIScrollView是几个UIKit类的超类,包括UITableView和UITextView。
UIScrollView对象(或者简单地说,滚动视图)的中心概念是它是一个其起源可以在内容视图上调整的视图。它将内容剪切到frame中,frame通常(但不一定)与应用程序的主窗口一致。滚动视图跟踪手指的运动,并相应地调整原点(自身bounds的原点,不是frame)。显示其内容的视图通过滚动视图根据新原点绘制其自身的该部分内容,该原点固定在内容视图中的一个偏移量上。滚动视图本身除了显示垂直和水平滚动指示器外,不进行任何绘图。滚动视图必须知道内容视图的大小,以便知道何时停止滚动。默认情况下当滚动超过内容的边界时,它会有一个“回弹”效果。
滚动视图中管理显示内容的绘制对象需要平铺内容的子视图,这样视图就不会超过屏幕的大小。当用户在滚动视图中滚动时,此对象会根据需要添加和删除子视图。
因为滚动视图没有滚动条,所以滚动视图必须知道触摸是表示滚动的意图还是表示跟踪内容中的子视图的意图。为了做出这个判断,它通过启动一个定时器来临时拦截触事件,在定时器触发之前,观察触摸的手指是否有任何移动。如果计时器在没有显著位置变化的情况下触发,滚动视图将跟踪事件发送到内容视图的触摸子视图。如果用户在计时器执行之前将手指拖到足够远的地方,滚动视图将取消子视图中的任何跟踪并执行滚动自身。子类可以覆盖touchesShouldBegin:withEvent:inContentView:方法、pagingEnabled方法和touchesShouldCancelInContentView:方法(由滚动视图调用),以影响滚动视图处理滚动手势的方式。
滚动视图还处理内容的缩放和平移。当用户做出捏合或张开手势时,滚动视图会调整内容的偏移量和比例。当手势结束时,管理内容视图的对象应根据需要更新内容的子视图。(注意,这个手势可以结束,手指也可以放下。)在手势执行过程中,滚动视图不会向子视图发送任何跟踪调用。
UIScrollView类的代理对象必须采用UIScrollViewDelegate协议。为了实现缩放和平移,委托必须同时实现viewForZoomingInScrollView:和scrollViewDidEndZooming:withView:atScale:方法,此外最大缩放比例(maximumZoomScale)和最小缩放比例(minimumZoomScale)必须不同。
滚动视图的约束
将UIScrollView添加到故事板中,并设置UIScrollView相对于父视图的约束后,通常我们会得到如下错误:
因为UScrollView 的可滚动大小是根据其子视图的约束自动计算的。为了完全定义可滚动内容的宽度和高度,需要对可滚动内容相对于滚动视图的所有边缘(leading,trailing,top, bottom)进行约束。确保可以在滚动视图的子视图中从滚动视图的leading(或top)边缘开始,到trailing(或bottom)边缘跟踪一行连续连接的约束。
所以通常情况下,在IOS11.0及更高的版本中,可以根据contentLayoutGuide属性布局:
但为了兼容IOS11.0以下的版本,通常不使用contentLayoutGuide属性,而是添加一个视图充当UIScrollView的内容视图,然后在添加的内容视图上布局子视图:
重要的事情说两遍 : 如果滚动视图水平滚动,那么滚动视图的子视图的水平布局约束一定是连续的,以撑起水平方向可滚动内容的宽度;如果滚动视图垂直滚动,那么滚动视图的子视图的垂直布局约束一定是连续的,以撑起垂直方向可滚动内容的高度。
UIScrollView常用属性
@property(nonatomic) CGSize contentSize;
属性描述 :Scrollview中的可显示区域,可以设置ScrollView的滚动范围,假如有一个Scrollview,它的frame为(0,0,320,480),而它的contentSize为(320,960).也就是说,这个scrollview整个内容的大小为(320,960),要通过上下滑动Scrollview来查看(320,480)后的内容。
@property(nonatomic) CGPoint contentOffset;
属性描述 : Scrollview当前显示区域顶点相对于frame顶点的偏移量,可以设置ScrollView当前滚动的位置,假如有一个Scrollview,它的frame为(0,0,320,480),而它的contentSize为(320,960),也就是说你拉到最下面,contentOffset就是(0 ,480),也就是y偏移了480。
@property(nonatomic) UIEdgeInsets contentInset;
属性描述 :Scrollview中内容视图的原点与Scrollview本身原点的关系,这个属性可以在四周增加滚动范围,比如有一个Scrollview,它的frame为(0,0,320,480),内容视图的frame为(0,30,320,480),那么相对于Scrollview的contentInset则为(0, 30)。
@property(nonatomic) BOOL bounces;
属性描述 :一个布尔值,只是滚动到内容边界时是否反弹。如果此属性的值为“YES”,则滚动视图在遇到内容边界时会反弹。视觉上的反弹表示滚动已到达内容的边缘。如果值为“NO”,则滚动将立即在内容边界处停止,而不反弹。默认值为“YES”。
@property(nonatomic) BOOL alwaysBounceHorizontal;
属性描述 :一个布尔值,用于确定当水平滚动到达内容视图的结尾时是否始终发生反弹。如果此属性设置为YES并且bounces为YES,则即使内容小于滚动视图的边界,也允许水平拖动。默认值为NO。
@property(nonatomic) BOOL alwaysBounceVertical;
属性描述 :一个布尔值,用于确定垂直滚动到达内容结尾时是否始终发生反弹。如果此属性设置为YES并且bounces设置为YES,则即使内容小于滚动视图的边界,也允许垂直拖动。默认值为NO。
@property(nonatomic,getter=isScrollEnabled) BOOL scrollEnabled;
属性描述 :决定是否启用滚动的布尔值。如果此属性的值为YES,则启用滚动,如果为NO,则禁用滚动。默认是YES。当滚动被禁用时,滚动视图不接受触摸事件,并将事件沿响应链向上转发。
@property(nonatomic,getter=isPagingEnabled) BOOL pagingEnabled API_UNAVAILABLE(tvOS);
属性描述 :一个布尔值,用于确定是否为滚动视图启用分页。如果此属性的值为YES,则当用户滚动时,滚动视图将以滚动视图边界的倍数停止。默认值为NO。
@property(nonatomic) BOOL showsHorizontalScrollIndicator;
属性描述 :一个布尔值,用于控制水平滚动指示器是否可见。默认值是YES。跟踪正在进行的滚动时,指示器是可见的,跟踪滚动之后,指示器淡出。
@property(nonatomic) BOOL showsVerticalScrollIndicator;
属性描述 : 一个布尔值,用于控制垂直滚动指示器是否可见。默认值是YES。跟踪正在进行的滚动时,指示器是可见的,跟踪滚动之后,指示器淡出。
@property(nonatomic) UIScrollViewIndicatorStyle indicatorStyle;
属性描述 :滚动指示器的样式。默认的样式是UIScrollViewIndicatorStyleDefault。
@property(nonatomic,readonly,getter=isDragging) BOOL dragging;
属性描述 :一个布尔值,指示用户是否已开始滚动内容。此属性持有的值可能需要一些时间或滚动距离才能设置为YES。
@property(nonatomic,readonly,getter=isTracking) BOOL tracking;
属性描述 :返回用户是否已触摸内容以启动滚动。如果用户已经触摸了内容视图,但是可能还没有开始拖动它,那么这个属性的值是YES。
@property(nonatomic,readonly,getter=isDecelerating) BOOL decelerating;
属性描述 :是否正在减速,返回用户抬起手指后内容是否在滚动视图中移动。如果用户没有拖动内容,但仍在滚动,则返回值为YES。
@property(nonatomic,readonly,getter=isZooming) BOOL zooming;
属性描述 :一个布尔值,指示内容视图当前是放大还是缩小。如果用户正在进行缩放手势,则此属性的值为YES,否则为NO。
@property(nonatomic) CGFloat minimumZoomScale;
属性描述 :指定可应用于滚动视图内容的最小比例因子的浮点值。这个值决定了内容可以缩放的大小。默认值是1.0。
@property(nonatomic) CGFloat maximumZoomScale;
属性描述 :指定可应用于滚动视图内容的最大缩放因子的浮点值。这个值决定了内容可以缩放的大小。它必须大于最小缩放比例,以使用缩放功能。默认值是1.0。
@property(nonatomic,getter=isDirectionalLockEnabled) BOOL directionalLockEnabled;
属性描述 :确定是否在特定方向上禁用滚动,如果此属性为“YES”,并且用户开始沿一个常规方向(水平或垂直)拖动,则滚动视图将禁用沿另一个方向的滚动。如果拖动方向是对角线,则滚动将不会被锁定,用户可以在拖动完成之前向任何方向拖动。默认值为“NO”。
@property(nonatomic) BOOL delaysContentTouches;
属性描述 :一个布尔值,用于确定滚动视图是否延迟处理触控手势。如果这个属性的值是YES,滚动视图将延迟处理触摸手势,直到它能够确定目的是否是滚动。如果值为NO,滚动视图立即调用touchesShouldBegin:withEvent:inContentView:函数。默认值是YES。
@property(nonatomic) UIScrollViewDecelerationRate decelerationRate API_AVAILABLE(ios(3.0));
属性描述 :一个浮点值,用于确定用户抬起手指后的减速率。应用程序可以使用UIScrollViewDecelerationRateNormal和UIScrollViewDecelerationRateFast常量作为合理减速率的参考。
@property(nonatomic) BOOL scrollsToTop API_UNAVAILABLE(tvOS);
属性描述 :一个布尔值,用于控制是否启用滚动到顶部手势。滚动到顶部的手势是点击状态栏。当用户做这个动作时,系统会要求最靠近状态栏的滚动视图滚动到最上面。如果那个滚动视图将scrollsToTop设置为NO,那么它的委托将从scrollViewShouldScrollToTop:返回NO,或者内容已经在顶部,则什么也不会发生。
在滚动视图滚动到内容视图的顶部之后,它向委托发送一个scrollViewDidScrollToTop:消息。scrollsToTop的默认值是YES。
@property(nonatomic) UIScrollViewContentInsetAdjustmentBehavior contentInsetAdjustmentBehavior API_AVAILABLE(ios(11.0),tvos(11.0));
属性描述 :用于确定调整的内容偏移的行为。此属性指定如何使用安全区域插入来修改滚动视图的内容区域。此属性的默认值为UIScrollViewContentInsetAdjustmentAutomatic。
- UIScrollViewContentInsetAdjustmentBehavior提供的枚举值:
typedef NS_ENUM(NSInteger, UIScrollViewContentInsetAdjustmentBehavior) {
//自动调整滚动视图插图。当滚动视图是当前由导航或标签栏控制器显示的视图控制器的内容视图时,内容总是垂直调整。
//如果滚动视图是水平可滚动的,则当存在非零值安全区域插入时,也会调整水平内容偏移。
UIScrollViewContentInsetAdjustmentAutomatic,
//仅在可滚动方向调整插入。当垂直内容大小大于滚动视图本身的高度时,顶部和底部的插入会包括安全区域插入值。
//当alwaysBounceVertical属性为YES时,也会调整顶部和底部插入。
//类似地,当水平内容大小大于滚动视图的宽度时,左侧和右侧的插入会包括安全区域插入值。
UIScrollViewContentInsetAdjustmentScrollableAxes,
//不要调整滚动视图的插入。
UIScrollViewContentInsetAdjustmentNever,
//始终在内容调整中包含安全区域的插入。
UIScrollViewContentInsetAdjustmentAlways,
} API_AVAILABLE(ios(11.0),tvos(11.0));
通常滚动视图(包括其子类,如UICollectionView)因为该属性的默认值,会自动调整内容的插入以适应安全区域,如图:
如果不需要这种对内容插入的调整,可以设置该属性值为UIScrollViewContentInsetAdjustmentNever,如果设置该属性值未生效,则需要检查导航栏是否是透明的,如果导航栏不是透明的,则需要检查滚动视图(包括其子类,如UICollectionView)所在的视图控制器的extendedLayoutIncludesOpaqueBars属性是否设置为YES,导航栏透明的情况下,则不需要检查extendedLayoutIncludesOpaqueBars属性。设置完成后,如图:
@property(nullable,nonatomic,weak) id<UIScrollViewDelegate> delegate;
属性描述 :滚动视图对象的代理,代理必须采用UIScrollViewDelegate协议。
UIScrollViewDelegate提供的函数
- (void)scrollViewDidScroll:(UIScrollView *)scrollView;
函数描述 :contentOffset内容偏移量改变时调用,委托通常实现此方法以从scrollView获取内容偏移量的更改,并绘制内容视图的受影响部分。
参数 :
scrollView:发生滚动的滚动视图对象。
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView;
函数描述 : 开始滚动时调用(可能需要一些时间或距离才能调用),在较短距离内发生滚动的话代理可能无法接收此消息。
参数 :
scrollView:即将滚动内容视图的滚动视图对象。
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset API_AVAILABLE(ios(5.0));
函数描述 : 将要结束滚动时调用,应用程序可以更改targetContentOffset参数的值,以调整scrollview完成其滚动动画的位置。
参数 :
scrollView : 用户结束触摸的滚动视图对象。
velocity : 释放触摸时滚动视图的速度(以点为单位)。
targetContentOffset : 滚动操作减速到停止时的预期偏移量。
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;
函数描述 : 结束滚动时调用。
参数 :
scrollView:完成内容视图滚动的滚动视图对象。
decelerate:设为“YES”,如果在滚动操作期间的触摸手势之后,滚动移动将继续,但会减速。如果该值为“NO”,则触摸后滚动将立即停止。
- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView;
函数描述 : 开始减速滚动时调用,在滚动操作期间,当用户的手指离开触摸时,滚动视图调用此方法;之后,滚动视图将继续移动一小段距离。
参数 :
scrollView:正在减慢内容视图滚动速度的滚动视图对象。
- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView;
函数描述 : 如果要点击状态栏滚动到顶部,请返回“YES”。如果没有定义,则默认YES。要使滚动到顶部手势(点击状态栏)生效,UIScrollView的scrollsToTop属性必须设置为YES。
参数:
scrollView :请求此信息的滚动视图对象。
返回值:
“YES”允许滚动到内容的顶部,“NO”禁止。
- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView;
函数描述 : 通知代理滚动视图通过点击状态栏滚动到内容的顶部。滚动视图在滚动到内容顶部时发送此消息。如果内容的顶部已经显示,它可能会立即调用它。要使滚动到顶部手势(点击状态栏)生效,UIScrollView的scrollsToTop属性必须设置为YES。滑动到顶部不会执行。
参数 :
scrollView:执行滚动操作的滚动视图对象。
手势缩放
1.设置UIScrollView的id<UISCrollViewDelegate> delegate代理对象
2.设置minimumZoomScale :缩小的最小比例
3.设置maximumZoomScale :放大的最大比例
4.让代理对象实现viewForZoomingInScrollView:(UIScrollView *)scrollView方法,返回需要缩放的视图控件
与缩放相关的常用UIScrollViewDelegate函数
- (void)scrollViewDidZoom:(UIScrollView *)scrollView API_AVAILABLE(ios(3.2));
函数描述 : 正在缩放的时候调用。
参数 :
scrollView : 缩放因子已更改的滚动视图对象。
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(nullable UIView *)view atScale:(CGFloat)scale;
函数描述 : 缩放完毕的时候调用。
参数 :
scrollView : 显示内容视图的滚动视图对象。
view : 表示需要缩放的内容视图的那部分的视图对象。
scale : 用于缩放的比例因子;这个值必须在UIScrollView属性maximumZoomScale和minimumZoomScale所建立的限制之间。
分页效果
设置pagingEnabled = YES即可,UIScrollView会被分割成多个独立页面,用户的滚动体验则变成了页面翻转,一般会配合UIPageControl增强分页效果。
UIPageControl - 分页控件
一种显示一系列水平点的控件,每个点对应于应用程序文档或其他数据模型实体中的一页。当用户点击分页控件以移动到下一页或上一页时,该控件将发送UIControlEventValueChanged事件以供代理处理。然后代理可以计算currentPage属性以确定要显示的页面。分页控件在任何方向上都只前进一个页面。当前浏览的页面由一个白点表示。根据设备的不同,在裁剪之前,屏幕上会显示一定数量的点。
UIPageControl的常用属性
@property(nonatomic) NSInteger numberOfPages;
属性描述 :调用方显示的页数(以点表示)。属性的值是分页控件显示为点的页数。默认值为0。
@property(nonatomic) NSInteger currentPage;
属性描述 :当前页,由调用方显示为一个白点。属性值是一个整数,指定显示的当前页减去1,因此值为0(默认值)表示第一页,超出可能范围的值固定为0或numberOfPages减1。分页控件将当前页显示为白点。
@property(nonatomic) BOOL hidesForSinglePage;
属性描述 :一个布尔值,用于控制只有一个页面时控件是否隐藏。当只有一个页面时,为YES赋值以隐藏分页面控件,如果只有一个页面,则分配NO(默认值)来显示页面控件。
@property(nullable, nonatomic,strong) UIColor *pageIndicatorTintColor API_AVAILABLE(ios(6.0)) UI_APPEARANCE_SELECTOR;
属性描述 :用于页面指示器的颜色(即分页的圆点标记),页面指示点的默认颜色为半透明白色。页面指示点用于屏幕上不可见的所有页面。为此属性分配新值不会自动更改currentPageIndicatorTintColor属性中的颜色,因为这两个属性的值不会自动从另一个属性派生。两个属性都必须单独指定。类似地,没有alpha应用于此属性。建议(但不是必需)为此参数指定的颜色包含一些透明度,即alpha值应小于1.0。
@property(nullable, nonatomic,strong) UIColor *currentPageIndicatorTintColor API_AVAILABLE(ios(6.0)) UI_APPEARANCE_SELECTOR;
属性描述 :用于当前页指示器的颜色。默认颜色是当前页指示点的不透明白色。当前页面指示点用于指示当前可见的页面。为该属性指定新值不会自动更改PageIndicatorIntColor属性中的颜色,因为这两个属性的值不会自动从另一个属性派生。两个属性都必须单独指定。
练习代码
素材是这样的 :
代码示例 :
//引入ViewController头文件
#import "ViewController.h"
//声明为UIScrollView的代理类
@interface ViewController ()<UIScrollViewDelegate>
//分页视图控件
@property(nonatomic,strong) UIPageControl *pageControl;
//定时器
@property(nonatomic,strong) NSTimer *timer;
@end
@implementation ViewController
//视图生命周期的方法,加载视图时调用
- (void)viewDidLoad {
[super viewDidLoad];
//初始化UIScrollWiew大小为屏幕大小
UIScrollView *scrollView = [[UIScrollView alloc]initWithFrame:self.view.frame];
//设置滚动的范围
scrollView.contentSize = CGSizeMake(CGRectGetWidth(self.view.frame) * 8, CGRectGetHeight(self.view.frame));
//设置分页效果
scrollView.pagingEnabled = YES;
//水平滚动条隐藏
scrollView.showsHorizontalScrollIndicator = NO;
//声明图片名称
NSString *imageName = nil;
//声明图片
UIImage *image = nil;
for (int i = 0; i < 8; i++) {
//为图片名称赋值
imageName = [NSString stringWithFormat:@"0%d",i + 1];
//初始化图片
image= [UIImage imageNamed:imageName];
//初始化图片视图,x轴的坐标在原点随着每次循环增加一个屏幕的宽度,y轴始终为0,宽高为屏幕的宽高
UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(CGRectGetWidth(self.view.frame) * i, 0, CGRectGetWidth(self.view.frame), CGRectGetHeight(self.view.frame))];
//为图片视图设置图片
[imageView setImage:image];
//为每个图片视图设置标签
imageView.tag = 100 + I;
//将图片视图添加到滚动视图
[scrollView addSubview:imageView];
}
//viewWithTag的作用就是根据tag属性获取到对应的view、imageview、label等等。
//获取第一张图片视图
UIImageView *firstImageView = [scrollView viewWithTag:100];
//为滚动视图最后面加一个视图,它和第一个视图一样,到这里实际已经有了9张图片视图
UIImageView *lastImageView = [[UIImageView alloc]initWithFrame:CGRectMake(CGRectGetWidth(self.view.frame) * 8, 0, CGRectGetWidth(self.view.frame), CGRectGetHeight(self.view.frame))];
//将第一张图片视图的图片赋值给最后一张图片视图的图片属性
lastImageView.image = firstImageView.image;
//将最后一张图片视图添加到滚动视图上
[scrollView addSubview:lastImageView];
//将滚动视图添加到控制器视图
[self.view addSubview:scrollView];
//为滚动视图添加标签
scrollView.tag = 100;
//初始化分页视图控件,并设置好位置
self.pageControl = [[UIPageControl alloc]initWithFrame:CGRectMake(0, CGRectGetHeight(self.view.frame) - 50, CGRectGetWidth(self.view.frame), 50)];
//设置分为多少页
self.pageControl.numberOfPages = 8;
//设置当前所在页
self.pageControl.currentPage = 0;
//设置页面指示器的颜色(即分页的圆点标记)
self.pageControl.pageIndicatorTintColor = [UIColor redColor];
//设置当前所在页面指示器的颜色
self.pageControl.currentPageIndicatorTintColor = [UIColor greenColor];
//将分页视图控件添加到控制器视图
[self.view addSubview: self.pageControl];
//初始化定时器,scheduledTimerWithTimeInterval设定间隔时间1.0秒,target指定发送消息给哪个对象,selector指定要调用的方法名,userInfo可以给消息发送参数,repeats设定是否重复
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(circularDisplayView) userInfo:nil repeats:YES];
//将本类赋值给scrollView的代理属性
scrollView.delegate = self;
}
#pragma mark - 滚动视图的代理方法
//视图被拖拽时调用,在此方法中会暂停控制器,停止图片循环展示
-(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{
//setFireDate:设置定时器的启动时间
//[NSDate distantFuture]:遥远的未来
[self.timer setFireDate:[NSDate distantFuture]];
}
//视图静止时(没有被拖拽)时调用,在此方法中会开启定时器,让图片循环展示
-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
//延迟2秒在开启定时器
//[NSDate dateWithTimeInterval:2 sinceDate:[NSDate date]] 返回值为现在时刻开始在过1.5秒的时刻
[self.timer setFireDate:[NSDate dateWithTimeInterval:2 sinceDate:[NSDate date]]];
}
//定时器的回调方法,切换页面
-(void)circularDisplayView{
//得到scrollView
UIScrollView *scrollView = [self.view viewWithTag:100];
//通过改变contentOffset来切换滚动视图的子界面
CGFloat offset_x = scrollView.contentOffset.x;
//每次切换一个屏幕
offset_x +=CGRectGetWidth(self.view.frame);
//当额外添加的最后一张图片视图开始滚动时,将偏移量改为第一个图片视图的位置
if(offset_x > CGRectGetWidth(self.view.frame) * 8){
scrollView.contentOffset = CGPointMake(0, 0);
}
//当显示的是额外添加的最后一张图片视图时,将分页控件的当前分页指示器重新开始设置,否则正常显示
if(offset_x == CGRectGetWidth(self.view.frame) * 8){
self.pageControl.currentPage = 0;
}else{
self.pageControl.currentPage = offset_x / CGRectGetWidth(self.view.frame);
}
//设置最终偏移量
CGPoint finalPoint = CGPointMake(offset_x, 0);
// 切换视图时带动画效果
//当额外添加的最后一张图片视图开始滚动时,即相当于第一张图片开始滚动,直接将偏移量设置到第二张图片的位置
if (offset_x > CGRectGetWidth(self.view.frame) * 8) {
self.pageControl.currentPage = 1;
[scrollView setContentOffset:CGPointMake(CGRectGetWidth(self.view.frame), 0) animated:YES];
}else{
[scrollView setContentOffset:finalPoint animated:YES];
}
}
@end