大家好,我是微微笑的蝸牛,??。
上一節(jié)我們講了如何確定節(jié)點(diǎn)布局信息,輸出了布局樹(shù)。今天,將介紹最后一個(gè)環(huán)節(jié),繪制。內(nèi)容不多,相對(duì)也比較好理解。
附上前面幾節(jié)的鏈接,想了解的童靴可以先看看。
- 聽(tīng)說(shuō)你想寫(xiě)個(gè)渲染引擎 - 前言
- 聽(tīng)說(shuō)你想寫(xiě)個(gè)渲染引擎 - html 解析
- 聽(tīng)說(shuō)你想寫(xiě)個(gè)渲染引擎 - css 解析
- 聽(tīng)說(shuō)你想寫(xiě)個(gè)渲染引擎 - 樣式樹(shù)
- 聽(tīng)說(shuō)你想寫(xiě)個(gè)渲染引擎 - 布局樹(shù)
整體流程
整個(gè)過(guò)程如下:
- 根據(jù)布局樹(shù)生成繪制命令列表
- 光柵化,生成像素點(diǎn)信息
- 將像素轉(zhuǎn)換為圖片
- 顯示到屏幕
繪制命令是個(gè)啥?它描述了你該如何繪制一個(gè)圖形,比如位置大小、顏色、形狀等等。
這里,我們只處理背景色和邊框的繪制,文字暫不涉及。所以只需支持矩形繪制,知道矩形的位置大小、顏色就好。
數(shù)據(jù)結(jié)構(gòu)
繪制命令,定義為枚舉類(lèi)型。暫且只支持矩形,關(guān)聯(lián)顏色和區(qū)域。
// 繪制命令,目前只支持顏色繪制
enum DisplayCommand {
case SolidColor(Color, Rect)
}
畫(huà)布,用來(lái)保存像素信息。
// 畫(huà)布
struct Canvas {
var width: Int
var height: Int
// 像素點(diǎn),argb
var pixels: [Color]
}
生成繪制命令
背景
背景的繪制命令比較好生成。從節(jié)點(diǎn)樣式表中取出背景色 background,再計(jì)算出背景區(qū)域就好。
背景區(qū)域包括「內(nèi)容區(qū)+內(nèi)邊距+邊框」。

代碼如下:
// 繪制背景
func renderBackground(list: inout [DisplayCommand], layoutBox: LayoutBox) {
// 獲取背景色
if let color = getColor(layoutBox: layoutBox, name: "background") {
// 背景包括 padding + border + content
let displayCommand = DisplayCommand.SolidColor(color, layoutBox.dimensions.borderBox())
list.append(displayCommand)
}
}
邊框
同樣,首先從樣式表中取出邊框顏色 border-color。
另外,邊框分為上下左右四個(gè)矩形區(qū)域,需分別計(jì)算出來(lái)。如下所示:

繪制命令的生成跟背景色差不多,只是注意下矩形區(qū)域的計(jì)算。
總繪制列表
遞歸遍歷布局樹(shù),將每個(gè)節(jié)點(diǎn)的繪制命令加入到數(shù)組,即可得到總繪制命令列表。
// 生成總體繪制列表
func buildDisplayList(layoutRoot: LayoutBox) -> [DisplayCommand] {
var list: [DisplayCommand] = []
renderLayoutBox(list: &list, layoutBox: layoutRoot)
return list
}
func renderLayoutBox(list: inout [DisplayCommand], layoutBox: LayoutBox) {
// 繪制背景
renderBackground(list: &list, layoutBox: layoutBox)
// 繪制邊框
renderBorder(list: &list, layoutBox: layoutBox)
// 遍歷子節(jié)點(diǎn),遞歸生成命令
for child in layoutBox.children {
renderLayoutBox(list: &list, layoutBox: child)
}
}
光柵化
將繪制命令轉(zhuǎn)換為像素信息。
繪制命令中包含顏色和區(qū)域,將該區(qū)域中每個(gè)點(diǎn)的色值寫(xiě)入像素?cái)?shù)組就好。
// 生成像素點(diǎn)
mutating func genPixel(color: Color, rect: Rect) {
// 將 rect 范圍內(nèi)的點(diǎn)填充為 color,不能超過(guò)畫(huà)布大小
// clamp 主要是用于限制范圍
let x0 = Int(rect.x.clamp(min: 0, max: Float(self.width)))
let y0 = Int(rect.y.clamp(min: 0, max: Float(self.height)))
let x1 = Int((rect.x + rect.width).clamp(min: 0, max: Float(self.width)))
let y1 = Int((rect.y + rect.height).clamp(min: 0, max: Float(self.height)))
// 遍歷所有點(diǎn),橫向一行行填充
for y in y0...y1 {
for x in x0...x1 {
let index = y * width + x
pixels[index] = color
}
}
}
生成圖片
得到像素信息數(shù)組后,使用 CoreGraphics 的 api 生成圖片。注意 color 的順序是 argb。
func imageFromARGB32Bitmap(pixels: [Color], width: Int, height: Int) -> UIImage? {
guard width > 0 && height > 0 else { return nil }
guard pixels.count == width * height else { return nil }
let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue)
let bitsPerComponent = 8
let bitsPerPixel = 32
var data = pixels // Copy to mutable []
guard let providerRef = CGDataProvider(data: NSData(bytes: &data,
length: data.count * MemoryLayout<PixelData>.size)
)
else { return nil }
guard let cgim = CGImage(
width: width,
height: height,
bitsPerComponent: bitsPerComponent,
bitsPerPixel: bitsPerPixel,
bytesPerRow: width * MemoryLayout<PixelData>.size,
space: rgbColorSpace,
bitmapInfo: bitmapInfo,
provider: providerRef,
decode: nil,
shouldInterpolate: true,
intent: .defaultIntent
)
else { return nil }
return UIImage(cgImage: cgim)
}
顯示
這步只需創(chuàng)建一個(gè) ImageView 將 image 顯示出來(lái)。
測(cè)試數(shù)據(jù)
測(cè)試數(shù)據(jù)在工程目錄下的 Example 下面,test.html 和 test.css。
效果如下圖所示:

完整代碼可點(diǎn)此查看:https://github.com/silan-liu/tiny-web-render-engine-swift。
總結(jié)
這一節(jié)主要介紹了繪制的相關(guān)操作,重點(diǎn)在于將布局信息生成繪制列表,然后進(jìn)行光柵化,轉(zhuǎn)換為像素點(diǎn)的過(guò)程。
至此,「聽(tīng)說(shuō)你想寫(xiě)個(gè)渲染引擎」系列已全部完結(jié),感謝您的閱讀~