在iOS中,滾動視圖(scroll view)是用來瀏覽無法在整個畫面容下的其他內容。滾動視圖有兩個主要用途:
- 提供使用者拖曳至他們想要呈現的內容區域
- 提供使用者使用手指縮放手勢來對所呈現的內容放大或縮小
在iOS App的常見控制 – UITableView – 是一個UIScrollView
的子類別,提供了一個可以檢視視圖內容大於本身畫面的一個很棒的方式。
本篇教學中,我們會來看各種UIScrollView的觀念、其中包括以程式建立一個滾動視圖與介面建構器(Interface Builder)、滾動(scrolling)與縮放(zooming)、以及巢狀滾動視圖(nested scroll views)。
往下閱讀之前,先 下載這個專案的原始檔案, 其中包括了我們要在這篇教學中使用的所有檔案。
以程式建立滾動視圖
滾動視圖就跟其他視圖的建立方式一樣,不是以程式來建立就是在介面建構器中建立,只要稍微設置一下,就可以完成滾動的功能。
滾動視圖就跟其他視圖一樣是以插進去一個控制器或視圖階層(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)設定其尺寸為 70x70。將它放到滾動視圖的左側,然後複製這個視圖佈滿整個滾動視圖,視圖間的間距不需要太準確,如下圖所示。我變更了視圖的Background Color 為淡灰色,讓它有所區分。
選取最左邊的視圖,並將其定位在上方以及左側,另外也加入Height與Width的約束條件。
選取最右邊的視圖,並定位它的上方以及右側,並加入Width與Height的約束條件。
在文件大綱中,選取Scroll View並做以下選取,Editor > Resolve Auto Layout Issues > All Views > Add Missing Constraints。這會加入約束條件至其他視圖,執行這個App,你應該能夠見到跟前面一樣的垂直滾動,但是在容器視圖,你也可以水平滾動內容,如下例所示,我設定視圖控制器的Background Color為Clear Color
本篇的教學就到尾聲了,我們沒有介紹所有滾動視圖的內容,但是我希望這篇文章可以協助你設計滾動視圖。針對其他更多有關滾動視圖的主題,你可以進一步閱讀滾動視圖程式設計指南.
為了讓您方便參考,你可以在這裏下載這兩個完整專案檔.
原文: A Beginner’s Guide to UIScrollView