AGCircularPicker源码解析②:渐变圆环渲染与像素级取色的隐藏技巧
2026/8/25 10:15:54 网站建设 项目流程

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.swiftsetupView()(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×1 的离屏上下文,它的 4 字节内存正是一个像素的 RGBA;
  2. translateBy平移坐标系,让目标像素恰好落在上下文原点;
  3. render(in:)把整棵图层树重新渲染进这个 1×1 上下文——被"压"进内存的,就是目标像素的 RGB+A;
  4. 读出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-L181layer.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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询