(本文翻译自AppCoda,详见:原文链接:A Beginner’s Guide to
UIScrollView)

在iOS中,滚动视图UIScrollView用于查看大于屏幕的内容。Scroll
View有两个主要目的:

原文: appcoda.com (IOS Beginner’s Guide to UIScrollView 由joyce
echessa
发布翻译: 安明哲说明: 转载时请注明出处

在iOS开发中,滚动视图(UIScrollView)通常用于显示内容尺寸大于屏幕尺寸的视图。滚动视图有以下两个主要作用:

  • 让用户拖动视图以显示更多内容区域。
  • 让用户使用捏合手势放大或缩小所显示的内容。

在IOS中,scroll view
本用作显示屏幕上不能完全装下的内容。它主要有两个作用:

  • 让用户可以通过拖拽手势来观看想看到的内容

  • 让用户可以通过捏合手势来放大或缩小观看的内容

下图显示了UIScrollView拖动要显示的内容区域,以显示不同内容。UIScrollView上添加一个子视图UIImageViewUIImageView上显示一个男孩的图片。当手指在屏幕上滑动时,屏幕上显示的内容随之移动,滚动条开始显示;当手指离开屏幕,滚动条消失。

  • 用户可以通过拖动的方式显示更多内容
  • 用户可以通过手势缩放现实的内容

在iOS应用中常见的表格视图(UITableView)就继承自滚动视图,并因此可以通过上下滚动来显示更多的内容。

yzc579亚洲城官网 1ScrollViewIndicator.jpg

IOS的公共控件UITbaleView就是继承与UIScrollView并提供了一种不错的方式去呈现内容(当这个内容大于屏幕尺寸)

在本篇教程中,我们将讨论滚动视图的诸多方面内容,主要包括:使用纯代码和可视化编程两种方式来创建一个滚动视图、实现滚动和缩放功能,以及如何嵌套使用滚动视图。

UIScrollView不包含任何视图用以显示其内容,只能滚动其子视图。另外,UITableViewUICollectionViewUITextView均继承自UIScrollView

在本节课程中,我们将从多方面了解ScrollView,特性,创建一个ScrollView(通过代码和通过Interface
Builder),滚动和缩放,insets和outsets。

yzc579亚洲城官网 2

这篇文章将使用纯代码来学习UIScrollView,使用Interface
Builder
添加滚动视图和添加其他视图没有区别,这里不再介绍。

yzc579亚洲城官网 3开始阅读之前,请首先下载本届课程的源代码。

继续阅读之前,请先下载本文示例代码所需的资源文件,详见:资源文件地址。(译注:原文资源文件地址需要FQ访问,本人已转存到GitHub上,详见这里)

UIScrollView创建后需要添加到其他视图控制器或视图层级中,要使用滚动视图,必须配置以下两项:

译者注:这里的下载需要自带梯子,如果没有梯子,可以从我的服务器获取

UIScrollView同其他视图一样,可以通过纯代码和可视化编程两种方式来创建。在创建之后,只需要少量额外设置就可以让UIScrollView获得基本的滚动功能。

  • 必须设定contentSize属性。contentSize属性用于指定滚动视图可以滚动的区域。
  • 必须为滚动视图添加一个或多个视图,滚动视图用这些视图显示内容和实现滚动。

通过编码方式创建ScrollView

不管是通过代码或者Interface builder都可以在一个view中创建一个Scroll
View,然后做一点点必要的配置就实现一个基本的ScrollView的功能了。

  • 你必须设置 ContentSize
    属性,此属性用于指定你要展现的内容的Size,IOS由此确认滚动区域。
  • 你还必须添加一个或者多个View以供显示。

当然,还有许多可选的配置,垂直或是水平滚动,滑动、缩放的效果,滚动条的路径等等。

现在,我们开始通过代码创建一个ScrollView,打开
ScrollViewDemo,仅仅是一个简单地Signle
View,其中ScollerViewController这个类与Interface
Builder中的UIViewController关联,并且此项目还包含了一张图片image.png
(图片来自unsplash.com)

打开ScrollViewController.swift 并且添加如下代码:

var scrollView: UIScrollView!var imageView: UIImageView!

修改 viewDidLoad() 如下:

override func viewDidLoad() { super.viewDidLoad() imageView = UIImageView(image: UIImage(named: "image.png")) scrollView = UIScrollView(frame: view.bounds) scrollView.backgroundColor = UIColor.blackColor() scrollView.contentSize = imageView.bounds.size //译者注:如果你是用的是swift2.x 这行代码会出现问题 scrollView.autoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight scrollView.addSubview(imageView) view.addSubview(scrollView)}

译者注:上面的代码中带注释的一行会在swift2下报错,在swift2.0中同时取得两个枚举值不再支持使用
| 运算符,而改用数组
,所以swift2中正确的代码应该是这个样子scrollView.autoresizingMask = [UIViewAutoresizing.FlexibleHeight,UIViewAutoresizing.FlexibleWidth]

上面的代码创建了一个scroll
view和一张图片。其中ImageView是ScrollView的子view;contentSize指定了可滚动的地带为图片的大小(2000
x 1500);scrollView的背景为黑色,设置autoresizeMask为.FlexibleWidth和
.FlexibleHeight以便于在屏幕旋转的时候scrollview与parsentView维持正确位置关系。运行这个app的时候你应该能够图片的没一部分。

yzc579亚洲城官网 4

当你运行的时候,你可能会注意到,通常你只能看到图片左上角的那一部分,就像下面这样:

yzc579亚洲城官网 5这是因为scrollview的bound被设置为,也就是图片的左上角。如果你希望重新定位第打开app时图片的显示位置,你需要改变scrollview的bound,SrollerView有一个contentOffset属性可以帮助你实现这个需求。

添加如下代码到你的代码中,(注意这段代码应该在autoresizingMask之后):

scrollView.contentOffset = CGPoint(x: 1000, y: 450)

这个时候再次运行app你将看到scrollview已经移动到图皮的另一部分,这样当view被加载的时候,你就可以确定你要给用户呈现什么。

yzc579亚洲城官网 6

UIScrollView也和其他视图一样,应该被一个控制器管理或者添加到某个视图层级中。想要完成滚动功能还需要对UIScrollView进行以下两步设置:

其他属性均是可选实现,如:scrollsToToppagingEnabledbouncesrefreshControl等。

缩放

我们已经创建了一个scrollview并且允许用户通过滚动来控制一个较大尺寸的view,但是如果视图可以缩放,将进一步增强用户体验。

yzc579亚洲城官网,要支持缩放,你必须为view添加一个delegate,这个delegate必须遵从IScrollViewDelegate这个协议,并且必须要实现viewForZoomingInScrollView(),该方法返回一个view,这个view将可以在scrollview内缩放。

你依旧要做一点点的工作来支持缩放,你可以设置scrollvierw的
minimumZoomScale 和
maximumZoomScale属性(如果不设定这两个属性,他们将会有一个默认值–>1.0)

修改ScrollerView的定义如下:

class ScrollViewController: UIViewController, UIScrollViewDelegate

然后添加如下方法到类中:

func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? { return imageView }

最后,添加如下代码到viewDidLoad()的底部:译者注:主要就是绑定delegate,设置缩放的范围和步长

scrollView.delegate = selfscrollView.minimumZoomScale = 0.1scrollView.maximumZoomScale = 4.0scrollView.zoomScale = 1.0

上面的代码中设置了zoomScale为1.0用于指定最大缩放和最小缩放的缩放因子(默认情况下缩放的大小),当你运行app,你可以滚动并且缩放,我们设置最大缩放比例4.0,所以你最大可以放大这张图片到原始尺寸的4倍,但是当我们把图片放大的时候,图片会变得很很模糊,这并不是用户想要看到的,下一步中我们将让图片返回到1.0初始的状态。

yzc579亚洲城官网 7

在上边,我们设置minimumZoomScale为0.1以至于缩小之后返回一个很小的图片并且屏幕中出现了很多空白。我们想让图片自适应

yzc579亚洲城官网 8

要实现这个功能,我们需要根据scrollview的size和imageview的size计算最小缩放比例。

首先删除viewDidLoad里面的几行代码:

scrollView.minimumZoomScale = 0.1scrollView.maximumZoomScale = 4.0scrollView.zoomScale = 1.0

添加一个方法到类中,我们得到的宽度和高度的比例,并选择较小的两者,并设置为minimumScale。提示一下,我们删除了maxinmumZoomScale,所以他会被默认设置为1.0.

func setZoomScale(){ let imageViewSize = imageView.bounds.size let scrollViewSize = scrollView.bounds.size let widthSacle = scrollViewSize.width / imageViewSize.width let heightSacle = scrollViewSize.height / imageViewSize.height scrollView.minimumZoomScale = min(widthSacle, heightSacle) scrollView.zoomScale = 1.0 }

然后再viewDidLoad()中调用这个方法

setZoomScale()

同时添加以下代码,以便在设备方向改变后图像依旧铺满屏幕。

 override func viewWillLayoutSubviews() { setZoomScale() }

yzc579亚洲城官网 9从上面的图片中,你可能会注意到,图片的位置在屏幕左上角,我们想改变他到屏幕的中心。

添加如下方法到类中:

func scrollViewDidZoom(scrollView: UIScrollView) { let imageViewSize = imageView.frame.size let scrollViewSize = scrollView.bounds.size let verticalPadding = imageViewSize.height < scrollViewSize.height ? (scrollViewSize.height - imageViewSize.height) / 2 : 0 let horizontalPadding = imageViewSize.width < scrollViewSize.width ? (scrollViewSize.width - imageViewSize.width) / 2 : 0 scrollView.contentInset = UIEdgeInsets(top: verticalPadding, left: horizontalPadding, bottom: verticalPadding, right: horizontalPadding)}

这个方法会在每次一缩放操作后执行,它告诉delegate,scrllowView的缩放比例发生了改变,上面的代码计算了padding并且根据padding重新设置了scrollview
内容的padding

此时,运行app缩放到最小是 将得到如下效果

yzc579亚洲城官网 10

  • 必须设置UIScrollViewcontentSize属性,它提供了UIScrollView的内容的大小,也就是可以滚动的区域的大小。

  • 必须为UIScrollView添加一个或多个用于显示和滚动的子视图,这些视图提供了UIScrollView显示的内容。

自iOS10开始,UIScrollView增加了refreshControl属性,用于把配置好的UIRefreshControl赋值给该属性,这样UIScrollView就有了下拉刷新功能。在此之前,只能在UITableViewController中使用系统提供的下拉刷新功能,想了解具体使用方法可以查看我的另一篇文章:在UIScrollView、UICollectionView和UITableView中添加UIRefreshControl实现下拉刷新

通过连按缩放

默认情况下ScrollView通过少量的代码即可实现支持缩放手势,但是支持更多的手势我们则需要再做一些工作。

IOS人机交互的借口定义了一种通过双击来进行缩放的方法。但是这个方法认为view的缩放级别是单一的总之就是双击一次执行了放大操作后下一次会默认执行缩小操作。但是很多程序的交互行为是需要更灵活的双击缩放的,比如地图应用,需要不停地双击放大,而不是放大后再双击变成缩小。

在我们app中,我们将实现double-tap 把图片放到最大,而后双击再缩小

添加如下代码到类中:

func setupGestureRecognizer() { let doubleTap = UITapGestureRecognizer(target: self, action: "handleDoubleTap:") doubleTap.numberOfTapsRequired = 2 scrollView.addGestureRecognizer(doubleTap)} func handleDoubleTap(recognizer: UITapGestureRecognizer) { if (scrollView.zoomScale > scrollView.minimumZoomScale) { scrollView.setZoomScale(scrollView.minimumZoomScale, animated: true) } else { scrollView.setZoomScale(scrollView.maximumZoomScale, animated: true) }}

然后再viewDidLoad底部调用他们:

setupGestureRecognizer()

在上面的代码中,我们添加了一个gesture
recognize,当用户双击之后,我们可以根据当前的缩放级别来进行放大和缩小。这样,我们就可以通过双击放大和缩小图片了。

你还可以根据应用的具体需求设置UIScrollView的一些显示效果,比如:是否显示水平和竖直方向的滚动条、滚动的弹性效果、缩放的弹性效果,以及允许的滚动方向等。

打开Xcode,创建新的工程。选取iOS一栏下Application中的Single View
Application
模板,点击Next。在Product
Name
中填写ScrollView,点击Next选择文件位置,点击Create创建工程。

接下来我们将在代码中创建一个UIScrollView。在下载的资源文件中打开ScrollViewDemo工程。它就是一个简单的Single View Application工程,只不过将storyboard中根控制器的类型绑定为自己新建的叫做ScrollViewController的控制器,还在项目中添加了一张我们要用到的图片,图片名称为image.png

在开始之前,点击这里下载图片,将其添加到Assets.xcassets。打开ViewController.m,添加以下声明。

接下来打开ScrollViewController.swift文件,添加如下代码。

@interface ViewController ()@property (strong, nonatomic) UIScrollView *scrollView;@property (strong, nonatomic) UIImageView *imageView;@end
var scrollView: UIScrollView!var imageView: UIImageView!

使用懒加载初始化以上属性。

按如下代码所示修改viewDidLoad()方法。

- (UIImageView *)imageView { if (!_imageView) { UIImage *image = [UIImage imageNamed:@"image"]; // 1.初始化imageView _imageView = [[UIImageView alloc] initWithImage:image]; } return _imageView;}- (UIScrollView *)scrollView { if (!_scrollView) { // 2.初始化、配置scrollView _scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds]; _scrollView.backgroundColor = [UIColor blackColor]; _scrollView.contentSize = self.imageView.frame.size; _scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; } return _scrollView;}
override func viewDidLoad() { super.viewDidLoad() imageView = UIImageView(image: UIImage(named: "image.png")) scrollView = UIScrollView(frame: view.bounds) scrollView.backgroundColor = UIColor.blackColor() scrollView.contentSize = imageView.bounds.size scrollView.autoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight scrollView.addSubview(imageView) view.addSubview(scrollView)}

上述代码分步说明如下:

上述代码创建了一个UIScrollViewUIImageViewUIImageView被设置为UIScrollView的子视图。contentSize属性控制滚动区域的大小,我们将它设置为跟图片的尺寸一样大(2000×1500)。我们将滚动视图的背景色设置为黑色,这样图片就像在一块黑色幕布上滚动一样。我们将滚动视图的autoresizingMask属性设置为.FlexibleWidth.FlexibleHeight,使它能够在设备旋转之后自动适应新的宽度和高度。运行当前应用,你已经可以通过拖拽手势来滚动显示图片了。

  1. 使用initWithImage:方法初始化imageView。用这种方法初始化的图片视图其大小与图片大小一致,这里图片大小为2000*1500。
  2. 初始化scrollView时,指定scrollViewframe与当前控制器视图大小、位置一致。设定contentSize为整个imageView的大小。最后设定scrollViewautoresizingMaskUIViewAutoresizingFlexibleWidthUIViewAutoresizingFlexibleHeight,这样在屏幕旋转时,scrollView就可以自动调整布局。

yzc579亚洲城官网 11

进入viewDidLoad,把scrollView添加到控制器视图,把imageView添加到scrollView

当你启动应用后,你会发现图片初始显示区域是它左上角的部分。

- viewDidLoad { [super viewDidLoad]; [self.view addSubview:self.scrollView]; [self.scrollView addSubview:self.imageView];}

yzc579亚洲城官网 12

运行demo,现在可以拖动视图查看图像的不同部分。

这是因为滚动视图的bounds的起点默认为,代表了左上角。如果你想改变启动后显示的位置,你需要更改滚动视图的bounds的起点。因为这种需求经常被提起,所以UIScrollView专门提供了一个属性contentOffset用来实现这种需求。

yzc579亚洲城官网 13ScrollViewDrag.gif

在代码中添加如下语句,注意添加在设置autoresizingMask语句之后。

当滑动到屏幕边缘时再继续滑动(如上面GIF中开始部分),如果bounces属性为YES,则会反弹;反之,则直接不可滑动。另外,点击状态栏(status
bar),滚动视图会直接回到顶部,这是因为scrollsToTop属性默认值为YES。但如果存在多个滚动视图,scrollsToTop属性均为默认值,点击状态栏时将没有任何效果。如果想要获得滑到顶部效果,必须设置其他scrollsToTop属性为NO,只能有一个scrollsToTop属性为YES

scrollView.contentOffset = CGPoint(x: 1000, y: 450)

滚动视图的origin可以在内容视图上移动,滚动视图按照自身frame裁剪内容视图。滚动视图跟踪手指的移动并相应调整origin,内容视图会根据新的origin绘制内容。滚动视图自身只绘制滚动条,除此之外不绘制任何其他内容。

重新运行应用,你会发现一开始就会显示图片的另一部分而不是左上角。你可以通过这种方式来决定程序启动后将要显示的内容。

运行demo时,图片原点显示在scrollView左上角。如果想要修改内容视图显示的内容,需要修改滚动视图的originUIScrollView中的contentOffset属性用于指定内容视图的origin偏离滚动视图origin
的点。默认值为CGPointZero

yzc579亚洲城官网 14

scrollView的懒加载方法底部,添加以下代码:

我们已经添加了一个UIScrollView,并且能够让用户通过拖拽来观看尺寸大于屏幕尺寸的内容。相当棒,但如果视图能够缩放的话会带来更好的体验。

- (UIScrollView *)scrollView { if (!_scrollView) { ... _scrollView.contentOffset = CGPointMake(1000, 450); } return _scrollView;}

要支持缩放功能,你必须为UIScrollView设置一个代理,而且代理必须遵守UIScrollViewDelegate协议,代理还需要实现viewForZoomingInScrollView()方法,该方法返回想要被缩放的视图。

运行demo,可以看到滚动视图已经移动到照片的其他部分。因此,你可以决定加载视图时显示滚动视图的哪一部分。

你还应该为缩放设置一个比例,可以通过UIScrollViewminimumZoomScalemaximumZoomScale这两个属性来实现,它们的默认值都是1.0。

yzc579亚洲城官网 15ScrollViewcontentOffset.png

按照如下代码更改ScrollViewController的定义:

我们添加了一个UIScrollView,使用户可以浏览超出屏幕大小的内容视图。如果用户可以放大和缩小视图,将会更加实用。

class ScrollViewController: UIViewController, UIScrollViewDelegate {

为支持缩放功能,定义的类必须遵守UIScrollViewDelegate协议,必须实现viewForZoomingInScrollView:代理方法,在该代理方法中返回要缩放的视图。另外,还需要使用maximumZoomScaleminimumZoomScale指定可应用于滚动视图的最大、最小缩放比。这两个属性的默认值均为1.0

然后添加如下代码:

视图控制器遵守UIScrollViewDelegate协议,更新ViewController.m如下:

func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? { return imageView}
#import "ViewController.h"@interface ViewController () <UIScrollViewDelegate>

接下来在viewDidLoad()方法的最后添加如下代码:

在实现部分添加以下代理方法,指定可以缩放的视图为imageView

scrollView.delegate = self scrollView.minimumZoomScale = 0.1scrollView.maximumZoomScale = 4.0scrollView.zoomScale = 1.0
- viewForZoomingInScrollView:(UIScrollView *)scrollView { return self.imageView;}

在上述代码中,我们设置了zoomScale为1.0,然后设置了缩放的最大和最小比例。在程序运行后,会按照图片的原始尺寸显示(因为zonmScale为1.0),当你使用捏合手势来操作图片时,你会发现图片可以被缩放了。我们设置了maximumZoomScale为4.0,所以图片最大只能放大到4倍。你也会发现,图片放大4倍后会变得很模糊,所以接下来我们会把它的缩放比例重新设置为1.0。

scrollView的懒加载方法中,将当前控制器设置为scrollView的代理,scrollView的最小、最大缩放比分别为0.14.0,当前缩放比zoomScale1.0zoomScale默认值为1.0

yzc579亚洲城官网 16

- (UIScrollView *)scrollView { if (!_scrollView) { ... _scrollView.delegate = self; _scrollView.minimumZoomScale = 0.1; _scrollView.maximumZoomScale = 4.0; _scrollView.zoomScale = 1.0; } return _scrollView;}

从上面的图片中我们可以发现,我们之前将minimumZoomScale设置为0.1实在是太小了,屏幕空出了很多空闲的地方。在横屏模式下,空闲的区域看上去更大。我们希望图片能在某一方向上能与屏幕相匹配,让图片既能完全显示,又能尽量减少屏幕的空闲空间。

运行demo,图片将与之前的显示一样。此时,zoomScale1.0。当缩放视图时,可以缩放到minimumZoomScalemaximumZoomScale设定的比例。当图片放大到四倍时,图像会变的模糊

yzc579亚洲城官网 17

yzc579亚洲城官网 18ScrollViewBlurry.png

要达到这样的效果,你必须通过图片尺寸和UIScrollView的尺寸来计算最小的缩放比例。

当我们把图片缩小到最小时(此时,zoomScale0.1),会产生一个很小的图像,并在屏幕上产生大量的空白,如下图。

首先在viewDidLoad()方法中删除以下三行代码:

yzc579亚洲城官网 19ScrollViewzooming.png

scrollView.minimumZoomScale = 0.1scrollView.maximumZoomScale = 4.0scrollView.zoomScale = 1.0

我们想要让图像在完整显示的前提下,尽可能多的填充scrollView,即imageView使用UIViewContentModeScaleAspectFit效果。为此,我们将使用滚动视图和图像视图大小的比例来计算最小比例因子(scale
factor)。

在控制器类中添加如下方法。在方法中,我们算出图片同UIScrollView的高度和宽度的比值,并将最小缩放比例设置为两者中更小的那个。注意,我们已经删除了maximumZoomScale的设置,所以它的默认值为1.0。

首先移除scrollView懒加载中的以下代码:

func setZoomScale() { let imageViewSize = imageView.bounds.size let scrollViewSize = scrollView.bounds.size let widthScale = scrollViewSize.width / imageViewSize.width let heightScale = scrollViewSize.height / imageViewSize.height scrollView.minimumZoomScale = min(widthScale, heightScale) scrollView.zoomScale = 1.0}
 _scrollView.minimumZoomScale = 0.1; _scrollView.maximumZoomScale = 4.0; _scrollView.zoomScale = 1.0;

viewDidLoad()方法最后调用这个方法:

在实现部分添加以下方法。先取得scrollViewimageView宽、高比,将minimumZoomScale设置为其中小的值。这里没有设定maximumZoomScale,所以只能放大到默认值1.0,和初始zoomScale默认值相同。

setZoomScale()
- setZoomScale { CGFloat widthScale = CGRectGetWidth(self.scrollView.frame) / CGRectGetWidth(self.imageView.frame); CGFloat heightScale = CGRectGetHeight(self.scrollView.frame) / CGRectGetHeight(self.imageView.frame); self.scrollView.minimumZoomScale = MIN(widthScale, heightScale);}

viewWillLayoutSubviews()方法中也需要调用该方法,这样当用户改变屏幕方向后,图片的尺寸仍然是正确的。

最后在viewDidLoad底部调用该方法。

override func viewWillLayoutSubviews() { setZoomScale()}
- viewDidLoad { ... [self setZoomScale];}

运行程序,现在你会发现无论你缩放到多小,图片都会完整显示并且尽量占满剩余的空间。

重写viewWillLayoutSubviews方法如下:

yzc579亚洲城官网 20

- viewWillLayoutSubviews { [super viewWillLayoutSubviews]; // 1.移除子视图 for (UIView *view in self.scrollView.subviews) { [view removeFromSuperview]; } // 2.初始化imageView 将其添加到scrollView 设置contentSize self.imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"image"]]; [self.scrollView addSubview:self.imageView]; self.scrollView.contentSize = self.imageView.frame.size; // 3.重设minimumZoomScale [self setZoomScale];}

我们可以发现,图片是被定位在屏幕左上角的,我们希望将它放在屏幕中间。

上述代码的分步说明如下:

在代码中添加如下方法。

  1. 移除scrollView的所有子视图。
  2. 使用图片image初始化imageView,并将其添加到scrollView,最后重新设定scrollView的可滑动区域contentSize。这样做是为了在设备旋转时,重新计算imageView的位置、大小,同时更新scrollView内容视图大小。
  3. 最后再次调用setZoomScale方法,设定最小缩放比。
func scrollViewDidZoom(scrollView: UIScrollView) { let imageViewSize = imageView.frame.size let scrollViewSize = scrollView.bounds.size let verticalPadding = imageViewSize.height < scrollViewSize.height ? (scrollViewSize.height - imageViewSize.height) / 2 : 0 let horizontalPadding = imageViewSize.width < scrollViewSize.width ? (scrollViewSize.width - imageViewSize.width) / 2 : 0 scrollView.contentInset = UIEdgeInsets(top: verticalPadding, left: horizontalPadding, bottom: verticalPadding, right: horizontalPadding) }

运行app,当缩小imageView时,imageView会尽可能占用屏幕上空间,同时可以看到完整的图像视图。

这个方法在缩放的时候就会被调用,它会通知代理UIScrollView的缩放比例发生改变了。在上面的方法中,我们计算了图片在滚动视图中的内间距,从而使图片始终在屏幕的中间。对于上、下方向的内边距,我们首先判断图片视图的高度是否小于滚动视图的高度,如果是就将边距设为两者的差值的一半,否则设为0。水平间距我们采用同样的方式计算。然后通过contentInset属性设置所有方向的内边距,这个属性代表了UIScrollView的内容距离UIScrollView本身四周的距离。

yzc579亚洲城官网 21ScrollViewaspectFit.png

运行程序,你会发现当你缩小图片时,图片始终保持在屏幕的中间。

从上面图像可以看到,图像位于屏幕的左上角,我们希望图像位于屏幕的中心。在代理方法scrollViewDidZoom:中添加以下代码:

yzc579亚洲城官网 22

- scrollViewDidZoom:(UIScrollView *)scrollView { // 计算imageView缩小到最小时 imageView与屏幕边缘距离 CGFloat horizontalPadding = CGRectGetWidth(self.imageView.frame) < CGRectGetWidth(scrollView.frame) ? (CGRectGetWidth(scrollView.frame) - CGRectGetWidth(self.imageView.frame)) / 2 : 0 ; CGFloat verticalPadding = CGRectGetHeight(self.imageView.frame) < CGRectGetHeight(scrollView.frame) ? (CGRectGetHeight(scrollView.frame) - CGRectGetHeight(self.imageView.frame)) / 2 : 0 ; scrollView.contentInset = UIEdgeInsetsMake(verticalPadding, horizontalPadding, verticalPadding, horizontalPadding);}

UIScrollView默认只支持通过捏合手势来实现缩放效果,如果想实现通过双击来缩放,则需要自己做些额外的设置。

每次缩放操作后,系统都会调用该方法用以告诉滚动视图zoomScale已改变。在上面的代码中,我们先计算出图片与屏幕间的填充。对于顶部和底部,先判断imageView的高是否小于scrollView的高,在imageView的高小于scrollView的高时,填充高度为两个视图高之差的二分之一,否则,填充为0;水平方向的填充与此类似。最后设定scrollViewcontentInset属性,该属性用于指定内容视图与滚动视图边缘的距离,单位为point,默认值为UIEdgeInsetsZero

iOS人机界面指南中介绍了可以通过双击手势来达到缩放的效果。使用双击手势进行缩放需要一定的前提:要缩放的视图只能在最大和最小比例两个固定值之间来回缩放,就像苹果官方的相册应用一样,当你双击图片时,图片放大至最大,当你再次双击时,图片缩小至最小,或者可以通过连续的双击使视图一点点达到最大,然后再次双击的时候,将视图恢复为全屏显示。但是大多数应用需要实现更灵活的双击缩放效果,例如地图应用,当你双击时会使其放大,继续双击会继续放大,想要缩小则可以使用双指捏合手势来实现。

运行demo,在缩小视图至最小时,视图应当位于屏幕的中心。

要想在你的程序中实现双击缩放功能,你需要监听UIScrollView的手势并进行处理。在我们的程序中,我们将模仿苹果官方的相册应用的效果,当你双击时放大到最大值,再次双击时则缩小到最小值。

yzc579亚洲城官网 23ScrollViewUIEdgeInsets.png

在代码中添加如下两个方法。

在获取视图的宽、高时应当使用CGRectGetWidth()CGRectGetHeight()方法,避免使用self.view.bounds.width直接从CGRect中读取、操作数据。这是因为CGRect数据结构中的宽、高有可能是负数值。比如A矩形CGRectMake(0, 0, 100, 100)与b矩形CGRectMake(100, 100, -100, -100)位置、尺寸完全一致。宽、高中正数值表示向右、向下方向,负数值表示向左、向上方向。通过调用CGRectGetWidthCGRectGetHeight方法获得width、height为正数值。

func setGestureRecognizer() { let doubleTap = UITapGestureRecognizer(target: self, action: "handleDoubleTap:") doubleTap.numberOfTapsRequired = 2 scrollView.addGestureRecognizer(doubleTap)}func handleDoubleTap(recognizer: UITapGestureRecognizer) { if (scrollView.zoomScale > scrollView.minimumZoomScale) { scrollView.setZoomScale(scrollView.minimumZoomScale, animated: true) } else { scrollView.setZoomScale(scrollView.maximumZoomScale, animated: true) }}

滚动视图默认支持了UIPanGestureRecognizerUIPinchGestureRecognizer。想要了解更多手势使用,查看我的另一篇文章:手势控制:点击、滑动、平移、捏合、旋转、长按、轻扫

然后在viewDidLoad()方法最后调用上面的方法。

添加几行代码就可以让UIScrollView支持捏合手势。在iOS Human Interface
Guidlines
中建议双击放大或缩小视图,但这一约定默认了视图只有一个缩放等级,如在相册应用中双击把图片放大至最大,再次双击把图片缩小至最小。或者双击会将视图放大至最大,一旦放大到最大后再次双击,视图将缩小至全屏。但也有一些应用在使用双击放大这一功能时会有特殊需求,例如地图应用。地图应用需要双击放大视图,再次双击继续放大视图。为了缩小视图,需要两个手指在屏幕上滑动,当手指靠近时视图缩小。

setGestureRecognizer()

为了让应用支持点击缩放功能,需要在遵守UIScrollViewDelegate协议的类中添加响应手势的方法。当该类检测到点击、双击、两个手指触摸动作时,会触发对应响应操作。

在上面的代码中,我们为UIScrollView添加了一个双击手势的监听,然后根据图片当前的缩放比例,来判断是将图片放大或者缩小。

在这个demo将实现以下功能:当视图最小时,双击视图放大至maximumZoomScale;否则,双击缩小视图至minimumZoomScale。与日常使用中的相册应用一致。

运行程序,你会发现已经能通过双击手势来缩放图片了。

viewDidLoad中为scrollView添加双击手势。

使用storyboard可以实现和我们上面使用代码方式实现的同样的功能,而且更为简单,代码量更少。

- viewDidLoad { ... // 添加双击手势 UITapGestureRecognizer *doubleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleDoubleTap:)]; doubleTap.numberOfTapsRequired = 2; [self.scrollView addGestureRecognizer:doubleTap];}

Main.storyboard文件中,拖一个新的视图控制器,并将其设置为初始控制器(既可以将箭头拖到新控制器上,也可以在属性选项卡中选中Is Initial View Controller复选框)。

在实现部分添加双击手势响应方法。

拖一个UIScrollView到新的控制器中,然后设置其边缘始终粘着屏幕。

- handleDoubleTap:(UITapGestureRecognizer *)doubleTap { if (self.scrollView.zoomScale > self.scrollView.minimumZoomScale) { // 视图大于最小视图时 双击将视图缩小至最小 [self.scrollView setZoomScale:self.scrollView.minimumZoomScale animated:YES]; } else { // 视图为最小时 双击将视图放大至最大 [self.scrollView setZoomScale:self.scrollView.maximumZoomScale animated:YES]; }}

yzc579亚洲城官网 24

在上面代码中,我们根据当前zoomScale进行放大或缩小视图的操作。

然后拖一个UIImageView到刚才的UIScrollView中,将它的边缘设置为粘着UIScrollView

运行demo,可以通过双击放大、缩小视图。

yzc579亚洲城官网 25

yzc579亚洲城官网 26ScrollViewDoubleTap.gif

要记住UIScrollView需要知道它的内容的大小,才可以实现滚动。当你为UIImageView设置图片时,UIScrollView的内容大小就会被自动设置为图片的大小。

观察上面GIF你会发现,使用双击放大手势点击scrollView任意位置,图像均以自身的中心为基点进行放大操作,而不是以点击位置为中心进行放大。我们在查看照片时,更希望视图以点击位置为中心进行放大。

UIImageView的属性选项卡中,将Image属性设置为image.png,然后通过updating the frames解决自动布局的问题。运行程序,你会发现已经实现图片的滚动显示了,并且没有敲一行代码。你还可以在UIScrollView的属性选项卡中查看还有哪些属性可以设置,比如可以设置最大和最小的缩放比例。

下面实现以点击位置为中心放大图片。更新handleDoubleTap:方法如下:

如果想要实现缩放功能,你仍然需要通过代码,设置代理并实现viewForZoomingInScrollView()方法,同我们之前做过的一样,就不再重复一遍了。

- handleDoubleTap:(UITapGestureRecognizer *)doubleTap { ... else { /* // 视图为最小时 双击将视图放大至最大 [self.scrollView setZoomScale:self.scrollView.maximumZoomScale animated:YES]; */ // 1.获取点击位置 CGPoint touchPoint = [doubleTap locationInView:self.imageView]; // 2.获取要显示的imageView区域 CGRect zoomRect = [self zoomRectForScrollView:self.scrollView withScale:self.scrollView.maximumZoomScale withCenter:touchPoint]; // 5.将要显示的imageView区域显示到scrollView [self.scrollView zoomToRect:zoomRect animated:YES]; }}- zoomRectForScrollView:(UIScrollView *)scrollView withScale:scale withCenter:center { // 3.声明一个区域 滚动视图的宽除以放大倍数可以得到要显示imageView宽度 CGRect zoomRect; zoomRect.size.width = CGRectGetWidth(scrollView.frame) / scale; zoomRect.size.height = CGRectGetHeight(scrollView.frame) / scale; // 4.点击位置x坐标减去1/2图像宽度,可以得到要显示imageView的原点x坐标 y坐标类似 zoomRect.origin.x = center.x - zoomRect.size.width / 2; zoomRect.origin.y = center.y - zoomRect.size.height / 2; return zoomRect;}

可以在一个UIScrollView中嵌套另一个UIScrollView,两个UIScrollView既可以是相同方向滚动的,也可以是不同方向的。这部分内容的示例代码请使用NestedScrollViews项目。

上述代码分布说明如下:

相同方向的UIScrollView嵌套是指一个UIScrollView,它有另一个UIScrollView作为子控件,并且它们的滚动方向一致。你可以用相同方向的嵌套来实现这样的效果,比如在UIScrollView中添加多组要区分开的数据,你还可以通过它来实现两个UIScrollView同时滚动时的视差效果。在我们的示例中,我们将两个相同方向的UIScrollView设置不同的滚动速度,从而实现滚动时的视差效果。

  1. 获取点击位置在imageView坐标系统中的位置。
  2. 调用zoomRectForScrollView: withScale: withCenter:方法,获取要显示的imageView区域。
  3. 先声明一个区域用以表示要返回的imageView可见区域。scrollView的宽除以放大倍数得到要显示的imageView原图的宽,即zoomRect的宽。取得高的方法也是如此。
  4. 点击位置x坐标减去1/2要显示imageView宽(即zoomRect的宽),可以得到zoomRect原点x坐标。y坐标取得过程类似。
  5. 使用zoomRect: animated:方法把要显示的imageViewzoomRect区域发送给scrollView。这里的zoomRect一定是imageView坐标系统上的区域。

打开NestedScrollViews项目中的storyboard文件,你将看到两个UIScrollView,分别叫做foreground和background。background里面添加了一个UIImageView,并将图片设置为image.png,foreground里面添加了一些标签和一个作为容器用的UIVIew,这些标签只是为了方便我们观看视图的滚动,容器视图我们将在下一节内容中才用到。

zoomToRect:animated:的rect参数必须为viewForZoomingInScrollView:方法返回视图的坐标系统,如果rect大小与content
view大小不同,会自动调整zoomScale

scrollRectToVisible:animated:的rect参数使用scroll
view坐标系统。如果rect视图已经显示在当前屏幕,则该方法什么也不做。

我们的界面这样就算搭建完成了,现在运行程序的话,你会发现只有foreground视图在滚动,而background视图保持不动。接下来我们将要实现background的滚动,并且实现滚动的视差效果。

运行demo,点击不同位置,查看放大效果。

首先将foreground和background两个UIScrollView连线到控制器,之后代码会如下所示:

yzc579亚洲城官网 27zoomRect:

@IBOutlet weak var background: UIScrollView!@IBOutlet weak var foreground: UIScrollView!

UIScrollView支持分页模式。在该模式下,用户每滑动一次手指,移动单个屏幕内所有内容。该模式一般用在电子书或引导页中。

我们需要知道foreground视图滚动了多长的距离,用来计算background视图需要滚动多长的距离。所以我们需要为foreground视图设置一个代理,用来监听它的滚动。

除了像上面那样配置滚动视图,还需要把BOOL类型的pagingEnabled属性设置为YES。该属性用于指定滚动视图是否开启分页。contentSize属性中的高为滚动视图的高,宽为滚动视图的宽乘页数。另外,一般滚动视图的滚动条会被禁用,如果想要显示进度,可以使用UIPageControl。下图显示了开启分页模式的滚动视图滑动过程。

class ViewController: UIViewController, UIScrollViewDelegate {

yzc579亚洲城官网 28ScrollViewpageModeScroll.jpg

viewDidLoad()方法中设置foreground视图的代理。

使用快捷键command+N添加文件,选取弹出框中iOS一栏下Source中的Cocoa
Touch Class
模板,点击NextClassPageViewControllerSubclass
of
UIViewController,点击Next。选择文件位置,点击Create创建文件。我们将在这个文件内实现翻页浏览功能。

foreground.delegate = self

在这一部分我们将在PageViewController上添加一个滚动视图,水平滑动显示上一页、下一页视图,滚动视图上显示当前页的图片,页面底部将会显示UIPageControl

然后实现如下代理方法。

yzc579亚洲城官网 29ScrollViewPagingEnabled.gif

func scrollViewDidScroll(scrollView: UIScrollView) { let foregroundHeight = foreground.contentSize.height - CGRectGetHeight(foreground.bounds) let percentageScroll = foreground.contentOffset.y / foregroundHeight let backgroundHeight = background.contentSize.height - CGRectGetHeight(background.bounds) background.contentOffset = CGPoint(x: 0, y: backgroundHeight * percentageScroll)}

开始之前先添加这一部分所需五张图片,可以通过文章底部的网址下载源码获取。

在上面的代码中,我们获取了foreground视图可以滚动的最大高度,然后用当前滚动的距离除以它以获取滚动的比例,然后获取background视图可以滚动的最大高度,将其乘以滚动比例,就可以得到background应该滚动的距离。运行你的程序,在进行滚动时你会发现foreground和background两个视图都在滚动,并且background视图滚动的更快,从而有一种视差效果。

进入PageViewController.m,添加以下声明。

yzc579亚洲城官网 30

@interface PageViewController ()@property (strong, nonatomic) UIScrollView *scrollView;@property (strong, nonatomic) NSArray *contentList;@property (strong, nonatomic) UIPageControl *pageControl;@end

交叉方向的UIScrollView嵌套是指一个UIScrollView,它有另一个UIScrollView作为子控件,并且它们的滚动方向正好相差90°,接下来我们就演示一下这种情况。

其中,数组contentList内包含要显示照片名称。

NestedScrollViews项目中,你会发现在foreground里面有一个Container View,我们将用它来设置我们水平滚动的UIScrollView

在实现部分前添加预定义,用于指定pageControl的高,同时也是scrollView与顶部距离。

storyboard中新拖入一个控制器,按住Control键从Container View拖到新的控制器,选择embed方式。然后选中这个控制器,将它的Size选项设为Freeform并将其高度设为128,因为Container View的高度就是128。

#define pageControlHeight 70

往新控制器中拖入一个UIScrollView,设置其边缘始终粘着父控件。然后在UIScrollView中拖入一个70×70的UIView,将其背景色设为灰色方便我们观看,然后复制多个,从左到右依次摆放在UIScrollView中。你不需要精确地去设置每一个UIView的位置,接下来我会教你们怎么去做。现在我们的控制器界面应该是这个样子。

使用懒加载初始化scrollViewpageControl

yzc579亚洲城官网 31

- (UIScrollView *)scrollView { if (!_scrollView) { _scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, pageControlHeight, CGRectGetWidth(self.view.frame), CGRectGetHeight(self.view.frame) - 2*pageControlHeight)]; _scrollView.contentSize = CGSizeMake(CGRectGetWidth(self.view.frame) * self.contentList.count, CGRectGetHeight(self.view.frame) - 2*pageControlHeight); _scrollView.backgroundColor = [UIColor whiteColor]; _scrollView.pagingEnabled = YES; _scrollView.delegate = self; _scrollView.showsHorizontalScrollIndicator = NO; } return _scrollView;}- (UIPageControl *)pageControl { if (!_pageControl) { _pageControl = [[UIPageControl alloc] initWithFrame:CGRectMake(0, CGRectGetHeight(self.view.frame) - pageControlHeight, CGRectGetWidth(self.view.frame), pageControlHeight)]; _pageControl.numberOfPages = self.contentList.count; _pageControl.currentPage = 0; } return _pageControl;}

选择最左边的UIView,添加它上边和左边的约束,再添加宽度和高度约束。

scrollViewframe就是上面GIF图中的白色背景部分,这里设定contentSize时其宽度为滚动视图宽度的整数倍,整数由页数决定。最后把pagingEnabled属性设置为YES

yzc579亚洲城官网 32

pageControl的总页数由数组contentList内元素个数决定。开始时,默认显示第一页,即currentPage属性为0

再选择最右边的UIView,添加它的上边、右边、宽度和高度约束。

此时,系统会在_scrollView.delegate = self一行处发出内容为*Assigning
to ‘id<UIScrollViewDelegate>_Nullable’from incompatible type
‘PageViewController const
_strong’
的警告,这是因为我们没有遵守UIScrollDelegate协议。在PageViewController.m的interface添加该协议。

yzc579亚洲城官网 33

@interface PageViewController () <UIScrollViewDelegate>

接下来,选中我们的UIScrollView,然后点击上面菜单栏中的Editor > Resolve Auto Layout Issues > All Views > Add Missing Constraints。这样我们所有的UIView就都添加好了约束。运行你的程序,竖直滚动到底部,你会看见我们的Container View,你可以水平滚动它里面的内容。下图中,我将控制器自身视图的背景色设置为透明,所以你看到的效果就是这样的。

viewDidLoad方法中初始化数组,添加scrollViewpageControl到控制器视图,为scrollView每一页添加图片。更新后如何:

yzc579亚洲城官网 34

- viewDidLoad { [super viewDidLoad]; // 1.初始化数组 self.contentList = @[@"one", @"two", @"three", @"four", @"five"]; // 2.将scrollView和pageControl添加到view 设定控制器背景颜色 [self.view addSubview:self.scrollView]; [self.view addSubview:self.pageControl]; self.view.backgroundColor = [UIColor blackColor]; // 3.为scrollView每一页添加图片 for (NSUInteger i=0; i<self.contentList.count; ++i) { UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(CGRectGetWidth(self.scrollView.frame) * i, 0, CGRectGetWidth(self.scrollView.frame), CGRectGetHeight(self.scrollView.frame))]; imageView.contentMode = UIViewContentModeScaleAspectFit; imageView.image = [UIImage imageNamed:self.contentList[i]]; [self.scrollView addSubview:imageView]; }}

我们的教程这就结束了,并没有包含UIScrollView所有的方方面面,但我希望通过这篇教程可以让你对UIScrollView有初步的了解,更多UIScrollView的知识,你可以查看苹果的官方文档:Scroll
View Programming Guide。

上述代码分步说明如下:

你可以在这里下载完整的示例程序作为学习参考。(译注:原文资源文件地址需要FQ访问,本人已转存到GitHub上,详见这里)

  1. 设定数组内元素,这些元素会在后面用作每一页图片的名称。
  2. scrollViewpageControl添加到控制器视图,设定背景颜色为黑色。
  3. 使用for循环为每一页内容添加图片。每一页图片的宽和高与scrollView的宽高相同,原点x为滚动视图宽乘i,因为要添加的图片在滚动视图内,其坐标系统为滚动视图,所以原点坐标y为0。设定imageViewcontentModeUIViewContentModeScaleAspectFit。之后为imageView添加图片。最后添加imageViewscrollView

在代理方法scrollViewDidEndDecelerating:中,根据当前位置计算出当前视图所处的页数,最后更新pageControl的当前页码currentPage

- scrollViewDidEndDecelerating:(UIScrollView *)scrollView { CGFloat pageWidth = CGRectGetWidth(self.scrollView.frame); NSUInteger page = floor((scrollView.contentOffset.x - pageWidth/2)/pageWidth) + 1; self.pageControl.currentPage = page;}

开启分页后,scrollView的偏移坐标x只有在超过了该页的中点后,才会进入新的一页,否则将会停留在上一页。计算出当前所处页后更新pageControlcurrentPage属性。

在运行demo前,先指定PageViewController为根视图控制器,否则,运行结果还是前一部分的视图。

打开AppDelegate.m,导入PageViewController.h,并在application: didFinishLaunchingWithOptions:方法中设定根控制器。

- application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // 设置PageViewController为根控制器 self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; self.window.rootViewController = [[PageViewController alloc] init]; [self.window makeKeyAndVisible]; return YES;}

添加上面代码后运行demo。

yzc579亚洲城官网 35ScrollViewPagingEnabledTest.gif

可以看到,上面的demo存在这两个问题:

  1. 因为背景色为黑色,导致顶部状态栏的时间、运营商等信息完全看不清。
  2. 点击pageControl时,虽然currentPage属性会自动更新,但滚动视图中的图片并没有更新。

首先添加以下代码,将状态栏的文字改为白色。

- (UIStatusBarStyle)preferredStatusBarStyle { return UIStatusBarStyleLightContent;}

pageControl中添加点击事件,当接收到UIControlEventValueChanged事件时,更新滚动视图中图片。

- (UIPageControl *)pageControl { if (!_pageControl) { ... [_pageControl addTarget:self action:@selector(changePage:) forControlEvents:UIControlEventValueChanged]; } return _pageControl;}- changePage:sener { NSUInteger page = self.pageControl.currentPage; CGRect bounds = self.scrollView.bounds; bounds.origin.x = CGRectGetWidth * page; bounds.origin.y = self.scrollView.frame.origin.y; [self.scrollView scrollRectToVisible:bounds animated:YES];}

在点击事件的响应方法changePage:中,根据当前的currentPage计算出目前页码的frame,该frame的坐标系统为scrollView。最后使用scrollRectToVisible:
方法滑动当前视图到该区域。

完成后运行如下:

yzc579亚洲城官网 36pageEnabled.gif

在上面的视图中,内容视图上内容很少,我们可以在一个视图上(即滚动视图的contentSize)一次绘制所有内容。这样很简单,但在处理内容视图上有很多内容(如电子书有成千上万页),或内容视图的绘制非常耗费时间时,就会变得非常低效。这时你需要使用多个视图,每个视图只显示一页内容。如一个视图显示当前页内容,一个视图显示上一页内容,另一个视图显示下一页内容,当用户滑动视图时,循环使用这些视图。这里不再详细说明,想要详细了解可以点击这里查看。

为提供更丰富的使用体验,可以在应用中嵌套使用UIScrollView

嵌套的滚动视图,可以分为相同方向滚动和滚动方向相差90度的滚动。如下图:

yzc579亚洲城官网 37ScrollViewNestedScrollViews.jpg

嵌套类滚动视图的一个应用示例是股票类应用程序。在使用嵌套滚动视图时,我们不需要做任何额外操作,系统默认支持该功能。

Demo名称:ScrollView源码地址:

参考资料:

  1. A Beginner’s Guide to UIScrollView
  2. About Scroll View Programming
  3. PageControl
  4. CGGeometry

欢迎更多指正:

Author

发表评论

电子邮件地址不会被公开。 必填项已用*标注