本篇教学中,我们会来看各种UIScrollView的观念、其中包括以程式建立一个滚动视图与介面建构器(Interface Builder)、滚动(scrolling)与缩放(zooming)、以及巢状滚动视图(nested scroll views )。
在iOS中,滚动视图(scroll view)是用来浏览无法在整个画面容下的其他内容。滚动视图有两个主要用途:
- 提供使用者拖曳至他们想要呈现的内容区域
- 提供使用者使用手指缩放手势来对所呈现的内容放大或缩小
在iOS App的常见控制– UITableView –是一个UIScrollView
的子类别,提供了一个可以检视视图内容大于本身画面的一个很棒的方式。
往下阅读之前,先下载这个专案的原始档案,其中包括了我们要在这篇教学中使用的所有档案。
以程式建立滚动视图
滚动视图就跟其他视图的建立方式一样,不是以程式来建立就是在介面建构器中建立,只要稍微设置一下,就可以完成滚动的功能。
滚动视图就跟其他视图一样是以插进去一个控制器或视图阶层(view hierarchy)中来建立。只要两个步骤就可以完成滚动视图的设置:
- 你必须要设定
contentSize
属性为滚动内容的大小。这里指定滚动区域的大小。 - Y你必须加入可以显示以及可以让滚动视图滚动的视图。这些视图主要作为内容的呈现。
你可以帮你的程式任意设置视觉上的提示,像是垂直与水平滚动指示器、拖曳反弹以及滚动的方向限制。
我们会以程式来建立滚动视图来开始。从你下载的专案档案打开ScrollViewDemo专案。里面包含了一个简单专案,其中在Storyboard加上一个single view controller,并连结上专案中所建立的ScrollViewController类别。我也加入了一张我们待会会用到名称为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
scrollView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
scrollView.addSubview(imageView)
view.addSubview(scrollView)
}
上面的程式建立了一个滚动视图与一个图像视图。图像视图设为滚动视图的子视图。contentSize
设定滚动区域的大小。我们设定跟图像视图(2000×1500)大小相同。我们设定跟图像视图(2000×1500)大小相同。我们也设定滚动视图的背景颜色为黑色,所以图像会有个黑色背景,我们设定滚动视图的autoresizingMask
为.FlexibleWidth
与.FlexibleHeight
所以当装置旋转时,会重新调整大小。执行这个App,你应该能够滚动并看见图片的其他部分。
当你执行App,你可能会注意到显示的部分图像是原来图片的左上角。
这是因为滚动视图边界原点设为(0,0),也就是左上角,如果你想要变更App启动后图像内容显示的位置,你必须变更视图的边界原点,因为设定这个位置在处理滚动视图时很常见,UIScrollView
有一个contentOffset
属性,可以设定跟改变边界原点一样效果。
将以下的陈述贴至该行后面,设定滚动视图的autoresizingMask
.
scrollView.contentOffset = CGPoint(x: 1000, y: 450)
再次执行App,你会见到滚动视图,已经移动并显示照片其他的部分。所以你可以决定当视图载入后,所要呈现的部分。
缩放
我们已经加入滚动视图,可以让使用者滚动浏览超过画面大小的图片部分。这很棒,但是如果使用者可以放大缩小这会更有用。
想要支援缩放功能,你必须为你的滚动视图设定一个代理(delegate)。这个代理物件必须遵循UIScrollViewDelegate协定(protocol)。该代理类别必须实作viewForZoomingInScrollView()
方法并回传要缩放的视图。
你也必须要指定使用者可以缩放的量。只要设定滚动视图的minimumZoomScale
与maximumZoomScale
的属性值。两者预设值皆为1.0。
修改ScrollViewController 类别定义如下所示。
class ScrollViewController: UIViewController, UIScrollViewDelegate {
然后加入以下的函式至类别。
func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
return imageView
}
然后在viewDidLoad()
底下加入以下几行。
scrollView.delegate = self
scrollView.minimumZoomScale = 0.1
scrollView.maximumZoomScale = 4.0
scrollView.zoomScale = 1.0
在以上的程式中我们设定zoomScale
为1.0并指定最小以及最大的缩放因子。执行这个App,一开始会有相同的缩放因子,如前所示( zoomScale
of 1.0)。当你以手指缩放视图,便可以将其往上或往下滚动至它的最大以及最小缩放因子。我们设定maximumZoomScale
为4.0,maximumZoomScale,因此你可以将图片放大四倍,不过结果并不是太好,因为尺寸比原来大上许多,所以变得有点模糊。我们在下一步将其变更回原来1.0的预设值。
以上,我们设定minimumZoomScale
为0.1,结果图片变得非常小,画面上留下了许多空白。在横向模式,图片旁空白的区域变得更大。我们想要让这个图片「纵横比相符并显示(Aspect Fit)」在滚动视图中,所以它可以在显示全图像的情况下尽可能占满较多的空间。
要这么做的话,我们将会使用滚动视图与图像视图比来计算最小缩放因子。
首先从viewDidLoad()
移除以下三个陈述。
scrollView.minimumZoomScale = 0.1
scrollView.maximumZoomScale = 4.0
scrollView.zoomScale = 1.0
加入以下的函式至类别中,我们取得宽度与高度比,并挑选最小的值,设定它为最小缩放比,注意我已经移除maximumZoomScale
的设定,所以它被设定为预设值1.0。
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
}
然后在viewDidLoad()
底部呼叫这个函式
setZoomScale()
同样的,加入以下这段程式,这可以让使用者将装置转向后,做缩放时图像会跟着调整比例。
override func viewWillLayoutSubviews() {
setZoomScale()
}
执行这个App,现在当你将图像缩小,图像会布满画面较多的空间,并显示全部画面。
从上面的图片你可以注意到,滑动视图的内容,也就是图片本身,位于画面的左上方,我们要把它置于画面中心。
加入以下的函式至类别中。
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)
}
这个函式会在每次缩放后被呼叫。它告诉代理滑动视图的缩放因子已经变更。在以上的程式中,我们计算会应用在图片周边的padding/inset来将其置中。对于上方与底部的值,我们检查图像的高度是否小于滚动视图的高度,如果是的话是设定其留白(padding)值为两个视图差的一半,否则的话就设定其值为0。针对水平留白部分也做同样的处理,然后我们设定视图的contentInset
。这是内容视图插入后与滚动视图周边的距离。
执行这个App,现在当你缩至最小比例时,内容应该已经能够置中了。
按下做缩放
基本的UIScrollView 类别只要加上一点程式就可以支援手指缩放手势(pinch in与pinch out)。但是如果要使用侦测手指按下手势来支援更丰富的缩放体验,则需要多一点的工作才能完成。
iOS使用者介面指南(iOS Human Interface Guidlines)定义了点两下(double-tap)来缩放。不过这有些条件:就是视图单一方向的缩放(像是照片App 一样,在相片上按两下会放大至最大比例,然后再按两下则缩回最小比例),或者连续点击会放到最大,达到之后则会回到全萤幕视图。但是一些应用在面对按下缩放功能时需要更多弹性的处理方式,一个例子是地图应用程式。地图App支援点两下放大,再按两下放更大,如果要缩小,则使用两只手指靠在一起来逐渐将地图缩小。
为了让你的程式支援缩放功能,需要在类别中实作触控事件的处理,也就是UIScrollView的代理方法viewForZoomingInScrollView()
的回传。该类别会负责追踪画面上手指数,以及按下数。当它侦测按一下,按两下,或者两只手指触控,它会做出相对的反应。假如是按两下,以及两只手指触控的事件,它可以用程式指定适当的因子来缩放滚动视图。
针对我们的App,我们将会实作按两下来放到最大比例,反之如果在最大放大比例下按两下,则可以缩到最小,跟照片App类似。
加入以下的函式至类别中。
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()
在以上的程式,我们加入手势控制器至scrollview,这辨识使用者是否有按两下,然后我们依照目前的缩放程度来处理放大或缩小的需求。
执行这个App,你应该能够按两下来放大或缩小了。
在介面建构器中建立滚动视图
在介面建构器实作滚动视图比直接写在程式中还来得简单许多。使用Storyboard只有几个步骤就可以完成跟刚刚所建立的一样内容。
在Main.storyboard,拖曳另一个视图控制器至画面中。并设定它为初始视图控制器(initial view controller),你可以将Storyboard Entry Point箭头拖曳至该控制器,或者在新的视图控制器的属性检阅器(Attributes Inspector)中勾选Is Initial View Controller
来设定。
A加入一个滚动视图至视图控制器的视图,并定位其所有的边缘,让它能够充满整个画面。
然后加上一个图像视图(Image View)至Scroll View,并定位其所有边缘对齐滚动视图。
记得一个滚动视图需要知道其内容大小,当你设定图像视图的图像,它的大小会被用来作为滚动视图的内容大小。
在属性检阅器,选取image.png 作为图像视图的图片。透过更新frame来解决所有Auto Layout的问题。只要几个步骤且不需要撰写任何一行程式,执行App后,你将得到跟之前一样的滚动视图。你可以看一下属性检阅器,选取Scroll View,看看里面有什么属性,譬如你可以设定它的最小与最大缩放比例。
要缩放的话,你还是需要实作跟前面一样的viewForZoomingInScrollView()
d代理方法。这里我不打算介绍,因为跟前面一段会有所重复。这表示,倘若你需要更多滚动视图的功能,你还是需要写些程式。
巢状滚动视图
要将一个滚动视图嵌入另一个滚动视图是可行的。嵌入的方式可以是相同或者交叉方向来嵌入。这篇教学的所需要的程式是参考专案模板中的NestedScrollViews 。
相同方向的滚动
相同方向的滚动是当一个UIScrollView的子视图在UIScrollView间以相同方向来滚动。你可以在主滚动视图的视图中加入另一个区块,来将部分资料做个别的呈现。你也可以使用相同方向滚动视图来达到一些像是视差特效。在我们的范例App中,我们会使用相同方向的滚动,并针对两个不同的滚动视图设定不同的滚动速度。这样会造成两个视图有视差的视觉效果。
打开NestedScrollViews专案的Storyboard,你应该会见到一个视图控制器上的主视图内有两个滚动视图。这些滚动视图有Background 与Foreground的ID,Background的滚动视图上面有一个图像视图,其边缘都与滚动视图对齐,没有留白。这个图像已经设为image.png,两个滚动视图与该视图间都无间距。
在Foreground 滚动视图,有一些标签加在上面以及一个容器视图(container view),这些标签只是让我们在执行App时有一个具备内容的滚动视图。这个容器视图将会在下一段中的交叉方向滚动中用到。倘若你想要较长尺寸的视图控制器,你可以选取视图控制器,然后至尺寸检阅器中,变更Simulated Size 为Freeform,就可以设定其尺寸。在这个范例,我将高度提高为1,200。这只是协助你在处理视图时,如果需要更多空间在视图控制器上呈现的作法。这并不会影响App的执行。这个对于在你正在布局的元件,譬如滚动视图下半部的元素,有能可在执行App时会超出视图外的处理很方便。
既然UI已经设定好了,视差特效的建立将会变快许多。首先执行App,并注意Foreground的滚动,Background还是保持不动。
我们会先建立两个滚动视图的outlet。打开助理编辑器(Assistant Editor),并建立一个outlet给Background 滚动视图,并命名为background,对于Foreground的,则命名为foreground。你应该在ViewController.swift 中有以下的程式。
@IBOutlet weak var background: UIScrollView!
@IBOutlet weak var foreground: UIScrollView!
我们需要知道何时foreground视图已经滚动,所以我们才可以计算背景背景视图应该滚动的量,然后使用这个值来滚动它。这里我们会使用UIScrollViewDelegate 方法。
变更ViewController 类别宣告如下。
class ViewController: UIViewController, UIScrollViewDelegate {
将以下这行加至viewDidLoad()
下方。我们只关心foreground,所以我们不会设定background的代理。
foreground.delegate = self
将以下函式加进类别中。
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的高度,并计算foreground滚动多少量。取得这个值之后,我们将它乘上background的高度,使用它来设定background的contentOffset
如此一来,在每一次foreground滚动时,可以让background比foreground跑得快一点。执行这个App,你将会见到这个视差特效。
交叉方向滚动
交叉方向滚动是表示另一个滚动视图的子视图与滚动视图呈现90度的交叉滚动,接下来我们要来建立它。
在NestedScrollViews 专案中,你会在Foreground滚动视图中注意到一个容器视图。这也是水平滚动视图置放之处。
加上一个视图控制器至介面建构器中。按住Control键,并从容器视图中拖曳至新增的视图控制器,并选取embed segue
。在选取完这个视图控制器之后,至尺寸检阅器,并变更其Simulated Size为Freeform ,并设定它的高度为128。128是我们容器视图的尺寸,所以我们设定这个值为模拟视图(simulated view)的尺寸,这样有助于我们来看最后滚动视图的样貌。视图控制器如下所示。
拖曳一个滚动视图至视图控制器并定位边缘如下所示。
然后加入一个视图至滚动视图,并在尺寸检阅器(Size Inspector)设定其尺寸为70×70。将它放到滚动视图的左侧,然后复制这个视图布满整个滚动视图,视图间的间距不需要太准确,如下图所示。我变更了视图的Background Color 为淡灰色,让它有所区分。
选取最左边的视图,并将其定位在上方以及左侧,另外也加入Height与Width的约束条件。
选取最右边的视图,并定位它的上方以及右侧,并加入Width与Height的约束条件。
在文件大纲中,选取Scroll View并做以下选取,Editor > Resolve Auto Layout Issues > All Views > Add Missing Constraints。这会加入约束条件至其他视图,执行这个App,你应该能够见到跟前面一样的垂直滚动,但是在容器视图,你也可以水平滚动内容,如下例所示,我设定视图控制器的Background Color为Clear Color
本篇的教学就到尾声了,我们没有介绍所有滚动视图的内容,但是我希望这篇文章可以协助你设计滚动视图。