IOS的UIScrollView滚动视图入门

本篇教学中,我们会来看各种UIScrollView的观念、其中包括以程式建立一个滚动视图与介面建构器(Interface Builder)、滚动(scrolling)与缩放(zooming)、以及巢状滚动视图(nested scroll views )。


在iOS中,滚动视图(scroll view)是用来浏览无法在整个画面容下的其他内容。滚动视图有两个主要用途:

  • 提供使用者拖曳至他们想要呈现的内容区域
  • 提供使用者使用手指缩放手势来对所呈现的内容放大或缩小

在iOS App的常见控制– UITableView –是一个UIScrollView的子类别,提供了一个可以检视视图内容大于本身画面的一个很棒的方式。

UIScrollView Guide

往下阅读之前,先下载这个专案的原始档案,其中包括了我们要在这篇教学中使用的所有档案。

以程式建立滚动视图

滚动视图就跟其他视图的建立方式一样,不是以程式来建立就是在介面建构器中建立,只要稍微设置一下,就可以完成滚动的功能。

滚动视图就跟其他视图一样是以插进去一个控制器或视图阶层(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,你应该能够滚动并看见图片的其他部分。

scroll view demo #1

当你执行App,你可能会注意到显示的部分图像是原来图片的左上角。

scroll view demo #2

这是因为滚动视图边界原点设为(0,0),也就是左上角,如果你想要变更App启动后图像内容显示的位置,你必须变更视图的边界原点,因为设定这个位置在处理滚动视图时很常见,UIScrollView有一个contentOffset属性,可以设定跟改变边界原点一样效果。

将以下的陈述贴至该行后面,设定滚动视图的autoresizingMask.

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

再次执行App,你会见到滚动视图,已经移动并显示照片其他的部分。所以你可以决定当视图载入后,所要呈现的部分。

scroll view image demok

缩放

我们已经加入滚动视图,可以让使用者滚动浏览超过画面大小的图片部分。这很棒,但是如果使用者可以放大缩小这会更有用。

想要支援缩放功能,你必须为你的滚动视图设定一个代理(delegate)。这个代理物件必须遵循UIScrollViewDelegate协定(protocol)。该代理类别必须实作viewForZoomingInScrollView()方法并回传要缩放的视图。

你也必须要指定使用者可以缩放的量。只要设定滚动视图的minimumZoomScalemaximumZoomScale的属性值。两者预设值皆为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,一开始会有相同的缩放因子,如前所示( zoomScaleof 1.0)。当你以手指缩放视图,便可以将其往上或往下滚动至它的最大以及最小缩放因子。我们设定maximumZoomScale为4.0,maximumZoomScale,因此你可以将图片放大四倍,不过结果并不是太好,因为尺寸比原来大上许多,所以变得有点模糊。我们在下一步将其变更回原来1.0的预设值。

image03

以上,我们设定minimumZoomScale为0.1,结果图片变得非常小,画面上留下了许多空白。在横向模式,图片旁空白的区域变得更大。我们想要让这个图片「纵横比相符并显示(Aspect Fit)」在滚动视图中,所以它可以在显示全图像的情况下尽可能占满较多的空间。

image04

要这么做的话,我们将会使用滚动视图与图像视图比来计算最小缩放因子。

首先从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,现在当你将图像缩小,图像会布满画面较多的空间,并显示全部画面。

image05

从上面的图片你可以注意到,滑动视图的内容,也就是图片本身,位于画面的左上方,我们要把它置于画面中心。

加入以下的函式至类别中。

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,现在当你缩至最小比例时,内容应该已经能够置中了。

image06

按下做缩放

基本的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加入一个滚动视图至视图控制器的视图,并定位其所有的边缘,让它能够充满整个画面。

image07

然后加上一个图像视图(Image View)至Scroll View,并定位其所有边缘对齐滚动视图。

image08

记得一个滚动视图需要知道其内容大小,当你设定图像视图的图像,它的大小会被用来作为滚动视图的内容大小。

在属性检阅器,选取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时会超出视图外的处理很方便。

image09

既然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,你将会见到这个视差特效。

sv01

交叉方向滚动

交叉方向滚动是表示另一个滚动视图的子视图与滚动视图呈现90度的交叉滚动,接下来我们要来建立它。

在NestedScrollViews 专案中,你会在Foreground滚动视图中注意到一个容器视图。这也是水平滚动视图置放之处。

加上一个视图控制器至介面建构器中。按住Control键,并从容器视图中拖曳至新增的视图控制器,并选取embed segue。在选取完这个视图控制器之后,至尺寸检阅器,并变更其Simulated Size为Freeform ,并设定它的高度为128。128是我们容器视图的尺寸,所以我们设定这个值为模拟视图(simulated view)的尺寸,这样有助于我们来看最后滚动视图的样貌。视图控制器如下所示。

image10

拖曳一个滚动视图至视图控制器并定位边缘如下所示。

image11

然后加入一个视图至滚动视图,并在尺寸检阅器(Size Inspector)设定其尺寸为70×70。将它放到滚动视图的左侧,然后复制这个视图布满整个滚动视图,视图间的间距不需要太准确,如下图所示。我变更了视图的Background Color 为淡灰色,让它有所区分。

image12

选取最左边的视图,并将其定位在上方以及左侧,另外也加入Height与Width的约束条件。

image13

选取最右边的视图,并定位它的上方以及右侧,并加入Width与Height的约束条件。

image14

在文件大纲中,选取Scroll View并做以下选取,Editor > Resolve Auto Layout Issues > All Views > Add Missing Constraints。这会加入约束条件至其他视图,执行这个App,你应该能够见到跟前面一样的垂直滚动,但是在容器视图,你也可以水平滚动内容,如下例所示,我设定视图控制器的Background Color为Clear Color

scroll view demo

本篇的教学就到尾声了,我们没有介绍所有滚动视图的内容,但是我希望这篇文章可以协助你设计滚动视图。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,408评论 4 371
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,690评论 2 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,036评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,726评论 0 221
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,123评论 3 296
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,037评论 1 225
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,178评论 2 318
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,964评论 0 213
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,703评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,863评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,333评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,658评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,374评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,195评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,988评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,167评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,970评论 2 279

推荐阅读更多精彩内容

  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    X先生_未知数的X阅读 15,937评论 3 118
  • 01 最近认识了一个男孩,姑且就叫其阿康吧。 初识阿康是在一个社交软件上,我也是闲来无聊发个状态,结果引来一群围观...
    路人小A阅读 413评论 2 1
  • 有雨的深夜,像极了我此刻的心情,笼罩在无边的黑暗中,看不到希望,亦看不清自己。看着窗外密密的雨帘,思绪无边蔓延,抓...
    时间成行李的旧情人阅读 582评论 1 1
  • 简述 代码 NSInvocationOperation基本使用-(void)demo1{//1.封装操作/* 第一...
    LitterL阅读 386评论 4 2
  • 他从我的青春走过,让我痴迷沉醉,不知归路使;使我我坠入深渊,但我不后悔,因为我爱他,为了他我愿舍弃一切,所以,...
    夜千鸢阅读 216评论 1 1