CoreGraphics系列三:pattern和transparency layer

這篇文章將介紹如何在視圖的背景上繪製重複的 pattern,爲多個layer繪製一個陰影。

這篇文章基於前兩篇文章CoreGraphics系列一:pathCoreGraphics系列二:gradient和context。如果你對CoreGraphics還不熟悉,可以先查看前面兩篇文章。這篇文章所使用的demo從CoreGraphics系列二:gradient和context的結尾開始。

這篇文章主要涉及以下幾點:

  • 爲背景創建重複 pattern。
  • 繪製一個獎牌,獎勵喝八杯水的用戶。

1. 創建重複 pattern

這一部分使用UIKit提供的方法創建背景 pattern:

1.1 設置背景視圖

創建繼承自UIView的自定義視圖BackgroundView,將其設置爲ViewController的root view。更新BackgroundView如下:

import UIKit

class BackgroundView: UIView {
    
    // 暫時使用以下顏色和大小,其可以清晰看出繪製內容。
    let lightColor: UIColor = .orange
    let darkColor: UIColor = .yellow
    let patternSize: CGFloat = 200

    // Only override draw() if you perform custom drawing.
    // An empty implementation adversely affects performance during animation.
    override func draw(_ rect: CGRect) {
        // Drawing code
        guard let context = UIGraphicsGetCurrentContext() else {
            fatalError("\(#function): \(#line) Failed to get current context")
        }
        
        context.setFillColor(darkColor.cgColor)
        // 未設置path時,fill會填充整個rect。
        context.fill(rect)
    }
}

1.2 繪製三角形

下面使用UIBezierPath繪製下圖中的黃色三角形,數字表示代碼中點的順序。

BackgroundView.swift文件的draw(_:)方法底部添加以下代碼:

        let drawSize = CGSize(width: patternSize, height: patternSize)
        
        // Insert code here
        
        let trianglePath = UIBezierPath()
        // 1
        trianglePath.move(to: CGPoint(x: drawSize.width / 2.0, y: 0))
        trianglePath.addLine(to: CGPoint(x: 0, y: drawSize.height / 2.0))
        trianglePath.addLine(to: CGPoint(x: drawSize.width, y: drawSize.height / 2.0))
        
        // 4
        trianglePath.move(to: CGPoint(x: 0, y: drawSize.height / 2.0))
        // 5
        trianglePath.addLine(to: CGPoint(x: drawSize.width / 2.0, y: drawSize.height))
        // 6
        trianglePath.addLine(to: CGPoint(x: 0, y: drawSize.height))
        
        // 7
        trianglePath.move(to: CGPoint(x: drawSize.width, y: drawSize.height / 2.0))
        // 8
        trianglePath.addLine(to: CGPoint(x: drawSize.width / 2.0, y: drawSize.height))
        // 9
        trianglePath.addLine(to: CGPoint(x: drawSize.width, y: drawSize.height))
        
        lightColor.setFill()
        trianglePath.fill()

上述代碼使用同一 path,繪製了三個三角形。move(to:)就像提筆移動到另一點一樣。

運行後效果如下:

目前,直接向 view 的 context 繪製內容。想要重複該 pattern,必須在該 context 之外創建 image,然後使用該 image 在背景視圖的 context 中繪製重複 pattern。

Insert code here行後添加以下代碼:

        // Insert code here
        // 創建圖片上下文。
        UIGraphicsBeginImageContextWithOptions(drawSize, true, 0.0)
        // 獲取上面創建上下文的引用。
        guard let drawingContext = UIGraphicsGetCurrentContext() else {
            fatalError("\(#function):\(#line) Failed to get current context.")
        }

        // Set the fill color for the new context.
        darkColor.setFill()
        drawingContext.fill(CGRect(x: 0, y: 0, width: drawSize.width, height: drawSize.height))

如果此時再次運行demo,會發現三角形不見了。這是因爲UIGraphicsBeginImageContextWithOptions(_:_:_:)創建了一個新的 context,並被設置爲當前的繪製上下文。因此,目前的繪製操作都繪製到了新創建的 context 中。

UIGraphicsBeginImageContextWithOptions(_:_:_:)方法參數如下:

  • size:context的大小。UIGraphicsGetImageFromCurrentImageContext()返回圖片大小由該參數決定。如果想要獲得圖片像素大小,需乘以參數scale值。
  • opaque:位圖是否是不透明的。如果位圖是不透明的,設置爲true可以忽略alpha通道,優化位圖存儲空間;false表示位圖必須包含alpha通道,用來處理半透明。
  • scale:位圖的 scale。如果設置爲0.0,則採用設備 main screen 的 scale。

1.3 從context中創建圖片

draw(_:)底部添加以下代碼:

        // 從當前context中提取圖片
        guard let image = UIGraphicsGetImageFromCurrentImageContext() else {
            fatalError("""
            \(#function):\(#line) Failed to \
            get an image from current context.
            """)
        }
        // 關閉context後,context將恢復到視圖自身context,所有繪製操作都會直接在視圖中進行。
        UIGraphicsEndImageContext()

下面使用從context中創建的image,創建重複 pattern。在draw(_:)方法底部添加以下代碼:

        UIColor(patternImage: image).setFill()
        context.fill(rect)

運行後效果如下:

目前,重複 pattern 各項功能都已完成,可以將其顏色、大小修改爲以下值,即可得到文章開頭部分的背景:

    let lightColor = UIColor(red: 255.0 / 255.0, green: 255.0 / 255.0, blue: 242.0 / 255.0, alpha: 1.0)
    let darkColor = UIColor(red: 223.0 / 255.0, green: 255.0 / 255.0, blue: 247.0 / 255.0, alpha: 1.0)
    let patternSize = 30

2. 繪製圖片

這一部分將繪製一個獎牌,獎勵一天喝夠八杯水的用戶。

這裏將在 playground 中繪製獎牌,繪製完成後再將代碼複製到工程中。

2.1 創建 playground

創建 playground,設置其名稱爲MedalDrawing

在 playground 中添加以下代碼:

import UIKit

let size = CGSize(width: 120, height: 200)

UIGraphicsBeginImageContextWithOptions(size, false, 0.0)

guard let context = UIGraphicsGetCurrentContext() else {
    fatalError("\(#function):\(#line) Failed to get current context.")
}

// 從當前context中創建圖片
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

與創建 pattern 圖片一樣,這裏也創建了繪製上下文。

點擊UIGraphicsGetImageFromCurrentImageContext()右側的矩形,可以看到目前context中圖形。

2.2 繪製先後次序

繪製圖片前需先確定好繪製順序。獎牌繪製順序應如下圖所示:

  1. 後面的絲帶,即紅絲帶。
  2. 獎牌垂飾。
  3. 搭扣。
  4. 藍絲帶。
  5. 數字1。

整個繪製過程中,從 context 中獲取圖片、關閉上下文的代碼應始終處於最底部。

首先,定義繪製過程中用到的顏色:

// Gold colors
let darkGoldColor = UIColor(red: 0.6, green: 0.5, blue: 0.15, alpha: 1.0)
let midGoldColor = UIColor(red: 0.86, green: 0.73, blue: 0.3, alpha: 1.0)
let lightGoldColor = UIColor(red: 1.0, green: 0.98, blue: 0.9, alpha: 1.0)

2.3 紅絲帶

下面繪製紅絲帶:

// 紅絲帶
let lowerRibbonPath = UIBezierPath()
lowerRibbonPath.move(to: CGPoint(x: 0, y: 0))
lowerRibbonPath.addLine(to: CGPoint(x: 40, y: 0))
lowerRibbonPath.addLine(to: CGPoint(x: 78, y: 70))
lowerRibbonPath.addLine(to: CGPoint(x: 38, y: 70))
lowerRibbonPath.close()
UIColor.red.setFill()
lowerRibbonPath.fill()

上述代碼創建了 path 並填充。可以在UIGraphicsGetImageFromCurrentImageContext()預覽區看到繪製的紅絲帶。

2.4 搭扣

下面繪製搭扣:

// 搭扣
let claspPath = UIBezierPath(roundedRect: CGRect(x: 36, y: 62, width: 43, height: 20), cornerRadius: 5)
claspPath.lineWidth = 5
darkGoldColor.setStroke()
claspPath.stroke()

這裏使用UIBezierPath(rounedRect:cornerRadius:)創建搭扣的弧線。

2.5 垂飾

下面繪製垂飾:

// 獎牌垂飾
let medallionPath = UIBezierPath(ovalIn: CGRect(x: 8, y: 72, width: 100, height: 100))
//context.saveGState()
//medallionPath.addClip()

let colors = [
    darkGoldColor.cgColor,
    midGoldColor.cgColor,
    lightGoldColor.cgColor
] as CFArray
guard let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: colors, locations: [0, 0.51, 1]) else {
    fatalError("""
        Failed to instantiate an instance \
        of \(String(describing: CGGradient.self))
        """)
}
context.drawLinearGradient(gradient, start: CGPoint(x: 40, y: 40), end: CGPoint(x: 40, y: 162), options: [])
//context.restoreGState()

繪製圖片如下:

想要漸變成一定角度,如從左上角到右下角,可通過調整end point的 x 座標實現,如下:

context.drawLinearGradient(gradient, start: CGPoint(x: 40, y: 40), end: CGPoint(x: 100, y: 160), options: [])

取消繪製漸變中註釋掉的saveGState()addClip()restoreGState()。漸變會被限制在圓形區域中。另外,因爲在 clip 前保存了context,clip後又restore了context,當前的 context 沒有被裁切。

繪製獎牌垂飾內環時,可以使用垂飾圓弧,只需在繪製前對 path 進行縮放即可。

繼續添加以下代碼,繪製內環:

// 創建transform
// 縮放、向右下平移
var transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
transform = transform.translatedBy(x: 15, y: 30)
medallionPath.lineWidth = 2.0

// 應用transform
medallionPath.apply(transform)
medallionPath.stroke()

2.6 藍絲帶

使用以下代碼繪製藍絲帶:

// 藍絲帶
let upperRibbonPath = UIBezierPath()
upperRibbonPath.move(to: CGPoint(x: 68, y: 0))
upperRibbonPath.addLine(to: CGPoint(x: 108, y: 0))
upperRibbonPath.addLine(to: CGPoint(x: 78, y: 70))
upperRibbonPath.addLine(to: CGPoint(x: 38, y: 70))
upperRibbonPath.close()

UIColor.blue.setFill()
upperRibbonPath.fill()

這一部分與繪製紅絲帶類似,創建path並填充即可。

2.7 數字1

最後需要繪製的是數字1:

// 數字1
let numberOne = "1" as NSString // 必現是NSString類型,否則無法使用draw(in:)
let numberOneRect = CGRect(x: 47, y: 100, width: 50, height: 50)
guard let font = UIFont(name: "Academy Engraved LET", size: 60) else {
    fatalError("""
      \(#function):\(#line) Failed to instantiate font \
      with name \"Academy Engraved LET\"
      """)
}
let numberOneAttributes = [
    NSAttributedString.Key.font: font,
    NSAttributedString.Key.foregroundColor: darkGoldColor
]
numberOne.draw(in: numberOneRect, withAttributes: numberOneAttributes)

如果添加上陰影,使其更有立體感就更美觀了。

2.8 Transparency Layer 和陰影

繪製陰影需要三元素:顏色、偏移量和模糊。

在定義顏色後、繪製紅絲帶前添加以下代碼,繪製陰影:

// 陰影
let shadow = UIColor.black.withAlphaComponent(0.80)
let shadowOffset = CGSize(width: 2.0, height: 2.0)
let shadowBlurRadius: CGFloat = 5

context.setShadow(offset: shadowOffset, blur: shadowBlurRadius, color: shadow.cgColor)

上述代碼繪製了陰影,但其效果可能不是我們期望的:

向當前context繪製圖像時,setShadow(offset:blur:color:)會爲每個圖像創建陰影。

獎牌由五個對象構成,會產生五個陰影。可以將所有對象組合到一個透明的layer上,這樣就只會繪製一個陰影了。

在創建陰影后添加以下代碼,將所有layer組合到一個透明圖層:

// 開啓transparency layer
context.beginTransparencyLayer(auxiliaryInfo: nil)

開啓 group 後,也需要關閉group。在從context中獲取圖片前添加以下代碼:

// 關閉transparency layer
context.endTransparencyLayer()

現在,獎牌添加了陰影,更顯得有立體感:

2.9 添加到app中

目前,已經可以繪製獎牌。只需將其添加到demo中。

創建繼承自UIImageView的類MedalView,將其添加到counterView中。在MedalView.swift中添加以下方法:

    private func createMedalImage() -> UIImage {
        // 創建圖片時進行打印,這樣就可以知道何時創建了圖片。
        debugPrint("Creating Medal Image")
    }

將 playground 中所有繪製代碼複製到createMedalImage()方法中,並將最後兩行代碼替換成以下代碼:

        // 從當前context中創建圖片
        guard let image = UIGraphicsGetImageFromCurrentImageContext() else {
            fatalError("""
              \(#function):\(#line) Failed to get an \
              image from current context.
              """)
        }
        UIGraphicsEndImageContext()
        
        return image

MedalView類頂部添加以下lazy屬性:

    lazy var medalImage = createMedalImage()

使用lazy修飾的屬性在首次調用時纔會創建。使用lazy修飾一些昂貴操作,能夠提升性能。用戶喝到8杯水時,繪製一次獎牌;如果一直沒有喝到8杯,則永不創建。

添加以下代碼顯示、隱藏獎牌:

    func showMedal(show: Bool) {
        image = (show == true) ? medalImage : nil
    }

最後,將其添加到counterView上,並且在ViewController.swiftviewDidLoadpushButtonPressed(_:)方法中根據counter決定是否展示獎牌。如果遇到問題,可以在文章底部獲取源碼查看。

完成後,最終效果如下:

總結

這篇文章介紹瞭如何在視圖的背景上繪製重複的 pattern,爲多個layer繪製一個陰影。如果你在繪製過程中遇到了問題,可以通過下面的鏈接獲取源碼查看。

Demo名稱:CoreGraphics3
源碼地址:https://github.com/pro648/BasicDemos-iOS/tree/master/CoreGraphics-3

參考資料:

  1. Core Graphics Tutorial: Patterns and Playgrounds

歡迎更多指正:https://github.com/pro648/tips

本文地址:https://github.com/pro648/tips/blob/master/sources/CoreGraphics系列三:pattern和transparency%20layer.md

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章