【问题标题】:How to compose UIButton from CAGradientLayers and CAShapeLayers?如何从 CAGradientLayers 和 CAShapeLayers 组成 UIButton?
【发布时间】:2021-08-20 03:33:08
【问题描述】:

我想重新创建一个斜角效果圆形UIButton 子类,如下图所示(但使用用户选择的基色)。我对如何设置 CAGradientLayer / 掩蔽 CAShapeLayer 的边界感到困惑。

我想像在 SwiftUI 中那样组合几个 CAShapeLayers。这会根据需要呈现(无渐变)。

但是,当使用先前渲染的 CAShapeLayer 屏蔽 CAGradientLayer 时,渐变不会明显绘制。

我认为我的错误与边界/框架设置有关。当我将子层添加到layoutSubviews() 时,会渲染部分渐变环。此外,用户色环遵循父 VC 设置的约束,但其他层不遵循。

我已经阅读了几篇关于将CAShapeLayer 用作CAGradientLayer 的掩码的帖子(1234),但我不明白如何正确设置框架对于这些多层。


extension BeveledButton {
    
    /// Draws yellow ring in desired location
    func makeEmbossmentRingAsShapeLayer()  -> CAShapeLayer {
        let pathFrame = frame.insetBy(dx: insetBevel, dy: insetBevel)
        let path = CGPath.circleStroked(width: bevelRingWidth, inFrame: pathFrame)
        
        let shape = CAShapeLayer()
        shape.path = path
        shape.fillColor = UIColor.yellow.cgColor
        return shape
    }
    
    /// Does not draw gradient (visibly) where yellow ring was
    func makeEmbossmentRingAsGradientLayer()  -> CAGradientLayer {
        let pathFrame = frame.insetBy(dx: insetBevel, dy: insetBevel)
        let path = CGPath.circleStroked(width: bevelRingWidth, inFrame: frame)
        
        let mask = CAShapeLayer()
        mask.path = path
        mask.fillColor = UIColor.black.cgColor
        mask.lineWidth = 4
        mask.frame = pathFrame
        
        let gradient = CAGradientLayer(colors: [UIColor.orange, UIColor.green], in: pathFrame)
        gradient.frame = pathFrame
        gradient.mask = mask
        return gradient
    }
}


extension CGPath {
    
    static func circle(inFrame: CGRect) -> CGPath {
        CGPath(ellipseIn: inFrame, transform: nil)
    }
    
    static func circleStroked(width: CGFloat, inFrame: CGRect) -> CGPath {
        var path = CGPath(ellipseIn: inFrame, transform: nil)
        path = path.copy(strokingWithWidth: width, lineCap: .round, lineJoin: .round, miterLimit: 0)
        return path
    }
}

extension CAGradientLayer {
    
    convenience init(colors: [UIColor], in frame: CGRect) {
        self.init()
        self.colors = colors.map(\.cgColor)
        self.frame = frame
    }
}

import UIKit
import Combine

class BeveledButton: UIButton {
    
    weak var userColor: CurrentValueSubject<UIColor,Never>?
    private var updates: AnyCancellable? = nil
    
    private lazy var outerRing: CAShapeLayer = makeOuterRing()
    private lazy var innerReflection: CAShapeLayer = makeReflectionCircle()
    private lazy var userColorCircle: CAShapeLayer = makeUserColorCircle()
    private lazy var embossmentRing: CAGradientLayer = makeEmbossmentRingAsGradientLayer()
    //    private lazy var embossmentRing: CAShapeLayer = makeEmbossmentRingAsShapeLayer()
    
    init(frame: CGRect, userColor: CurrentValueSubject<UIColor,Never>) {
        self.userColor = userColor
        super.init(frame: frame)
        setupRings()
        updateUserColor(userColor)
    }
    
    required init?(coder: NSCoder) { fatalError("") }
    
    private let outerRingWidth: CGFloat = 2.5
    private let inset: CGFloat = 3
    private let insetReflection: CGFloat = 5
    private lazy var insetBevel: CGFloat = inset + 1
    private let bevelRingWidth: CGFloat = 2
}


private extension BeveledButton {
    
    func setupRings() {
        layer.addSublayer(outerRing)
        layer.addSublayer(userColorCircle)
        layer.addSublayer(innerReflection)
        layer.addSublayer(embossmentRing)
    }
    
    func makeOuterRing() -> CAShapeLayer {
        let path = CGPath.circle(inFrame: frame)
        let shape = CAShapeLayer()
        shape.fillColor = UIColor.clear.cgColor
        shape.strokeColor = UIColor.lightGray.cgColor
        shape.lineWidth = outerRingWidth
        shape.path = path
        return shape
    }
    
    func makeUserColorCircle()  -> CAShapeLayer {
        let pathFrame = frame.insetBy(dx: inset, dy: inset)
        let path = CGPath.circle(inFrame: pathFrame)
        
        let shape = CAShapeLayer()
        shape.path = path
        shape.fillColor = userColor?.value.cgColor
        return shape
    }
    
    func makeReflectionCircle()  -> CAShapeLayer {
        let pathFrame = frame.insetBy(dx: insetReflection, dy: insetReflection)
        let path = CGPath.circle(inFrame: pathFrame)
        
        let shape = CAShapeLayer()
        shape.path = path
        shape.fillColor = UIColor.lightGray.cgColor.copy(alpha: 0.4)
        return shape
    }
    
    func updateUserColor(_ userColor: CurrentValueSubject<UIColor,Never>) {
        updates = userColor.sink { [weak self] color in
            UIView.animate(withDuration: 0.3) {
                self?.userColorCircle.fillColor = color.cgColor
            }
        }
    }
}

【问题讨论】:

    标签: ios uikit cagradientlayer


    【解决方案1】:

    你在正确的轨道上......

    这个想法是添加 4 个子层:

    1. “外”CAShapeLayer
    2. “戒指”CAShapeLayer
    3. “斜角”CAGradientLayerCAShapeLayer 掩码
    4. “内部”CAGradientLayerCAShapeLayer 掩码

    将每一层的边框插入上一层的宽度,所以:

    • rect = bounds
    • 外层框架将是矩形
    • 按“外宽”插入矩形
    • 环形层框架将是那个插入矩形
    • 按“环宽度”插入矩形
    • 斜面层框架将是那个插入矩形
    • 以“斜角宽度”插入矩形
    • 内层框架将是那个插入矩形

    所以layoutSubviews 看起来像这样:

    override func layoutSubviews() {
        super.layoutSubviews()
        
        var pth: UIBezierPath = UIBezierPath()
        var frameRect: CGRect = .zero
        var pathRect: CGRect = .zero
        
        frameRect = bounds
        
        outerLayer.frame = frameRect
        pathRect.size = frameRect.size
        pth = UIBezierPath(ovalIn: pathRect)
        outerLayer.path = pth.cgPath
        
        frameRect = frameRect.insetBy(dx: outerWidth, dy: outerWidth)
        
        ringLayer.frame = frameRect
        pathRect.size = frameRect.size
        pth = UIBezierPath(ovalIn: pathRect)
        ringLayer.path = pth.cgPath
        
        frameRect = frameRect.insetBy(dx: ringWidth, dy: ringWidth)
        
        bevelGradLayer.frame = frameRect
        pathRect.size = frameRect.size
        pth = UIBezierPath(ovalIn: pathRect)
        bevelLayerMask.path = pth.cgPath
        
        frameRect = frameRect.insetBy(dx: bevelWidth, dy: bevelWidth)
        
        innerGradLayer.frame = frameRect
        pathRect.size = frameRect.size
        pth = UIBezierPath(ovalIn: pathRect)
        innerLayerMask.path = pth.cgPath
        
    }
    

    在您发布的代码中,您正在使用extension 尝试对代码功能进行分段... 可以 有所帮助,但它也可以使事情变得更复杂,并且可以使它有点难以遵循流程 - 特别是当元素需要相关时。

    这是您想要的版本...请注意,它是 @IBDesignable 具有各种用户可用的 @IBInspectable 属性,因此您可以在 IB / Storyboard 中看到它(如果需要):

    @IBDesignable
    class BevelButton: UIButton {
        
        @IBInspectable
        var outerColor: UIColor = UIColor(white: 0.75, alpha: 1.0) {
            didSet {
                outerLayer.fillColor = outerColor.cgColor
            }
        }
        @IBInspectable
        var ringColor: UIColor = .black {
            didSet {
                ringLayer.fillColor = ringColor.cgColor
            }
        }
        @IBInspectable
        var startColor: UIColor = UIColor(white: 0.9, alpha: 1.0) {
            didSet {
                bevelGradLayer.colors = [startColor.cgColor, endColor.cgColor]
                innerGradLayer.colors = [endColor.cgColor, startColor.cgColor]
            }
        }
        @IBInspectable
        var endColor: UIColor = UIColor(white: 0.75, alpha: 1.0) {
            didSet {
                bevelGradLayer.colors = [startColor.cgColor, endColor.cgColor]
                innerGradLayer.colors = [endColor.cgColor, startColor.cgColor]
            }
        }
        
        @IBInspectable
        var outerWidth: CGFloat = 6
    
        @IBInspectable
        var ringWidth: CGFloat = 2
    
        @IBInspectable
        var bevelWidth: CGFloat = 6
        
        private let outerLayer = CAShapeLayer()
        private let ringLayer = CAShapeLayer()
        private let innerLayerMask = CAShapeLayer()
        private let innerGradLayer = CAGradientLayer()
        private let bevelLayerMask = CAShapeLayer()
        private let bevelGradLayer = CAGradientLayer()
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
    
        private func commonInit() -> Void {
            
            layer.addSublayer(outerLayer)
            layer.addSublayer(ringLayer)
            layer.addSublayer(bevelGradLayer)
            layer.addSublayer(innerGradLayer)
            
            bevelGradLayer.mask = bevelLayerMask
            innerGradLayer.mask = innerLayerMask
            
            bevelLayerMask.fillColor = UIColor.black.cgColor
            innerLayerMask.fillColor = UIColor.black.cgColor
            
            outerLayer.fillColor = outerColor.cgColor
            ringLayer.fillColor = ringColor.cgColor
            
            bevelGradLayer.colors = [startColor.cgColor, endColor.cgColor]
            innerGradLayer.colors = [endColor.cgColor, startColor.cgColor]
            
        }
        
        override func layoutSubviews() {
            super.layoutSubviews()
            
            var pth: UIBezierPath = UIBezierPath()
            var frameRect: CGRect = .zero
            var pathRect: CGRect = .zero
            
            frameRect = bounds
            
            outerLayer.frame = frameRect
            pathRect.size = frameRect.size
            pth = UIBezierPath(ovalIn: pathRect)
            outerLayer.path = pth.cgPath
            
            frameRect = frameRect.insetBy(dx: outerWidth, dy: outerWidth)
            
            ringLayer.frame = frameRect
            pathRect.size = frameRect.size
            pth = UIBezierPath(ovalIn: pathRect)
            ringLayer.path = pth.cgPath
            
            frameRect = frameRect.insetBy(dx: ringWidth, dy: ringWidth)
            
            bevelGradLayer.frame = frameRect
            pathRect.size = frameRect.size
            pth = UIBezierPath(ovalIn: pathRect)
            bevelLayerMask.path = pth.cgPath
            
            frameRect = frameRect.insetBy(dx: bevelWidth, dy: bevelWidth)
            
            innerGradLayer.frame = frameRect
            pathRect.size = frameRect.size
            pth = UIBezierPath(ovalIn: pathRect)
            innerLayerMask.path = pth.cgPath
            
        }
        
        override var isHighlighted: Bool {
            get {
                return super.isHighlighted
            }
            set {
                if newValue {
                    innerGradLayer.colors = [startColor.cgColor, endColor.cgColor]
                    bevelGradLayer.colors = [endColor.cgColor, startColor.cgColor]
                } else {
                    bevelGradLayer.colors = [startColor.cgColor, endColor.cgColor]
                    innerGradLayer.colors = [endColor.cgColor, startColor.cgColor]
                }
                super.isHighlighted = newValue
            }
        }
    }
    

    这是它在 Storyboard 中的外观(使用默认属性):

    这是IBInspectable 属性面板:

    并且,在运行时,默认状态:

    和高亮状态(渐变垂直翻转):

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-03-07
      • 1970-01-01
      • 1970-01-01
      • 2016-11-11
      • 1970-01-01
      • 2021-12-07
      • 1970-01-01
      相关资源
      最近更新 更多