问题描述
我想重新创建一个斜角效果圆形 UIButton
子类,如下图所示(但使用用户选择的基色)。我对如何设置 CAGradientLayer/掩蔽 CAShapeLayer 的边界感到困惑。
我想像在 SwiftUI 中一样将它组合成几个 CAShapeLayers
。这会根据需要进行渲染(无梯度)。
但是,当使用先前渲染的 CAGradientLayer
遮罩 CAShapeLayer
时,渐变不会被明显地绘制。
我认为我的错误与边界/帧设置有关。当我将子层添加移动到 layoutSubviews()
时,渐变环的一部分被渲染。此外,用户颜色圆圈遵守父 VC 设置的约束,但其他层不遵守。
我已经阅读了几篇关于将 CAShapeLayer
作为掩码应用于 {{1} },但我无法理解如何为这些多层正确设置框架。
CAGradientLayer
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,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
}
}
解决方法
你走在正确的轨道上......
这个想法是添加4个子层:
- “外部”
CAShapeLayer
- “戒指”
CAShapeLayer
- 带有
CAGradientLayer
掩码的“斜角”CAShapeLayer
- 带有
CAGradientLayer
面具的“内在”CAShapeLayer
将每一层的边框插入上一层的宽度,所以:
- rect = 边界
- 外层框架将是矩形
- 通过“外宽度”插入矩形
- 环形层框架将是那个插入矩形
- 通过“环宽度”插入矩形
- 斜面层框架将是那个插入矩形
- 按“斜角宽度”插入矩形
- 内层框架将是那个插入矩形
所以 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,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
属性面板:
并且,在运行时,默认状态:
和突出显示状态(垂直翻转渐变):