开始

接下来我们将绘制一个调色盘控件。当你点击调色板,调色板上的颜色将被循环选中。效果如下图所示:

绘制界面

首先声明一些常量:

class ColorSwatchView: UIView {    // MARK: Constantsstruct Constants {static let angleSpan: CGFloat = CGFloat(22).toRadian()static let startAngle: CGFloat = CGFloat(114).toRadian()static let slotDiameter:CGFloat = 40static let slotBorderWidth:CGFloat = 5}

toRadian 是一个 CGFloat 的扩展方法:

extension CGFloat {func toRadian() -> CGFloat {return self/180*CGFloat.pi}
}

ColorSwatchView 就是组件的名称,我们把要用到的常量都定义在一个结构体内,方便统一维护。

然后定义私有变量:

 // MARK: Variablesprivate var selectedColor: PresetColor = .skyprivate var colors:[PresetColor] = []

colors 用于表示 6 个小圆中的颜色,selectedColor 用于表示当前选中的颜色。PresetColor 是我们定义的一个枚举,其中指定了一些预设的颜色:

public enum PresetColor: Int, CaseIterable {case rosecase skycase dawncase sagecase duskcase clearpublic func color() -> UIColor {switch self {case .rose:return UIColor(red: 0xf7/255, green: 0xda/255, blue: 0xe0/255, alpha: 1)//"f7dae0"case .sky:return UIColor(red:0xca/255, green: 0xdc/255, blue: 0xe4/255, alpha: 1)//"cadce4"case .dawn:return UIColor(red:0xf1/255, green: 0xe6/255, blue: 0xb2/255, alpha: 1)//"f1e6b2"case .sage:return UIColor(red: 0xc5/255, green: 0xd7/255, blue: 0xc3/255, alpha: 1)//"c5d7c3"case .dusk:return UIColor(red: 0xd3/255, green: 0xcc/255, blue: 0xd8/255, alpha: 1)//"d3ccd8"case .clear:return UIColor(red: 0xe9/255, green: 0xeb/255, blue: 0xea/255, alpha: 1)//"E9EBEA"}}
}

然后定义 3 个懒加载属性:

    // MARK: Lazy Propertieslazy var slotShadow: NSShadow! = {let shadow = NSShadow()shadow.shadowColor = UIColor(white: 0.1, alpha: 0.2)shadow.shadowOffset = CGSize(width: 0, height: 3)shadow.shadowBlurRadius = 10return shadow}()    lazy var circleLayer: CAShapeLayer = {let x = Constants.slotDiameter/2let rect = bounds.inset(by: UIEdgeInsets(top: x, left:x, bottom: x, right: x))let shapeLayer = CAShapeLayer()shapeLayer.frame = rectshapeLayer.fillColor = UIColor.clear.cgColorshapeLayer.path = UIBezierPath(ovalIn: shapeLayer.bounds).cgPathshapeLayer.backgroundColor = UIColor.clear.cgColorreturn shapeLayer}()lazy private var swatchLayer: CALayer = {let swatchLayer = CALayer()swatchLayer.frame = boundsreturn swatchLayer}()

slotShadow 定义了小圆阴影。

circleLayer 是一个 CAShapeLayer,用来绘制大圆。

swatchLayer 也是一个 CAShapeLayer,用来作为 6 个小圆的 super layer。

然后是构造函数:

    // MARK: Life Cycleoverride init(frame: CGRect) {super.init(frame: frame)commonInit()}required init?(coder aDecoder: NSCoder) {super.init(coder: aDecoder)commonInit()}fileprivate func commonInit() {layer.addSublayer(circleLayer)layer.addSublayer(swatchLayer)}

在 commonInit 中,我们添加了 circleLayer 和 swatchLayer。这会导致图层的绘制。

然后定义个私有方法,用来绘制一个小圆:

    private func colorSlotLayer(_ isSelect: Bool) -> CAShapeLayer {// 1let slotLayer = CAShapeLayer()slotLayer.frame = bounds// 2let borderWidth = isSelect ? Constants.slotBorderWidth : 0// 3slotLayer.strokeColor = UIColor.white.cgColorslotLayer.lineWidth = borderWidth// 4let w = Constants.slotDiameter+borderWidthlet rect = CGRect(x: bounds.midX-w/2, y:bounds.minY, width: w, height: w)// 5slotLayer.path = UIBezierPath(ovalIn: rect).cgPathslotLayer.backgroundColor = UIColor.clear.cgColor// 6if isSelect {slotLayer.shadowOffset = slotShadow.shadowOffsetslotLayer.shadowColor = (slotShadow.shadowColor as! UIColor).cgColorslotLayer.shadowRadius = slotShadow.shadowBlurRadiusslotLayer.shadowOpacity = 1}return slotLayer}
  1. 构建一个 ShapeLayer,并设置其 frame 为父 layer 大小。
  2. borderWidth 需要根据当前选中的状态而定,因为对于当前选中的颜色,我们需要在绘制小圆时绘制白边和阴影。
  3. 设置白边的线宽和颜色。当然,如果不是当前选中的颜色,线宽为 0,也就没有白边。
  4. 因为描边时默认情况下,边线的中轴会对齐几何图形的边缘,这样导致有一半线宽会占据圆内的面积,从而使得圆面积在描边的情况下“缩小”。为了避免这种情况,我们需要在描边时,把圆半径扩大一点。
  5. 用贝塞尔曲线绘制圆。同时设置默认图层背景色。
  6. 如果当前小圆代表的是当前选中颜色,那么还要绘制阴影。

目前 colors 和 selectedColor 是私有的,我们需要定义一个公共方法,允许从外部改变两个私有变量,并绘制图层:

func setData(colors: [PresetColor]?, selected: PresetColor) {// 1if let colors = colors {self.colors = colors}// 2selectedColor = selected// 3circleLayer.fillColor = selected.color().cgColorif !self.colors.isEmpty {// 4swatchLayer.sublayers?.forEach({ $0.removeFromSuperlayer() })// 5for (i,color) in self.colors.enumerated() {// 6let slotLayer = colorSlotLayer(color==selected)slotLayer.fillColor = color.color().cgColor// 7swatchLayer.anchorPoint = CGPoint(x: 0.5, y: 0.5)// 8let angle = Constants.startAngle-Constants.angleSpan*CGFloat(i)// 9slotLayer.transform = CATransform3DRotate(CATransform3DIdentity, angle, 0, 0, -1)// 10swatchLayer.addSublayer(slotLayer)}}}
  1. 修改 colors 数组。
  2. 修改 selectedColor。
  3. 将大圆颜色改为选定颜色。
  4. 将 swatchLayer 中原来的 sublayer 全部移除。
  5. 遍历 colors 数组,绘制多个小圆(一个颜色一个)。
  6. 调用 colorSlotLayer 方法生成一个 CAShapeLayer。同时修改图层的 fillColor 为对应的颜色。
  7. 设置锚点为图层的中心,等会我们将以锚点为中心旋转。注意 CATranform 旋转有一个限制,它只能在 CALayer 的 bounds 内旋转,因此别忘了设置 CALayer 的 bounds。
  8. 计算旋转的角度。第一个圆从 startAngle 开始,后面的圆依次递减 22 度。
  9. 旋转。
  10. 添加 slotLayer 到 swatchLayer 中。

触摸事件的响应

    // MARK: Touches Handles    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {if touches.count == 1 {changeColor()}    }func changeColor() {if selectedColor == colors[colors.count-1] {// 1setData(colors: nil, selected: colors[0]) }else if let index = colors.firstIndex(of: selectedColor) { // 2  setData(colors: nil, selected: colors[index+1]) }    }
  1. 如果当前颜色已经是 colors 列表中最后一个颜色,那么又从头开始循环。
  2. 否则,选择下一个颜色。

通过点击大圆循环切换颜色固然是好,但是如果能够直接点击颜色小圆来切换颜色岂非更好?只不过这需要涉及更多的工作。

首先,修改 setData 方法,将其中 slotLayer 的 3d 旋转过程删除,因为我们会将旋转动作从 CALayer 移到 UIPath 上,这样方便我们对每个小圆的触摸位置进行判断:

        for (i,color) in self.colors.enumerated() {let slotLayer = colorSlotLayer(color==selected,index: i)if let color = UIColor(hexString: color.hexString()) {slotLayer.fillColor = color.cgColor}swatchLayer.addSublayer(slotLayer)}

然后,在 colorSlotLayer() 方法中,对 UIPath 进行旋转,旋转的角度根据新增的参数 index 进行计算:

 // 1private func colorSlotLayer(_ isSelect: Bool, index: Int) -> CAShapeLayer { // 1......let path = UIBezierPath(ovalIn: rect)// 2path.apply(CGAffineTransform(translationX: -bounds.width/2, y: -bounds.height/2))path.apply(CGAffineTransform(rotationAngle: rotateAngle(slotIndex: index)))path.apply(CGAffineTransform(translationX: bounds.width/2, y: bounds.height/2))slotLayer.path = path.cgPath......
  1. 增加了 index 参数,即 colors 数组中每个元素的索引。
  2. Path 旋转时默认是从路径的原点(0,0)为中心旋转,所以在旋转之前先将它移动到整个 view 的中心,然后在旋转,选转完之后又将它移动回原来的距离。

旋转角度的计算也非常简单,调用的是 rotateAngle() 方法:

    private func rotateAngle(slotIndex i: Int) -> CGFloat {return -(Constants.startAngle-Constants.angleSpan*CGFloat(i))}

最后是 touchesEnded 方法,在其中调用 CGPath 的 contains 方法对触摸点的坐标进行判断,看它是否是位于某个小圆的 path 内:

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {let location = touches.first?.location(in: self)if let swatchLayers = swatchLayer.sublayers, let point = location {for (i, slot) in swatchLayers.enumerated() {if let shapeLayer = slot as? CAShapeLayer, let path = shapeLayer.path {if path.contains(point) {selectedColor = colors[i]sendActions(for: .touchUpInside)// touch inside of the slotreturn}}}}sendActions(for: .touchUpOutside)// touch outside of the slot}

ok,这样就大功告成了。当你点击调色板中的小圆时,大圆的背景色即 selectColor 属性会随之改变。

7-调色板-CALayer和触摸相关推荐

  1. CALayer( 一 )

    一 CALayer是什么? 摘自官网的一句话-Layers Provide the Basis for Drawing and Animations(Layers是绘图和动画的基础) Layer是在3 ...

  2. IOS开发CALayer隐式动画

    2019独角兽企业重金招聘Python工程师标准>>> 每一个自定义的layer都存在默认的隐私动画,隐式动画默认为1/4秒 @interface DYViewController ...

  3. iOS边练边学--CALayer,非根层隐式动画,钟表练习

    一.CALayer UIView之所以能显示在屏幕上,完全是因为他内部的一个图层 在创建UIView对象时,UIView内部会自动创建一个图层(即CALayer对象),通过UIView的layer属性 ...

  4. iOS开发UI篇—CAlayer(创建图层)

    一.添加一个图层 添加图层的步骤: 1.创建layer 2.设置layer的属性(设置了颜色,bounds才能显示出来) 3.将layer添加到界面上(控制器view的layer上)  1 // 2 ...

  5. 移动开发:iphone开发之触摸事件详解

    转:http://blog.sina.com.cn/s/blog_8988732e01012eaf.html iPhoneOS中的触摸事件基于多点触摸模型.用户不是通过鼠标和键盘,而是通过触摸设备的屏 ...

  6. iOS - CALayer 绘图层

    1.CALayer 绘图层 在 iOS 系统中,你能看得见摸得着的东西基本上都是 UIView,比如一个按钮.一个文本标签.一个文本输入框.一个图标等等,这些都是 UIView.其实 UIView 之 ...

  7. [iOS Animation]-CALayer 变换

    变换 很不幸,没人能告诉你母体是什么,你只能自己体会 -- 骇客帝国 在第四章"可视效果"中,我们研究了一些增强图层和它的内容显示效果的一些技术,在这一章中,我们将要研究可以用来对 ...

  8. iOS开发之CALayer

    1.概述 在iOS中,你能看得见摸得着的东西基本上都是UIView,比如一个按钮.一个文本标签.一个文本输入框.一个图标等等,这些都是UIView,其实UIView之所以能显示在屏幕上,完全是因为它内 ...

  9. iOS核心动画 - CALayer

    大家知道,在iOS中所有的视图都继承自UIView. UIView处理所有的触摸事件和画图. 事实上,UIView所有的渲染和动画是托管给另一个类来负责的,它就是CALayer. 但是,需要记住的是, ...

最新文章

  1. 关于 SAP 标准教程编号的说明
  2. 《Ossim应用指南》入门篇
  3. Postman接口测试神器从安装到精通
  4. loj2538 「PKUWC2018」Slay the Spire 【dp】
  5. 牛客 - Firework(多源起点的最短路)
  6. mysql安装包脚本之家_CentOS 7 MySQL5.7 TAR包解压 全自动化脚本
  7. freemarker -自定义指令
  8. mysql日期排序YMD_php将二维数组按日期(支持Ymd和Ynj格式日期)排序
  9. 【CDOJ1330】柱爷与远古法阵(高斯消元+卡精度+概率dp?)
  10. 信息学奥赛一本通答案大全
  11. 图像切割之(五)活动轮廓模型之Snake模型简单介绍
  12. 红冲发票,负数发票,作废发票
  13. 今天遇到安装CAD2014提示已安装磁盘空间显示0字节,无法下一步,已解决.#CAD2014提示已安装磁盘空间显示0字节无法下一步
  14. 十大【C语言】经典书籍,应该有你看过的吧
  15. Excel破解工作表保护密码
  16. 高校校园网代理Motion pro持续重连解决办法
  17. 商城系统官方网站PHP源码
  18. Forecasting (一):introduction
  19. spss 构建决策树 树形图 正在处理不显示问题
  20. 对 zebra 的一点理解 thread+socket+read部分 (备忘)

热门文章

  1. Unity Shader放大镜效果
  2. 夯实算力底座,“中原计算”开创河南AI产业新局面
  3. PS照片转手绘之(白发魔女)
  4. 服务报错:Required request body is missing
  5. 八、T100应付管理系统之员工费用报销管理篇
  6. bpsk传输系统实验matlab,通信原理实验4 BPSK系统仿真matlab程序
  7. 如何启用服务器的TCP IP协议,本地联接的属性里的TCP/IP协议被禁用,怎么开启啊?...
  8. 悟空号 量子计算机,悟空号获得世界上最精确的TeV电子宇宙射线能谱
  9. 场景金融丨神州信息签约第三个国家级单品大数据试点项目 助力场景金融创新
  10. Microsoft SQL Server 2012版下载与安装步骤