7-调色板-CALayer和触摸

Posted 颐和园

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了7-调色板-CALayer和触摸相关的知识,希望对你有一定的参考价值。

开始

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

绘制界面

首先声明一些常量:

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

toRadian 是一个 CGFloat 的扩展方法:

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

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

然后定义私有变量:

	// MARK: Variables
    private var selectedColor: PresetColor = .sky
    private var colors:[PresetColor] = []
    

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

public enum PresetColor: Int, CaseIterable {
    case rose
    case sky
    case dawn
    case sage
    case dusk
    case clear
    public 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 Properties
    lazy var slotShadow: NSShadow! = {
        let shadow = NSShadow()
        shadow.shadowColor = UIColor(white: 0.1, alpha: 0.2)
        shadow.shadowOffset = CGSize(width: 0, height: 3)
        shadow.shadowBlurRadius = 10
        return shadow
    }()    
    lazy var circleLayer: CAShapeLayer = {
        let x = Constants.slotDiameter/2
        let rect = bounds.inset(by: UIEdgeInsets(top: x, left:x, bottom: x, right: x))
        let shapeLayer = CAShapeLayer()
        shapeLayer.frame = rect
        shapeLayer.fillColor = UIColor.clear.cgColor
        shapeLayer.path = UIBezierPath(ovalIn: shapeLayer.bounds).cgPath
        shapeLayer.backgroundColor = UIColor.clear.cgColor
        return shapeLayer
    }()
    lazy private var swatchLayer: CALayer = {
        let swatchLayer = CALayer()
        swatchLayer.frame = bounds
        return swatchLayer
    }()

slotShadow 定义了小圆阴影。

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

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

然后是构造函数:

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

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

func setData(colors: [PresetColor]?, selected: PresetColor) {
		 // 1
        if let colors = colors {
            self.colors = colors
        }
        // 2
        selectedColor = selected
        // 3
        circleLayer.fillColor = selected.color().cgColor
        if !self.colors.isEmpty {
        	  // 4
            swatchLayer.sublayers?.forEach({ $0.removeFromSuperlayer() })
            // 5
            for (i,color) in self.colors.enumerated() {
            		// 6
                let slotLayer = colorSlotLayer(color==selected)
                slotLayer.fillColor = color.color().cgColor
				   // 7
                swatchLayer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
                // 8
                let angle = Constants.startAngle-Constants.angleSpan*CGFloat(i)
                // 9
                slotLayer.transform = CATransform3DRotate(CATransform3DIdentity, angle, 0, 0, -1)
                // 10
                swatchLayer.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] {   // 1            setData(colors: nil, selected: colors[0])        }else if let index = colors.firstIndex(of: selectedColor) { // 2            setData(colors: nil, selected: colors[index+1])        }    }
  1. 如果当前颜色已经是 colors 列表中最后一个颜色,那么又从头开始循环。
  2. 否则,选择下一个颜色。

以上是关于7-调色板-CALayer和触摸的主要内容,如果未能解决你的问题,请参考以下文章

在 CALayer 上触摸手势

创建(彩虹)调色板

在动画 UIView/CALayer 中处理触摸的最佳模式?

为啥必须使用 CALayer 的presentationLayer 进行命中测试?

Material Theme

上下拖动片段