IOS开发(10)–滚动
前言:
有时候,我们需要呈现的内容超出了屏幕所能容纳的大小。例如,当我们把在汽车编辑页面中,将屏幕横向,然后随便点击一个输入框,跳出的键盘会挡住一部分文本编辑框。我们想要的是能覆盖更大内容的虚拟窗口——用户可以操作这个窗口,使之显示想要看到的某一部分内容。
UIScrollView能帮助我们实现所有的这些,甚至更多。
在本节中,我们将学习并实践这种多功能视图的威力
为View/Edit场景添加滚动视图
小屏幕使智能手机便携(然而吐槽一下现在越做越大的手机…),但它们也限制了可以显示的信息量。有时候图片太大了,一个屏幕放不下。更常见的例子就是我们用手机看小说,一个屏幕肯定不能放下整部小说。
在iOS中,解决的方法是四处滚动。UIScrollView是更大内容至上的一个虚拟窗口,提供了所有用户所需要的支持:四处拖动内容,放大和缩小,以及应用程序可能需要的其他功能。
用户在滚动视图中拖动或轻扫手指,从而在固定的“窗口”之下四处滑动内容。
设置滚动视图的最简单的方法就是,将所有我们想滚动的内容放到一个视图中。这个新视图变为滚动视图的内容视图。对于Add/View场景,我们需要一个包含添加汽车区域、分隔符视图以及查看汽车区域的UIView视图。特别指出,UIViewController的视图就是内容视图。但是我们不能仅仅拖拽一个滚动视图并使之成为View/Add场景控制器的根视图。
这样就剩下两个选择。其一,可以添加一个滚动视图来作为根视图的子视图,然后再给滚动视图添加一个UIView子视图,并将所有的内容区域(添加汽车区域、分隔符视图、查看汽车区域)放进这个UIView姿势图。非常遗憾的是,我们需要重新设置布局、约束以及属性关联。这样做的话工作量巨大,而且很容易能引入新的BUG(显然我们不会这么做的)
其二,使用与将添加汽车视图控制器嵌入到导航控制器中时相同的基本技术。但是这一次,我们要使用Editor|Embed In|Scroll View。遗憾的是,并不像选择当前顶层视图并选择这条命令一样简单。
这是因为那个视图是添加汽车视图控制器的可视部分。使用嵌入式命令之前,必须创建一个新的顶层视图。这样就需要打破控制器和视图之间的连接。
确保使用的是CH06 CarValet Starter项目
- 将CarValet的根视图名称改为Content View并拖到下图位置
- 将一个UIView拖到原先的内容视图所在的位置
- 将Content View拖到下图位置
- 添加滚动
- 现在的根视图样子
接下来是解决约束的问题
- 首先选择新的滚动视图,选中Scroll View,如下约束
- 选中Content View,如下约束
- 选中Content View,如下约束
- 修改添加汽车视图约束
修改查看汽车分组视图
运行结果如下图:
- 首先选择新的滚动视图,选中Scroll View,如下约束
运行之后,我们会发现有一些东西不太正确,Total Cars标签离导航栏太远了。虽然这是似乎看起来是错误的行为,但实际上却是正确的
视图控制器有个automaticallyAdjustsScrollViewInsets属性,它决定滚动视图是否会为导航栏、工具栏和其他类似元素嵌入或收缩内容区域。默认值是YES,这意味着滚动视图调整了内容视图的高度,所以其顶部正好在导航栏的正下方。
在IB画布上,从内容视图顶部到添加汽车分组顶部的约束延伸到工具栏下。当内容视图的顶部被移到工具栏下方时,添加汽车分组到内容视图底部的距离仍为72点。这意味着它距离导航栏下方72点,这显然不是我们所想要的。
有两种方法来解决这个问题。第一种方法是将72点换成标准距离。这种方法唯一的问题是,在IB画布上,添加汽车分组的顶部被移动到了导航栏下。如果在模拟器中运行,这可以工作,但却难以维护。第二种方法更容易一些,将下面的代码添加到ViewController.m文件中的viewDidLoad方法中,放到超类的下面
self.automaticallyAdjustsScrollViewInsets = NO;
现在有了带滚动视图但没有弹跳功能的Add/View场景。可以通过选择滚动视图,打开Attributes检查器并选中Bounce Vertical复选框来添加弹跳功能。
在模拟器中运行该应用程序并使主视图弹跳。当内容视图弹跳时,下方的区域是白色的,因为滚动视图使用默认颜色。如果想显示内容视图和空白区域之间的不同,请将滚动视图的背景改成其他颜色。下图是改成灰色的样子。
弹跳效果的存在是为了向用户提供已经到达可滚动区域边缘的反馈。到目前为止,内容视图和滚动视图的大小相同,所以任何滚动尝试都会导致弹跳。现在,是时候移到编辑界面并添加对内容的滚动支持了。在当前这种情况下,当键盘出现时,将文本框滚动到可查看的区域。
处理键盘
我们还没有完成编辑场景在所有屏幕尺寸和方向的任务。问题是,键盘有时候会盖住输入至少一行,并且在键盘打开时,没有什么布局可以在所有方向和屏幕尺寸上都能够工作。解决的办法是将包含输入框的视图放在UIScrollView的里面。当屏幕足够大时,也就是说,当滚动视图的大小足以显示所有的内容时,它就只是弹跳滚动条(或者不弹)。用户不能将内容移到屏幕外。但是,当键盘打开时,我们将调整滚动视图的大小,使之处于键盘上方。内容视图保持同样的大小,因此,用户可以额在输入行中滚动。
添加滚动视图
在编辑视图中,我们首先想到的是,简单的将上图框住部分嵌入到滚动视图中,就好比添加视图的那个方法。然而在当前这种情况下,嵌入并不是我们想要的做法,原因是自动布局。
当内容视图与滚动视图具有相同大小的时候,可以创建一组规定内容视图与滚动视图之间关系的完整约束集合。当滚动视图中的内容大小与滚动视图的大小不同时,这不起作用。
请记住,滚动视图通过将原点相对于内容进行偏移来实现其功能,它是一个较大视图之上的窗口。将内容针对滚动视图制定约束,意味着它与滚动视图具有固定的关系——没有滚动。
相反,我们需要内容视图的约束独立于滚动视图,这样内容视图层次就存在于它自己的世界里。这主要可以通过两种方法来实现。IB的像素完美布局会起作用,如果内容视图的大小不会变化的话。这方面的一个示例是图像,在接下来的内容中会看到的。如果内容的大小可以变化,那么需要某种方式来更新内容视图和滚动视图容器的contenSize属性。对于表单视图,即使高度是静态的,宽度也可以在设备旋转时修改。
最简单的方法是使用代码来添加和管理内容视图和contenSize滚动视图属性。在完成添加滚动视图的任务之后,我们不会再在画布上看到表单视图,虽然对于视图控制器可用。按照下面步骤添加滚动视图:
先创键一个视图命名为Form View,将除了Car Number Label全扔进去,布局啥的自行解决咯,怎么好看怎么来
将Form View拖到First Responder下方
- 将一个滚动视图拖到场景中,使视图的顶部位于Car Number标签的下方。
- 对滚动视图进行约束(注意是距离Car Number标签15点)
最后将打开Assistant编译器并显示.h文件,创建两个属性:scrollView是滚动视图的引用,fromView是表单视图的引用(就是FormView)
IB栏中应该是这个样子的:
现在,我们有一个滚动视图了,我们还需要做的是,将表单视图扔到滚动视图里面并且管理他的大小。当更改表单视图的大小时,我们还需要更新滚动视图的contenSize属性。
不同于自动布局和约束,我们需要设置表单视图的边框,并让系统来做其余的事情。我们好需要知道“约束”
- 框架视图的源点起始于x和y坐标都为0的位置
- 框架视图的高度固定为200点,足以包含所有的标签/输入框
框架视图与滚动视图(其父视图)一样宽
现在的问题是在哪儿里放置将框架视图添加到滚动视图中、设置框架视图的大小和滚动视图的contenSize属性的代码。
我们回想一下编辑汽车场景时如何展现的:打开这一场景的唯一方法是点击编辑按钮。这意味着汽车界面每次都是新创建的。我们没办法返回到相同的编辑界面。所以说是一加载就要创建,也就是在vieDidLoad方法中。
修改代码如下:(这里我就贴之前的代码了,后面的也是如此)
- (void)viewDidLoad {
[super viewDidLoad];
defaultScrollViewHeightConstraint = self.scrollviewHeightConstraints.constant;
self.formView.translatesAutoresizingMaskIntoConstraints = YES;//1 该框架视图不具有任何相对于父视图的约束。让系统使用其当前边框创建约束
[self.scrollView addSubview:self.formView];//2 将表单视图添加到滚动视图
self.formView.frame = CGRectMake(0.0, 0.0, self.scrollView.frame.size.width, self.scrollView.frame.size.height);//3确保表单视图和滚动视图一样宽
self.scrollView.contentSize = self.formView.bounds.size;//4根据需要设置滚动视图的contenSize属性
...
}
调整键盘大小
滚动视图添加完了,是时候处理一下键盘了。相信在很多程序里面见到过,当我们点击输入框,键盘跳出来的时候,输入框自动就网上调,以免被键盘围住,我们现在要做的就是这个。
我们要添加代码去监听键盘的打开与关闭,需要三个步骤:
- 添加响应键盘开闭事件的方法
- 当视图打开时,为键盘开闭通知注册处理方法
- 当视图关闭时,为键盘开闭通知注销处理方法
调整滚动视图的大小
调整滚动视图的大小意味着修改一个或多个约束,正如我们在之前自动布局中的添加横向布局自动布局2 。那么我们要改什么呢?滚动视图有4个约束,到前后边缘的约束和到Car Number标签的约束不需要我们改,我们要改的只有Height。当键盘出现并覆盖滚动视图一部分的时候,我们要减少滚动视图的高度,也就是减去滚动视图与键盘重叠的地方。当键盘消失的时候,我们要恢复滚动视图之前的高度,所以我们还需要一个用于保存原高度约束的实例变量,同样需要一个队高度约束的引用。
创建高度约束并命名为
scrollviewHeightConstraints
在CarEditViewController.m中,更改@implementation
@implementation CarEditViewController{
CGFloat defaultScrollViewHeightConstraint;
}
- 在viewDidLoad中,在调用父类后初始化默认值:
defaultScrollViewHeightConstraint = self.scrollviewHeightConstraints.constant;
查找重叠
添加键盘开闭的方法:
- (void)keyboardDidShow:(NSNotification *)notification{
NSDictionary *userInfo = [notification userInfo];//1获取与通知相关联的信息字典
NSValue *aValue = userInfo[UIKeyboardFrameEndUserInfoKey];//2查找显示的键盘的最终视图边框
CGRect keyboardRect = [aValue CGRectValue];//3将最终视图边框的值转为CGRect,并将坐标空间从设备主窗口转换为查看汽车分组使用的坐标系,也就是说,转换到编辑场景视图控制器的根视图的坐标系
keyboardRect = [self.view convertRect:keyboardRect fromView:nil];
CGRect intersect = CGRectIntersection(self.scrollView.frame, keyboardRect);//4找到由滚动视图和键盘的交集定义的矩形。如果滚动视图和键盘不重叠,矩形是全0
self.scrollviewHeightConstraints.constant -= intersect.size.height;//5降低滚动视图的高度,者通过减少垂直的重叠量来完成,也就是相交的矩形的高度。
[self.view updateConstraints];//6因为滚动视图的高度常量可能已经发生改变,更新约束
}
- (void)keyboardWillHide:(NSNotification *)notification{//7当键盘关闭时,将高度约束设置为默认值,并更新约束
printf("1:%f/n",defaultScrollViewHeightConstraint);
self.scrollviewHeightConstraints.constant = defaultScrollViewHeightConstraint;
[self.view updateConstraints];
}
现在完成了后两部,剩下的就是在viewDidLoad:的下面添加viewDidAppear来未见怕开关通知进行注册:
- (void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
[[NSNotificationCenter defaultCenter]addObserver:self
selector:@selector(keyboardDidShow:)
name:UIKeyboardDidShowNotification
object:nil];
[[NSNotificationCenter defaultCenter]addObserver:self
selector:@selector(keyboardWillHide:)
name:UIKeyboardWillHideNotification
object:nil];
}
最后,删除作为键盘事件观察者而注册的视图控制器,添加下列代码:
- (void)viewWillDisappear:(BOOL)animated{
[super viewWillDisappear:animated];
[[NSNotificationCenter defaultCenter]removeObserver:self
name:UIKeyboardDidShowNotification
object:nil];
[[NSNotificationCenter defaultCenter]removeObserver:self
name:UIKeyboardWillHideNotification
object:nil];
...
}
添加调整大小
当屏幕旋转时,滚动视图和表单视图都需要调整大小。然后contenSize属性需要被更新。
在keyboardDidShow:
和keyboardWillHide:
方法最后添加代码self.scrollView.contentSize = self.formView.frame.size;
好了,到此为止,我们把键盘添加完成咯。
贴出效果图
在内容中滚动
下面将介绍一些UIScrollView中的高级特性
我们要添加一个新的场景,对一组可缩放的汽车图像进行分页。分页是指在内容单位间滚动,而不是流畅地在内容中滚动。在当前这种情况下,每个向左或向右手势移动一辆车的距离。我们需要以下操作:
- 创建一个新的视图控制器类,用于查看汽车图像
- 添加一个新的带有滚动视图的场景,用于新建的类
- 用汽车图片填充滚动视图
- 设置滚动视图,使之可在图像之间反野
- 增加方法和缩小汽车图像的能力
- 恩…创建一个叫做Car文件夹,自己找一些图片扔进去,然后把Car添加到项目中的如下位置
- 创建一个新的视图控制器(cocoa touch class 避免忘掉)名:CarImageViewController,并把它移到Supporting Files组上面
- 在故事面板中,添加一个新的视图控制器,将类设置为CarImageViewController(下图是完整版)
选择Add/View场景添加一个按钮Car Images,并把它链接到CarImageViewController(push segue),标题命名为Car Images
添加Car Number标签(从编辑场景复制就行了),约束为到顶部15点,前边缘0点,记得更新边框(就是Update Frame)
添加滚动视图,约束到Car Number标签15点,前后底部0点,更新边框
将滚动视图和Car Number标签引用拉到CarImageViewController.h中,滚动视图命名为scrollView,Car Number标签为carNumberLabel
最后故事面板图如下:
好了,可以正式开始了
添加滚动视图
在CarImageViewController.m中
- (void)setupScrollContent{
NSMutableArray *imageViews = [NSMutableArray new];//2
CGFloat atX = 0.0;
CGFloat maxHeight = 0.0;
UIImage *carImage;
UIImageView *atImageView;
for (NSString *atCarImageName in carImageNames) {//3
carImage = [UIImage imageNamed:atCarImageName];
atImageView = [[UIImageView alloc] initWithImage:carImage];
atImageView.frame = CGRectMake(atX, 0.0, atImageView.bounds.size.width, atImageView.bounds.size.height);//4
[imageViews addObject:atImageView];
atX += atImageView.bounds.size.width;//5
if (atImageView.bounds.size.height > maxHeight) {//6
maxHeight = atImageView.bounds.size.height;
}
}
UIView *carImageContainerView = [[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, atX, maxHeight)];//7
for (UIImageView *atImageView in imageViews) {//8
[carImageContainerView addSubview:atImageView];
}
[self.scrollView addSubview:carImageContainerView];//9
self.scrollView.contentSize = carImageContainerView.bounds.size;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.resetZoomButton.enabled = NO;
carImageNames = @[ @"Car/1.jpg",@"Car/2.jpg",@"Car/3.jpeg",@"Car/4.jpg",@"Car/5.jpg"];//10
[self setupScrollContent];
// Do any additional setup after loading the view.
}
注释:
- 为车名数组创建私有的实例变量。如果数组的内容可以修改,那么可以使用NSMutableArray
- 创建可变数组用于添加图像视图。
- 遍历每个图像文件名,创建UIImage并放在图像视图中
- 更改图像视图边框的起始x位置,使它挨着上一幅图像。将第一幅图像的源点的x左边设置为0,然后在5中将上一幅图像的宽度添加到原点
- 将当前图像的宽度添加到起始位置,得到下一幅图像的起始位置(如果这是最后一幅图像,那么得到该视图的最终宽度)
- 将内容视图的最大宽度设置为最高图像的高度
- 分配具有所有图像宽度和最高图像高度的容器视图
- 将每幅图像添加到新的容器视图中
- 将容器视图添加为滚动视图的子视图,并设置其内容大小
初始化汽车图像文件名的静态数组
运行结果如下图:(他是连续的,不是分页的这里)
它们具有不同的宽度和高度。更好的体验是让所有的图像具有相同的宽度。在调整图像尺寸之后,可以使用分页在图像之间移动。
用下面代码更换setupScrollContent
- (void)setupScrollContent{
if (carImageContainerView != nil) {
[carImageContainerView removeFromSuperview];
}
CGFloat scrollWidth = self.view.bounds.size.width;//1
CGFloat totalWidth = scrollWidth * [carImageNames count];//2
UIView *carImageContainerView = [[UIView alloc]initWithFrame:CGRectMake(0.0, 0.0, totalWidth, self.scrollView.frame.size.height)];
CGFloat atX = 0.0;
CGFloat maxHeight = 0.0;
UIImage *carImage;
for (NSString *atCarImageName in carImageNames) {
carImage = [UIImage imageNamed:atCarImageName];
CGFloat scale = scrollWidth / carImage.size.width;//3
UIImageView *atImageView = [[UIImageView alloc]initWithImage:carImage];
CGFloat newHeight = atImageView.bounds.size.height;//4
atImageView.frame = CGRectMake(atX, 0.0, scrollWidth, newHeight);
if (newHeight > maxHeight) {
maxHeight = newHeight;
}
atX += scrollWidth;
[carImageContainerView addSubview:atImageView];
}
CGRect newFrame = carImageContainerView.frame;
newFrame.size.height = maxHeight;
carImageContainerView.frame = newFrame;
[self.scrollView addSubview:carImageContainerView];
self.scrollView.contentSize = carImageContainerView.bounds.size;
}
注释:
- 获得滚动视图的宽度,用于设置各图像的宽度
- 为容器视图计算总宽度
- 计算使当前图像与滚动视图宽度相等的比例因子
- 基于新的高度缩放图像视图。同时检查最大高度值,用于设置内容视图
运行结果如下图:
滚动的时候就会发现分页的不同。
添加缩放
为了允许UIScrollView进行缩放,需要进行4处主要修改:
- 在汽车图像视图控制器中采用UIScrollViewDelegate协议
- 实现viewForZoomingInScrollView:协议方法
- 将滚动视图委托连接到视图控制器
- 指定大于1.0的最大缩放级别和/或小于1.0的最小缩放级别
- 在在汽车图像视图控制器中采用UIScrollViewDelegate协议,在CarImageViewController.h中修改:
@interface CarImageViewController : UIViewController
<UIScrollViewDelegate>
- 为缩放的视图创建一个私有变量,在当前这种情况下,即汽车图像容器视图。在CarImageViewController.m中
@implementation CarImageViewController{
NSArray *carImageNames;
UIView *carImageContainerView;
}
- 在setupScrollContent中为容器视图创建新的实例变量
- (void)setupScrollContent{
CGFloat scrollWidth = self.view.bounds.size.width;//1
CGFloat totalWidth = scrollWidth * [carImageNames count];//2
carImageContainerView = [[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, totalWidth, self.scrollView.frame.size.height)];
// UIView *carImageContainerView = [[UIView alloc]initWithFrame:CGRectMake(0.0, 0.0, totalWidth, self.scrollView.frame.size.height)];
...
}
- 在vieDidLoad的下方添加
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
return carImageContainerView;
}
这是用于返回要缩放视图的滚动视图委托协议方法。在当前这种情况下,只有一个可缩放的视图——它包含所有的图像视图。
上面的步骤处理了增加缩放视图的前两部分工作。现在,我们需要连接滚动视图委托。
打开故事面板
设置缩放倍数
ok,缩放完成,在模拟器上,按住option模拟两个手指头,效果如下:
现在来添加一个按钮,用来一键恢复图片圆形,按钮如下面的样子,注意一开始没有缩放的时候,按钮是禁止状态。
然后为这个按钮创建一个引用和一个action,引用的名称是resetZoomButton,action的名称resetZoom
在action中作如下修改:
- (IBAction)resetZoom:(id)sender {
[self.scrollView setZoomScale:1.0 animated:YES];
}
对了,可能有的会发现一开始按钮是可以按的,为了以防万一,在viewDidLoad中添加:
- (void)viewDidLoad {
[super viewDidLoad];
self.resetZoomButton.enabled = NO;
...
}
现在差不多完成啦,但是但我们在查看图片的时候,将屏幕横向,会发现又出现问题了。
为了解决这个问题,在转向的时候,生成新的视图,在vieDidload下添加代码:
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration{
[super willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration];
[self setupScrollContent];
}
然后,如果已经存在一个容器视图,就修改setupScrollContent来移除该容器视图,添加代码:
- (void)setupScrollContent{
if (carImageContainerView != nil) {
[carImageContainerView removeFromSuperview];
}
...
}
哦,对了,忘了弄Car Number了
在CarImageViewController.m中添加
- (void)updateCarNumberLabel {
NSInteger carIndex = [self carIndexForPoint:self.scrollView.contentOffset];
NSString *newText = [NSString stringWithFormat:@"Car Number: %ld",carIndex+1];
self.carNumberLabel.text = newText;
}
- (NSInteger)carIndexForPoint:(CGPoint)thePoint {
CGFloat pageWidth = self.scrollView.frame.size.width;
pageWidth *= self.scrollView.zoomScale; //1
return (NSInteger)(thePoint.x/pageWidth);//2
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
[self updateCarNumberLabel];//3
}
注释:
- 用缩放倍数乘以滚动视图的宽度,获得实际的页面宽度
- 将浮点除法的结果转换成整型
当滚动时,自动更新Car Number标签
我们还需要在视图第一次出现时设置汽车车牌。在viewDidLoad下面添加
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear: animated];
[self updateCarNumberLabel];
}
今天的介绍就到这里咯
我的另一个博客站点:Arnold-你们好啊