Swift 程式語言

CAGradientLayer 教學:如何在 iOS App 製作漸層顏色

CAGradientLayer 教學:如何在 iOS App 製作漸層顏色
CAGradientLayer 教學:如何在 iOS App 製作漸層顏色
In: Swift 程式語言

每個開發者在設計程式介面時都會用上不同的顏色組合和圖像,務求製作出賣相更吸引的作品。在顏色配搭上,單色使用有時候不免顯得單調,而漸層 gradient 的使用或許可以帶來意想不到的效果。我曾經好幾次在設計時運用了漸層顏色,感覺這個題目值得跟大家討論一下,就是這樣這篇文章便「誕生」了。它在技術上使用簡單,開發者們一定會喜歡這個簡單又好看的功能。

那麼,怎樣才能既快速又輕鬆地設定漸層?這裡為大家提供三個方案。第一個,也是不太建議使用的方法,就是使用包含漸變效果的圖像。原因是缺乏靈活性,視覺上雖然不一定有分別,但對於開發者而言就不能隨意調整漸變度,若有更改就得重新上載圖像。第二個方案是使用 Core Graphics 技術,它比較適合進階開發者,對相關知識(例如圖形上下文、顏色空間等等)有一定程度的認知要求,對菜鳥來說恐怕會顯得艱深難明(除非你想越級挑戰咯)。最後來到第三個方案,一個既簡單又快捷的途徑:使用 CAGradientLayer 物件。

每一個視圖物件都包含了 CALayer 類別,而作為子類別的 CAGradientLayer 正正就是為了製作漸層效果而生。只需要簡單的四行程式碼便能應用,還有幾個屬性可為效果作出微調,而且絕大部份都能動畫化。下文會作詳細的分享,現在我們先看看layer 視圖,正是CAGradientLayer 類別實際上處理漸層效果的地方。使用 CAGradientLayer 的不足之處是暫不支援 radial 漸層效果,不過線性漸層已經足夠應付一般使用。

接著下來我會更詳細地說明每一個微調漸層效果的屬性。我將會使用兩種比較色彩鮮明的顏色來作出舉例,而只取用兩種顏色是因為要簡單明顯地展示效果。然而,只要學懂了技巧,要用上兩種以上的顏色作漸層也是可以的。

製作漸層

製作漸層是一個快捷簡單的工作,當中已包括一些指定的動作。事實上,你只需要設定少量屬性便能用上漸層效果。然而,微調漸層效果卻是更花時間的一部分,不同的值都會帶來不同的效果,總得花上些心神去尋找出最合適的配搭值,製作出更完美的效果。

我們將會逐步製作和討論漸層效果,但我們先需要一支程式來作範例。打開 Xcode 並建立一個新程式,確認選擇 Single View Application 範例。

現在我們需要建立一個新的專案,在專案導覽器點擊 ViewController.swift 檔案。在類別開首宣告以下屬性:

var gradientLayer: CAGradientLayer!

gradientLayer 屬性將會是我們的測試物件。在這個小小的實作中,我們將會通過以下簡單步驟在目標視圖中製作一個漸層:

  1. 初始化 CAGradientLayer 物件 (即是本範例的 gradientLayer)。
  2. 為漸層設定框架。
  3. 設定漸層顏色。
  4. 在目標視圖層增加漸層子層。

事實上,開始以上步驟前,需要配置的屬性還有很多,這個我們接著下來會再作研究。現在,我們先集中在這四個步驟。務求簡單的闡述,我們將會使用預設視圖 viewController 類別為目標視圖,並為它製作漸層顏色。

ViewController 類別建立一個新方法,為 gradientLayer 屬性作出初始化以及設定一些預設值:

func createGradientLayer() {
    gradientLayer = CAGradientLayer()

    gradientLayer.frame = self.view.bounds

    gradientLayer.colors = [UIColor.redColor().CGColor, UIColor.yellowColor().CGColor]

    self.view.layer.addSublayer(gradientLayer)
}

上列的程式碼,首先把 gradientLayer 物件作出初始化。然後為它設定框架,把它的邊界設定與視圖控制器濶度相等。接下來,我們要指定漸層效果選用的顏色,最後在預設視圖層增加漸層子層。

使用以下程式碼呼叫 viewWillAppear(_:) 方法:

override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)
    createGradientLayer()
}

現在試一下執行程式,可以得到以下的效果:

t53_1_first_gradient

只是用了四行程式碼便得到這樣的效果,不錯吧!讓我們再深入一點。

漸層顏色

儘管前面的程式碼十分簡單易用,但實際上是包含了一個重要的屬性:colors 屬性。首先,不用我多說也知道,沒有使用它來設定顏色,根本就不會顯示漸層。第二,這個屬性得用上顏色的 array(說實在,其實是 AnyObject 物件)而非 UIColor 物件;顏色必須是 CGColor 物件。在上述例子我只是使用了兩種顏色,但實際運用上是沒有限制,絕對是可以用上多色漸層,如下:

gradientLayer.colors = [UIColor.redColor().CGColor, UIColor.orangeColor().CGColor, UIColor.blueColor().CGColor, UIColor.magentaColor().CGColor, UIColor.yellowColor().CGColor]

執行程式可以得到這個效果:

t53_2_second_gradient

使用 colors 屬性的好處是它能動畫化,意思是可以調節漸層顏色而製造動態效果。來試試建立和使用一個顏色陣列集。我們將會為每一種顏色製作顏色集(顏色陣列),並在點擊觀看圖像時應用上,而每種顏色之間將以動態方式來轉換。

首先,在 ViewController 類別,gradientLayer屬性之下宣告這兩個新屬性:

var colorSets = [[CGColor]]()

var currentColorSet: Int!

colorSets 陣列的元素其實是CGColor物件。currentColorSet 指定了應用在漸層效果的顏色。

現在就製作顏色集。以下的程式碼只是舉例,你可以隨意選用自己喜歡的顏色:

func createColorSets() {
    colorSets.append([UIColor.redColor().CGColor, UIColor.yellowColor().CGColor])
    colorSets.append([UIColor.greenColor().CGColor, UIColor.magentaColor().CGColor])
    colorSets.append([UIColor.grayColor().CGColor, UIColor.lightGrayColor().CGColor])

    currentColorSet = 0
}

在以上的方法,除了為陣列設定用上的顏色,並將他們附加到colorSets陣列中,我們還需要為currentColorSet屬性設定初始值。

viewDidLoad()方法作出呼叫,讓程式執行以下程式碼:

override func viewDidLoad() {
    super.viewDidLoad()

    createColorSets()
}

我們還需要為 createGradientLayer() 方法作出少量改動。找出以下程式碼:

gradientLayer.colors = [UIColor.redColor().CGColor, UIColor.orangeColor().CGColor, UIColor.blueColor().CGColor, UIColor.magentaColor().CGColor, UIColor.yellowColor().CGColor]

並以下列程式碼取代之:

gradientLayer.colors = colorSets[currentColorSet]

現在通過在currentColorSet屬性裡指定的顏色將會被使用,而非預設的顏色。

我說過會簡單地通過點擊視圖去觸發顏色之間的過渡動畫。意思是我們需要增加一個點擊手勢辨識,前往viewDidLoad()作出處理:

override func viewDidLoad() {
    ...

    let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(ViewController.handleTapGesture(_:)))
    self.view.addGestureRecognizer(tapGestureRecognizer)
}

當視圖收到點擊手勢便呼叫handleTapGesture(_:)方法。現在還不行,我們將會建立它。要注意CABasicAnimation將會在以下程式碼中被使用,所以它可以把colors屬性動畫化。我先假設你對CABasicAnimation類別有一定程式的認知,否則你便需要上網先行做點功課。在這裡我會跳過某些屬性,務求以最簡單的方式去展示動畫效果。

func handleTapGesture(gestureRecognizer: UITapGestureRecognizer) {
    if currentColorSet < colorSets.count - 1 {
        currentColorSet! += 1
    }
    else {
        currentColorSet = 0
    }

    let colorChangeAnimation = CABasicAnimation(keyPath: "colors")
    colorChangeAnimation.duration = 2.0
    colorChangeAnimation.toValue = colorSets[currentColorSet]
    colorChangeAnimation.fillMode = kCAFillModeForwards
    colorChangeAnimation.removedOnCompletion = false
    gradientLayer.addAnimation(colorChangeAnimation, forKey: "colorChange")
}

起初我們決定了顏色的順序。當來到colorSets陣列中最後一種顏色時,我們將會回到最初(currentColorSet = 0),其餘我們則透過遞增currentColorSet屬性來作出轉變效果。

接下來的程式碼都是和動畫有關。最重要的是duration屬性,意思是動畫所花的時間,以及toValue設定colors(建立在CABasicAnimation類別的初始化規定)期望的最終值。另外兩個屬性能使動畫變化保留在層中,而不是恢復到原來的顏色。但,這不是永久的。我們需要在動畫完成時,明確地設置新的漸層顏色。這可以通過覆蓋下面的方法時,在 CABasicAnimation完成時呼叫出來:

override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
    if flag {
        gradientLayer.colors = colorSets[currentColorSet]
    }
}

以及增加handleTapGesture(_:)方法,讓上述方法成為實際效果:

func handleTapGesture(gestureRecognizer: UITapGestureRecognizer) {
    ...

    // Add this line to make the ViewController class the delegate of the animation object.
    colorChangeAnimation.delegate = self

    gradientLayer.addAnimation(colorChangeAnimation, forKey: "colorChange")
}

就是這樣。現在可以隨意調節動畫過渡的時間。我故意設定了兩秒時間,這樣漸層顏色的改變效果會更明顯:

t53_3_color_change

顏色位置

知道如何設定或改變漸層效果的顏色是漸層效果的基礎知識,但並不足以完全掌握它的功能。明白如何修改層的顏色覆蓋區域和覆蓋顏色的默認佈局作用也很大。

看看我們之前所建立的漸層效果,你會發現每一種顏色是默認佔據一半區域:

t53_1_first_gradient

透過更改CAGradientLayer類別提供的locations屬性可以作出修改。這個屬性跟據NSNumber物件作值,而每一個數字決定了每種顏色的起始位置。而且,很重要的,這些數字限制在0.0至1.0之間。

來看看例子便更清楚。到createGradientLayer()方法,並加入以下程式碼:

gradientLayer.locations = [0.0, 0.35]

重新執行程式便得到以下的漸層效果:

CALayer Gradient Color

第二種顏色起始的位置是基於我們設定在locations陣列的第二個值。我們也可以把第二種顏色設定為佔據層的65%區域(1.0-0.35 = 0.65)。作為一個預防措施,必須確認之前的一種顏色的位置不能大過後來的顏色,否則就會出現重叠,如下圖:

CAGradientLayer Location Overlap

若然你想試試以上的效果,可以把數值設定為 [0.5, 0.35]。

再深入一點,我們將為視圖加入一個點擊手勢識別器。這次我們需要兩根手指去進行點擊。在viewDidLoad()加入以下程式碼:

override func viewDidLoad() {
    ...

    let twoFingerTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(ViewController.handleTwoFingerTapGesture(_:)))
    twoFingerTapGestureRecognizer.numberOfTouchesRequired = 2
    self.view.addGestureRecognizer(twoFingerTapGestureRecognizer)
}

handleTwoFingerTapGesture(_:)方法中,我們為兩種顏色的位置建立了隨機值。然而,我們必須確定第一個位置值比第二個小。除此之外,新位置將會同時發送到控制台。如下:

func handleTwoFingerTapGesture(gestureRecognizer: UITapGestureRecognizer) {
    let secondColorLocation = arc4random_uniform(100)
    let firstColorLocation = arc4random_uniform(secondColorLocation - 1)

    gradientLayer.locations = [NSNumber(double: Double(firstColorLocation)/100.0), NSNumber(double: Double(secondColorLocation)/100.0)]

    print(gradientLayer.locations!)
}

當兩根手指同時點擊程式,將會顯示以下的效果:

t53_7_change_locations

要注意,locations屬性並沒有預設值,所以要小心使用時引發潛在的問題導致程式意外彈出。

漸層方向

現在已經明白如果處理漸層的colors屬性,接著我們就了解一下漸層效果director。初學者們可以先看看這個截圖:

t53_1_first_gradient

圖中一看便清楚知道漸層效果是由上而下,而這是所有漸層的default顏色方向。同樣,我們可以作出微調,讓漸層做出不同方向的效果。

CAGradientLayer類別提供兩種屬性,可以指定漸層發生的方向:

  • startPoint
  • endPoint

CGPoint值可以分配到以上任何一個的屬性,而xy兩個值必須介乎0.0至1.0之間。實際上,startPoint描述了第一種顏色的起始坐標,而endPoint描述了最後一種顏色的結束坐標。然而,這裡有一個重要的細節:坐標存在於運作系統坐標空間之內。

什麼意思?

看看下列數字可有助理解:

t53_8_iPhone_image

在iOS,zero點(起始點)坐落於螢幕的左上角 (x = 0.0,y = 0.0),完結點在左下角 (x = 1.0, y = 1.0)。任何的坐標的點,即是xy必須介乎0.0至1.0之間。

以上的坐標與其他操作系統的處理不一樣。在Mac的Textedit舉個例子吧:

t53_9_mac_window

這裡的起始點是左下角,而完結點在右上角,與iOS的坐標空間有所不同。

在預設情況下,當endPoint相等於 (0.5, 1.0),startPoint 則相等於 (0.5, 0.0) 點。要注意x值是保持不變,但y值就由0.0(頂)開始並在1.0(底)完結,這樣便能設定方向。如果你想看到更大的分別,可以到createGradientLayer()方法:

func createGradientLayer() {
    ...

    gradientLayer.startPoint = CGPointMake(0.0, 0.5)
    gradientLayer.endPoint = CGPointMake(1.0, 0.5)
}

這個例子中x值由0.0換成1.0,而y則保持不變。這樣會得出向右的漸層效果:

t53_10_gradient_right

要完全明白漸層方向,可以對上列的程式碼作出修改,把xy的設定值調整至0.0-1.0之間的任何一個數值。現在,我們將會為範例增添一些漸層方向的新功能:為視圖增加Pan手勢,依照手指在螢幕上移動的手勢去改變漸層方向。以下的方向都能夠用上:

  • 朝上
  • 朝下
  • 朝右
  • 朝左
  • 由左上至右下
  • 由右上至左下
  • 由左下至右上
  • 由右下至左上

回到專案,在enum建立代表方向的指令:

enum PanDirections: Int {
    case Right
    case Left
    case Bottom
    case Top
    case TopLeftToBottomRight
    case TopRightToBottomLeft
    case BottomLeftToTopRight
    case BottomRightToTopLeft
}

然後,在ViewController類別宣告新屬性去指引漸層方向:

var panDirection: PanDirections!

panDirection屬性跟據手指移動動作得到相應的值。我們將會處理一個兩個步驟的工作:最初我們將決定所需方向及分配至以上屬性。然後,當pan手勢完成時,從相應的startPointendPoint屬性值決定漸層方向。

在開始之前,我們需要建立一個新的手勢辨識器物件,並將它加入視圖中。因此,來到viewDidLoad()方法,加入以下程式碼:

override func viewDidLoad() {
    ...

    let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(ViewController.handlePanGestureRecognizer(_:)))
    self.view.addGestureRecognizer(panGestureRecognizer)
}

在實作handlePanGestureRecognizer(_:),我們將會使用手勢辨識器屬性velocity。當 velocity 任何方向點大於300.0(xy),方向都會被考慮在內。邏輯上很容易理解:首要條件是檢查水平軸上的velocity,然後才檢查縱軸上的velocity,便能取得方向。可以看看以下程式碼和註釋:

func handlePanGestureRecognizer(gestureRecognizer: UIPanGestureRecognizer) {
    let velocity = gestureRecognizer.velocityInView(self.view)

    if gestureRecognizer.state == UIGestureRecognizerState.Changed {
        if velocity.x > 300.0 {
            // 方向朝右的例子
            // 以下是垂直移動手勢的具體實例

            if velocity.y > 300.0 {
                // 由左上移動至右下
                panDirection = PanDirections.TopLeftToBottomRight
            }
            else if velocity.y < -300.0 {
                // 由左下移動至右上
                panDirection = PanDirections.BottomLeftToTopRight
            }
            else {
                // 朝右移動
                panDirection = PanDirections.Right
            }
        }
        else if velocity.x < -300.0 {
            // 方向朝左的例子
            // 以下是垂直移動手勢的具體實例

            if velocity.y > 300.0 {
                // 由右上移動至左下
                panDirection = PanDirections.TopRightToBottomLeft
            }
            else if velocity.y < -300.0 {
                // 由右下移動至左上
                panDirection = PanDirections.BottomRightToTopLeft
            }
            else {
                // 朝左移動
                panDirection = PanDirections.Left
            }
        }
        else {
            // 垂直移動例子(朝上或朝下)

            if velocity.y > 300.0 {
                // 朝下移動
                panDirection = PanDirections.Bottom
            }
            else if velocity.y < -300.0 {
                // 朝上移動
                panDirection = PanDirections.Top
            }
            else {
                // 沒有移動
                panDirection = nil
            }
        }
    }
    else if gestureRecognizer.state == UIGestureRecognizerState.Ended {
        changeGradientDirection()
    }
}

這裡有兩點需要注意(進一步決定pan手勢方向)::

  1. 若果沒有其他條件被滿足,panDirection 將會是nil。
  2. Changed手勢狀態決定方向。當手勢完成時,changeGradientDirection()方法會被呼叫出來,panDIrection()屬性的值決定新的方向。

以下的方法相當簡單,只需要在漸層上為startPointendPoint屬性設定適當的值。看看xy如何從手勢方向取得值:

func changeGradientDirection() {
    if panDirection != nil {
        switch panDirection.rawValue {
        case PanDirections.Right.rawValue:
            gradientLayer.startPoint = CGPointMake(0.0, 0.5)
            gradientLayer.endPoint = CGPointMake(1.0, 0.5)

        case PanDirections.Left.rawValue:
            gradientLayer.startPoint = CGPointMake(1.0, 0.5)
            gradientLayer.endPoint = CGPointMake(0.0, 0.5)

        case PanDirections.Bottom.rawValue:
            gradientLayer.startPoint = CGPointMake(0.5, 0.0)
            gradientLayer.endPoint = CGPointMake(0.5, 1.0)

        case PanDirections.Top.rawValue:
            gradientLayer.startPoint = CGPointMake(0.5, 1.0)
            gradientLayer.endPoint = CGPointMake(0.5, 0.0)

        case PanDirections.TopLeftToBottomRight.rawValue:
            gradientLayer.startPoint = CGPointMake(0.0, 0.0)
            gradientLayer.endPoint = CGPointMake(1.0, 1.0)

        case PanDirections.TopRightToBottomLeft.rawValue:
            gradientLayer.startPoint = CGPointMake(1.0, 0.0)
            gradientLayer.endPoint = CGPointMake(0.0, 1.0)

        case PanDirections.BottomLeftToTopRight.rawValue:
            gradientLayer.startPoint = CGPointMake(0.0, 1.0)
            gradientLayer.endPoint = CGPointMake(1.0, 0.0)

        default:
            gradientLayer.startPoint = CGPointMake(1.0, 1.0)
            gradientLayer.endPoint = CGPointMake(0.0, 0.0)
        }
    }
}

假若panDiretion是nil,就不會有任何動作。

現在執行一下程式並依照支援的手勢方向移動指頭,漸層會隨著你的移動改變方向。

t53_11_direction_change_2

結語

來到最後,希望我能夠讓大家了解CAGradientLayer使用相當方便,以程式編寫去製作漸層效果。透過適當組合屬性的值讓漸層有更多的變化,得到更好的漸層效果。而動畫效果更是一大賣點。這個題目相對是顯淺易明,我相信你也能夠做得到並開始嘗試在作品中添加漸層顏色。

完整的範例專案,請到GitHub.com下載。

譯者簡介:小秘,業餘博客,翻譯新手,最愛睡覺,喜歡旅遊。

原文Creating Gradient Colors Using CAGradientLayer

作者
Gabriel Theodoropoulos
資深軟體開發員,從事相關工作超過二十年,專門在不同的平台和各種程式語言去解決軟體開發問題。自2010年中,Gabriel專注在iOS程式的開發,利用教程與世界上每個角落的人分享知識。可以在Google+或推特關注 Gabriel。
評論
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。