AGCircularPicker源码解析②:渐变圆环渲染与像素级取色的隐藏技巧
【免费下载链接】AGCircularPickerAGCircularPicker is helpful component for creating a controller aimed to manage any calculated parameter项目地址: https://gitcode.com/gh_mirrors/ag/AGCircularPicker
本文是 AGCircularPicker 源码解析的第二篇。AGCircularPicker 是一款面向 iOS 的轻量圆形选择器(circular picker)组件,本篇拆解它的两大核心技术:渐变圆环渲染与像素级取色——看它如何用一张带角度的渐变图裁出圆形,又如何"抠"出手指所在位置的精确颜色,新手也能看懂。
一、渲染渐变圆环:「渐变图 + 圆形遮罩」组合技 🎨
很多人渲染"渐变圆盘"的第一反应是直接用 CAGradientLayer。但 AGCircularPickerView 的做法是:先把渐变画成一张 UIImage,再用遮罩裁成圆形。这条渲染链的核心在AGCircularPicker/Classes/AGCircularPickerView.swift的setupView()(L122-L166)。
第一步:按指定角度绘制线性渐变图
入口是给 UIImage 增加的类方法(见AGCircularPicker/Classes/AGCircularPickerExtensions.swiftL175-L196):
let degree: CGFloat = (angle + 90) * .pi / 180 let center = CGPoint(x: size.width / 2, y: size.height / 2) let startPoint = CGPoint(x: center.x - cos(degree) * size.width / 2, y: center.y - sin(degree) * size.height / 2) let endPoint = CGPoint(x: center.x + cos(degree) * size.width / 2, y: center.y + sin(degree) * size.height / 2) context.drawLinearGradient(gradient, start: startPoint, end: endPoint, options: [])这里有两个小机关:
+ 90是角度偏移:当gradientAngle传 0 时,渐变恰好是"从上到下",符合直觉;- 起止点由一个角度通过三角函数算出,旋转渐变时中心点始终不变,所以只改一个角度数字,整条渐变就跟着转。
方法的四个参数colors / locations / size / angle分别是:渐变色数组、各色对应的位置(0~1)、绘制尺寸和旋转角度。
第二步:用图层遮罩把渐变裁成圆形
draw(_:)(L168-L184)先用四段三次贝塞尔曲线拼出一个完整的圆(L310-L344,控制点距离是radius * 0.552——0.552 正是近似 1/4 圆弧的经典魔数),按当前角度旋转后,做了一件关键的事:
let shapeMask = CAShapeLayer() shapeMask.path = ellipsePath.cgPath super.layer.mask = shapeMask把圆形路径设为super.layer的遮罩后,渐变图、旋钮等所有子层会一起被圆形裁剪,只露出圆内那一块渐变。这种"遮罩"写法比直接用 CAGradientLayer 更灵活——mask 可以换成任何路径,环、弧线、异形都能做。
第三步:加上可拖动的白色旋钮
knobPath(rect:)(L304-L308)在圆的顶部画一个直径 9pt 的小圆,knobLayer填充纯白置顶。拖动时旋钮跟随手指绕圆转动,就是选择器的"指针"。
二、像素级取色:用 1×1 上下文「抠出手指所在的颜色」🔍
整个项目最隐蔽的技巧是:拖动旋钮时,标题文字颜色会实时变成渐变在该处的颜色。项目没有手动插值计算渐变色,而是直接对屏幕上的像素"取色"。
如何从 CALayer 中读出单个像素
实现是 CALayer 扩展的colorOfPoint(point:)(见AGCircularPicker/Classes/AGCircularPickerExtensions.swiftL202-L224),核心就这几行:
let pixel = UnsafeMutablePointer<CUnsignedChar>.allocate(capacity: 4) let context = CGContext(data: pixel, width: 1, height: 1, bitsPerComponent: 8, bytesPerRow: 4, space: colorSpace, bitmapInfo: bitmapInfo.rawValue) context!.translateBy(x: -correctedPoint.x, y: -correctedPoint.y) render(in: context!)思路有点反直觉,但很优雅:
- 创建一个1×1 的离屏上下文,它的 4 字节内存正是一个像素的 RGBA;
translateBy平移坐标系,让目标像素恰好落在上下文原点;render(in:)把整棵图层树重新渲染进这个 1×1 上下文——被"压"进内存的,就是目标像素的 RGB+A;- 读出
pixel[0...3]除以 255,即得到 UIColor。
还有一个细节值得学习:取色前先把点用min/max夹紧到边界内缩 2 点的位置,拖到边缘也不会越界读取。
两个关键调用点:初始化与拖动时
- 初始化完成后(L159-L165):算出旋钮位置,调用
gradientImageView.layer.colorOfPoint(point:),把初始颜色通过circularPickerViewDidEndSetupWith交给 delegate; - 拖动过程中(
handleTouchesL203-L224):先把触点钳制到圆环边缘内,然后
currentColor = gradientImageView.layer.colorOfPoint(point: correctedPoint) delegate?.circularPickerViewDidChangeValue(currentValue.round(minValue), color: currentColor ?? .black, index: index)注意取色对象是gradientImageView.layer,即渐变图层本身,所以取到的颜色是"纯渐变色",不会被白色旋钮或标题干扰。
这个技巧的好处是"所见即所得":无论渐变有几段色、位置怎么分布、角度多大,取到的颜色都与用户肉眼看到的像素完全一致,一行插值公式都不用写。代价是render(in:)会触发一次重绘,因此它适合单点采样,不适合批量扫图。
三、自定义实战:3 行代码换颜色、位置与角度 ⚙️
上面渲染用的三个参数,全部收敛在一个结构体里(见AGCircularPicker/Classes/AGCircularPickerOption.swiftL105-L151):
| 参数 | 作用 | 默认值 |
|---|---|---|
gradientColors | 渐变色数组 | 青 (0,237,233) → 蓝 (0,135,217) → 紫 (138,28,195) |
gradientLocations | 每种颜色的位置(0~1) | 均匀分布 |
gradientAngle | 渐变旋转角度(度) | 0 |
只传颜色数组时,位置会自动均分;传角度,渐变随之旋转。官方示例Example/AGCircularPicker/MultiPickerViewController.swift中,hours 轮盘用gradientAngle: 20、minutes 用gradientAngle: -20,相邻两个轮盘就形成了镜像渐变:
let hourColorOption = AGCircularPickerColorOption( gradientColors: [hourColor1, hourColor2, hourColor3], gradientAngle: 20)UI 侧只需在 delegate 回调里用取到的颜色更新视图。示例Example/AGCircularPicker/SinglePickerViewController.swift的做法:
func circularPickerViewDidChangeValue(_ value: Int, color: UIColor, index: Int) { titleLabel.text = String(format: "%02d", value) titleLabel.textColor = color }四、本篇总结
| 技巧 | 位置 | 一句话要点 |
|---|---|---|
| 按角度画线性渐变图 | AGCircularPickerExtensions.swiftL175-L196 | 一个角度决定渐变方向 |
| 圆形遮罩裁剪 | AGCircularPickerView.swiftL179-L181 | layer.mask赋任意路径即可裁出任意形状 |
| 1×1 上下文取色 | AGCircularPickerExtensions.swiftL202-L224 | 把图层重渲进单个像素,得到真实颜色 |
| 手指位置取色回调 | AGCircularPickerView.swiftL203-L224 | 所见即所得,免去手动插值 |
下一篇会讲解AGCircularPickerExtensions.swift中的向量与角度数学扩展:如何算出两点间的夹角,并把"角度 ↔ 数值"的换算完整闭环。
【免费下载链接】AGCircularPickerAGCircularPicker is helpful component for creating a controller aimed to manage any calculated parameter项目地址: https://gitcode.com/gh_mirrors/ag/AGCircularPicker
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考