這篇文章將介紹如何在視圖的背景上繪製重複的 pattern,爲多個layer繪製一個陰影。
這篇文章基於前兩篇文章CoreGraphics系列一:path和CoreGraphics系列二: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。
整個繪製過程中,從 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.swift
的viewDidLoad
和pushButtonPressed(_:)
方法中根據counter決定是否展示獎牌。如果遇到問題,可以在文章底部獲取源碼查看。
完成後,最終效果如下:
總結
這篇文章介紹瞭如何在視圖的背景上繪製重複的 pattern,爲多個layer繪製一個陰影。如果你在繪製過程中遇到了問題,可以通過下面的鏈接獲取源碼查看。
Demo名稱:CoreGraphics3
源碼地址:https://github.com/pro648/BasicDemos-iOS/tree/master/CoreGraphics-3
參考資料:
歡迎更多指正:https://github.com/pro648/tips
本文地址:https://github.com/pro648/tips/blob/master/sources/CoreGraphics系列三:pattern和transparency%20layer.md