检查、修改和构建 PencilKit 绘图
Inspect, modify, and construct PencilKit drawings
2020年6月25日
一句话判断
PencilKit 终于开放了对绘图内容的程序化访问和修改能力——如果你的笔记或绘图应用还在用”黑盒”方式处理 PKDrawing,这场 Session 会改变你的开发思路。
这场 Session 讲了什么
iOS 14 对 PencilKit 做了重大升级,核心变化是引入了 PKStroke、PKStrokePath 和 PKInk 等新类型,让开发者可以逐笔画地读取和修改绘图数据。在此之前,PKDrawing 对开发者来说基本是个不透明的容器——你能显示它、能序列化它,但无法访问其中的具体笔画内容。
Session 详细介绍了新的 strokes API。每个 PKStroke 包含一条 PKStrokePath(笔画的点序列)和一个 PKInk(笔刷类型和颜色)。你可以遍历 drawing 中的所有 stroke,检查它们的属性,甚至创建全新的 stroke 来程序化生成绘图内容。这意味着你可以实现诸如笔画颜色批量替换、图形识别后的智能修正、导出为自定义矢量格式等高级功能。
除了 stroke 级别的操作,Session 还演示了如何利用新 API 构建更智能的绘图工具。例如,你可以检测用户画的是不是近似直线或圆形,然后自动吸附为标准图形;或者在用户完成标注后,用程序生成高亮区域。
值得深挖的点
PKStrokePath 的点数据结构
PKStrokePath 由一系列 PKStrokePoint 组成,每个点携带 x/y 坐标、时间戳、压力值和仰角(azimuth)信息。这些数据足够精确地还原 Apple Pencil 的完整输入轨迹。对于做手写识别或签名验证的开发者来说,这些原始数据是金矿。值得注意的是,点数据的坐标系统与 PKCanvasView 的 bounds 对应,所以做坐标变换时要注意 scale 和 offset。
程序化构建绘图的实际应用场景
Session 展示了几个很有启发性的用例:模板填充(在预设位置生成标准图形)、批注辅助(检测用户意图后自动生成标注框)、以及绘图数据导出(将 PencilKit 数据转换为 SVG 或自定义格式)。这些场景此前只能靠截图或私有 API 实现,现在终于有了官方途径。
代码片段
import PencilKit
// 遍历 PKDrawing 中的所有笔画并分析
func analyzeDrawing(_ drawing: PKDrawing) {
for (index, stroke) in drawing.strokes.enumerated() {
let ink = stroke.ink
print("笔画 \(index): 工具=\(ink.tool), 颜色=\(ink.color)")
// 获取笔画路径中的所有点
let path = stroke.path
print(" 包含 \(path.count) 个采样点")
// 计算笔画的边界框
if let firstPoint = path.first,
let lastPoint = path.last {
print(" 起点坐标: (\(firstPoint.x), \(firstPoint.y))")
print(" 终点坐标: (\(lastPoint.x), \(lastPoint.y))")
}
}
}
// 程序化创建新的笔画并添加到绘图中
func createHighlightStroke(in drawing: PKDrawing,
rect: CGRect) -> PKDrawing {
// 创建高亮笔刷(黄色半透明)
let highlightInk = PKInk(.pen, color: UIColor.yellow.withAlphaComponent(0.3))
// 构建矩形高亮区域的点序列
var points: [PKStrokePoint] = []
let corners = [
CGPoint(x: rect.minX, y: rect.minY),
CGPoint(x: rect.maxX, y: rect.minY),
CGPoint(x: rect.maxX, y: rect.maxY),
CGPoint(x: rect.minX, y: rect.maxY),
CGPoint(x: rect.minX, y: rect.minY),
]
for corner in corners {
points.append(PKStrokePoint(
location: corner,
timeOffset: 0,
size: CGSize(width: 20, height: 20), // 笔刷粗细
opacity: 0.3,
force: 1.0,
azimuth: CGFloat.pi / 4,
altitude: CGFloat.pi / 4
))
}
// 创建笔画路径和笔画
let path = PKStrokePath(controlPoints: points, creationDate: Date())
let newStroke = PKStroke(ink: highlightInk, path: path)
// 将新笔画添加到已有绘图中
var existingStrokes = drawing.strokes
existingStrokes.append(newStroke)
return PKDrawing(strokes: existingStrokes)
}
// 批量替换绘图中的颜色
func replaceColor(in drawing: PKDrawing,
from originalColor: UIColor,
to newColor: UIColor) -> PKDrawing {
let newStrokes = drawing.strokes.map { stroke -> PKStroke in
// 检查颜色是否匹配(近似比较)
if stroke.ink.color.isApproximatelyEqual(to: originalColor) {
// 用新颜色创建相同的笔刷和路径
let newInk = PKInk(stroke.ink.tool, color: newColor)
return PKStroke(ink: newInk, path: stroke.path)
}
return stroke
}
return PKDrawing(strokes: newStrokes)
}
最佳实践
- 在处理大量笔画数据时注意性能,
strokes数组的 map/filter 操作会创建新的PKDrawing实例,建议批量操作而非逐笔修改 - 保存自定义绘图数据时,可以同时保留原始
PKDrawing的序列化数据和你的解析结果,以便未来兼容 - 使用
PKStrokePoint的时间戳信息可以实现笔画回放动画,但要注意时间偏移是相对于笔画起点的 - 在 Apple Pencil 之外也考虑手指输入场景,
force值在手指输入时可能为 0
还有什么值得关注
PKInk的 tool 类型包括.pen、.pencil、.marker,每种工具的渲染效果不同,程序化生成时选择合适的类型很重要- Session 提到 Scribble 功能(全系统手写输入识别)底层也依赖类似的笔画数据结构,虽然 Scribble API 本身不直接暴露笔画级别操作
- 新的 stroke API 与
PKCanvasView的 tool picker 配合使用时,开发者可以在工具切换时做自定义的笔画后处理