使用 CATextLayer 屏蔽另一个 CALayer
Using a CATextLayer to Mask Out From Another CALayer
出于某种原因,我无法让它与 CATextLayer 一起使用。我怀疑这很明显,我看不到树木的森林,但我需要做的是使用 CATextLayer 将“孔”掩盖到 CAGradientLayer 中(因此效果是渐变,带有文本“cut出来”)。
我的这个工作正常,反之,但我出现了蛇眼,试图掩盖渐变中的文本。
这是我正在使用的代码(它是一个 UIButton class,这是 layoutSubviews() 覆盖):
override func layoutSubviews() {
super.layoutSubviews()
layer.borderColor = UIColor.clear.cgColor
if let text = titleLabel?.text,
var dynFont = titleLabel?.font {
let minimumFontSizeInPoints = (dynFont.pointSize * 0.5)
let scalingStep = 0.025
while dynFont.pointSize >= minimumFontSizeInPoints {
let calcString = NSAttributedString(string: text, attributes: [.font: dynFont])
let cropRect = calcString.boundingRect(with: CGSize.init(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
if bounds.size.width >= cropRect.size.width {
break
}
guard let tempDynFont = UIFont(name: dynFont.fontName, size: dynFont.pointSize - (dynFont.pointSize * scalingStep)) else { break }
dynFont = tempDynFont
}
titleLabel?.font = dynFont
}
if let titleLabel = titleLabel,
let font = titleLabel.font,
let text = titleLabel.text {
let textLayer = CATextLayer()
textLayer.frame = titleLabel.frame
textLayer.rasterizationScale = UIScreen.main.scale
textLayer.contentsScale = UIScreen.main.scale
textLayer.alignmentMode = .left
textLayer.fontSize = font.pointSize
textLayer.font = font
textLayer.isWrapped = true
textLayer.truncationMode = .none
textLayer.string = text
self.textLayer = textLayer
titleLabel.textColor = .clear
let gradient = CAGradientLayer()
gradient.colors = [gradientStartColor.cgColor, gradientEndColor.cgColor]
gradient.startPoint = CGPoint(x: 0.5, y: 0)
gradient.endPoint = CGPoint(x: 0.5, y: 1.0)
var layerFrame = textLayer.frame
if !reversed {
if 0 < layer.borderWidth {
let outlineLayer = CAShapeLayer()
outlineLayer.frame = bounds
outlineLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius).cgPath
outlineLayer.lineWidth = layer.borderWidth
outlineLayer.strokeColor = UIColor.white.cgColor
outlineLayer.fillColor = UIColor.clear.cgColor
layerFrame = bounds
textLayer.masksToBounds = false
if let compositingFilter = CIFilter(name: "CIAdditionCompositing") {
textLayer.compositingFilter = compositingFilter
outlineLayer.addSublayer(textLayer)
}
layer.mask = outlineLayer
} else {
layer.mask = textLayer
}
} else {
let outlineLayer = CAShapeLayer()
outlineLayer.frame = bounds
textLayer.foregroundColor = UIColor.white.cgColor
outlineLayer.backgroundColor = UIColor.white.cgColor
layerFrame = bounds
textLayer.masksToBounds = false
if let compositingFilter = CIFilter(name: "CISourceOutCompositing") {
outlineLayer.compositingFilter = compositingFilter
outlineLayer.addSublayer(textLayer)
}
layer.mask = outlineLayer
}
gradient.frame = layerFrame
layer.addSublayer(gradient)
}
}
问题出在这部分代码:
let outlineLayer = CAShapeLayer()
outlineLayer.frame = bounds
textLayer.foregroundColor = UIColor.white.cgColor
outlineLayer.backgroundColor = UIColor.white.cgColor
layerFrame = bounds
textLayer.masksToBounds = false
if let compositingFilter = CIFilter(name: "CISourceOutCompositing") {
outlineLayer.compositingFilter = compositingFilter
outlineLayer.addSublayer(textLayer)
}
layer.mask = outlineLayer
!reversed 部分工作正常。我得到了一个被文本遮盖的渐变,可能还有一个轮廓。
我需要的是获取渐变色来填充按钮,并“剪掉”文本,以便显示背景。
就像我说的,这似乎很明显,我似乎有一个障碍。
对于我可能搞砸的事情有什么建议吗?
我可能会把它变成一个游乐场,但也许这就足够了。
谢谢!
更新:
这里是游乐场:
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
@IBDesignable
class Rcvrr_GradientTextMaskButton: UIButton {
/* ################################################################## */
/**
This contains our text
*/
var textLayer: CALayer?
/* ################################################################## */
/**
The starting color for the gradient.
*/
@IBInspectable var gradientStartColor: UIColor = .white
/* ################################################################## */
/**
The ending color.
*/
@IBInspectable var gradientEndColor: UIColor = .black
/* ################################################################## */
/**
The angle of the gradient. 0 (default) is top-to-bottom.
*/
@IBInspectable var gradientAngleInDegrees: CGFloat = 0
/* ################################################################## */
/**
If true, then the label is reversed, so the background is "cut out" of the foreground.
*/
@IBInspectable var reversed: Bool = false
}
/* ###################################################################################################################################### */
// MARK: Base Class Overrides
/* ###################################################################################################################################### */
extension Rcvrr_GradientTextMaskButton {
/* ################################################################## */
/**
If the button is "standard" (the text is filled with the gradient), then this method takes care of that.
*/
override func layoutSubviews() {
super.layoutSubviews()
layer.borderColor = UIColor.clear.cgColor
if let text = titleLabel?.text,
var dynFont = titleLabel?.font {
let minimumFontSizeInPoints = (dynFont.pointSize * 0.5)
let scalingStep = 0.025
while dynFont.pointSize >= minimumFontSizeInPoints {
let calcString = NSAttributedString(string: text, attributes: [.font: dynFont])
let cropRect = calcString.boundingRect(with: CGSize.init(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
if bounds.size.width >= cropRect.size.width {
break
}
guard let tempDynFont = UIFont(name: dynFont.fontName, size: dynFont.pointSize - (dynFont.pointSize * scalingStep)) else { break }
dynFont = tempDynFont
}
titleLabel?.font = dynFont
}
if let titleLabel = titleLabel,
let font = titleLabel.font,
let text = titleLabel.text {
let textLayer = CATextLayer()
textLayer.frame = titleLabel.frame
textLayer.rasterizationScale = UIScreen.main.scale
textLayer.contentsScale = UIScreen.main.scale
textLayer.alignmentMode = .left
textLayer.fontSize = font.pointSize
textLayer.font = font
textLayer.isWrapped = true
textLayer.truncationMode = .none
textLayer.string = text
self.textLayer = textLayer
titleLabel.textColor = .clear
let gradient = CAGradientLayer()
gradient.colors = [gradientStartColor.cgColor, gradientEndColor.cgColor]
gradient.startPoint = CGPoint(x: 0.5, y: 0)
gradient.endPoint = CGPoint(x: 0.5, y: 1.0)
var layerFrame = textLayer.frame
if !reversed {
if 0 < layer.borderWidth {
let outlineLayer = CAShapeLayer()
outlineLayer.frame = bounds
outlineLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius).cgPath
outlineLayer.lineWidth = layer.borderWidth
outlineLayer.strokeColor = UIColor.white.cgColor
outlineLayer.fillColor = UIColor.clear.cgColor
layerFrame = bounds
textLayer.masksToBounds = false
if let compositingFilter = CIFilter(name: "CIAdditionCompositing") {
textLayer.compositingFilter = compositingFilter
outlineLayer.addSublayer(textLayer)
}
layer.mask = outlineLayer
} else {
layer.mask = textLayer
}
} else {
let outlineLayer = CAShapeLayer()
outlineLayer.frame = bounds
textLayer.foregroundColor = UIColor.white.cgColor
outlineLayer.backgroundColor = UIColor.white.cgColor
layerFrame = bounds
textLayer.masksToBounds = false
if let compositingFilter = CIFilter(name: "CISourceOutCompositing") {
outlineLayer.compositingFilter = compositingFilter
outlineLayer.addSublayer(textLayer)
}
layer.mask = outlineLayer
}
gradient.frame = layerFrame
layer.addSublayer(gradient)
}
}
}
class MyViewController : UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .yellow
let button = Rcvrr_GradientTextMaskButton()
button.frame = CGRect(x: 10, y: 200, width: 300, height: 50)
button.setTitle("HI", for: .normal)
button.gradientStartColor = .green
button.gradientEndColor = .blue
button.reversed = true
view.addSubview(button)
self.view = view
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
眼前的问题是您将渐变层 放在 被遮罩层的前面。你的掩蔽是有效的,但你在掩盖它!如果你想看到这里发生的事情,那不是 self.layer
你想要掩盖的;是 gradient
。把layer.mask =
到处改成gradient.mask =
,你会看到实际可见的结果。
然后你可能会意识到你的面具本身有问题,但至少你不会只是看着纯粹的梯度想知道面具去了哪里!
我可能会为此做更多的工作,但这是现在的工作(并回答问题)。
谢谢@matt!
更新: 该项目现已集成到 a published SPM module
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
/* ###################################################################################################################################### */
// MARK: - A Special Button Class That Can Be Filled With A Gradient -
/* ###################################################################################################################################### */
/**
This class can be displayed with either the text filled with a gradient, or the background filled, and the text "cut out" of it.
All behavior is the same as any other UIButton.
This allows you to specify a border, which will be included in the gradient fill.
If the borderWidth value is anything greater than 0, there will be a border, with corners specified by cornerRadius.
The border will be filled with the gradient, as well as the text.
This is a very, very simple control. I'll probably gussy it up, down the line, but it fills a need, right now.
*/
@IBDesignable
class Rcvrr_GradientTextMaskButton: UIButton {
/* ################################################################## */
/**
This caches the gradient layer.
*/
private var _gradientLayer: CAGradientLayer?
/* ################################################################## */
/**
This caches the mask layer.
*/
private var _outlineLayer: CAShapeLayer?
/* ################################################################## */
/**
The starting color for the gradient.
*/
@IBInspectable var gradientStartColor: UIColor = .white
/* ################################################################## */
/**
The ending color.
*/
@IBInspectable var gradientEndColor: UIColor = .black
/* ################################################################## */
/**
The angle of the gradient. 0 (default) is top-to-bottom.
*/
@IBInspectable var gradientAngleInDegrees: CGFloat = 0
/* ################################################################## */
/**
If true, then the label is reversed, so the background is "cut out" of the foreground.
*/
@IBInspectable var reversed: Bool = false { didSet { setNeedsLayout() }}
}
/* ###################################################################################################################################### */
// MARK: Computed Properties
/* ###################################################################################################################################### */
extension Rcvrr_GradientTextMaskButton {
/* ################################################################## */
/**
This returns the background gradient layer, rendering it, if necessary.
*/
var gradientLayer: CALayer? { makeGradientLayer() }
/* ################################################################## */
/**
This returns the mask layer, rendering it, if necessary.
*/
var outlineLayer: CALayer? { makeOutlineLayer() }
}
/* ###################################################################################################################################### */
// MARK: Instance Methods
/* ###################################################################################################################################### */
extension Rcvrr_GradientTextMaskButton {
/* ################################################################## */
/**
This creates the gradient layer, using our specified start and stop colors.
*/
func makeGradientLayer() -> CALayer? {
guard nil == _gradientLayer else { return _gradientLayer }
_gradientLayer = CAGradientLayer()
_gradientLayer?.frame = bounds
_gradientLayer?.colors = [gradientStartColor.cgColor, gradientEndColor.cgColor]
_gradientLayer?.startPoint = CGPoint(x: 0.5, y: 0) // .rotated(around: CGPoint(x: 0.5, y: 0.5), byDegrees: gradientAngleInDegrees)
_gradientLayer?.endPoint = CGPoint(x: 0.5, y: 1.0) // .rotated(around: CGPoint(x: 0.5, y: 0.5), byDegrees: gradientAngleInDegrees)
return _gradientLayer
}
/* ################################################################## */
/**
This uses our text to generate a mask layer.
*/
func makeOutlineLayer() -> CALayer? {
guard nil == _outlineLayer else { return _outlineLayer }
if let text = titleLabel?.text,
var dynFont = titleLabel?.font {
let minimumFontSizeInPoints = (dynFont.pointSize * 0.5)
let scalingStep = 0.025
while dynFont.pointSize >= minimumFontSizeInPoints {
let calcString = NSAttributedString(string: text, attributes: [.font: dynFont])
let cropRect = calcString.boundingRect(with: CGSize.init(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
if bounds.size.width >= cropRect.size.width {
break
}
guard let tempDynFont = UIFont(name: dynFont.fontName, size: dynFont.pointSize - (dynFont.pointSize * scalingStep)) else { break }
dynFont = tempDynFont
}
titleLabel?.font = dynFont
}
let foreColor = reversed ? UIColor.black.cgColor : UIColor.white.cgColor
let backColor = reversed ? UIColor.white.cgColor : UIColor.black.cgColor
if let titleLabel = titleLabel,
let font = titleLabel.font,
let text = titleLabel.text {
let textLayer = CATextLayer()
textLayer.frame = titleLabel.frame
textLayer.rasterizationScale = UIScreen.main.scale
textLayer.contentsScale = UIScreen.main.scale
textLayer.alignmentMode = .left
textLayer.fontSize = font.pointSize
textLayer.font = font
textLayer.isWrapped = true
textLayer.truncationMode = .none
textLayer.string = text
textLayer.foregroundColor = foreColor
let outlineLayer = CAShapeLayer()
outlineLayer.frame = bounds
outlineLayer.strokeColor = foreColor
outlineLayer.fillColor = backColor
outlineLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius).cgPath
outlineLayer.lineWidth = layer.borderWidth
if let compositingFilter = CIFilter(name: "CIAdditionCompositing") {
textLayer.compositingFilter = compositingFilter
outlineLayer.addSublayer(textLayer)
self._outlineLayer = outlineLayer
}
}
return _outlineLayer
}
}
/* ###################################################################################################################################### */
// MARK: Base Class Overrides
/* ###################################################################################################################################### */
extension Rcvrr_GradientTextMaskButton {
/* ################################################################## */
/**
We call this, when it's time to layout the control.
We subvert the standard rendering, and replace it with our own rendering.
Some of this comes from [this SO answer](
*/
override func layoutSubviews() {
super.layoutSubviews()
// This sets up the baseline.
_outlineLayer = nil
backgroundColor = .clear
layer.borderColor = UIColor.clear.cgColor
tintColor = .clear
titleLabel?.textColor = .clear
_gradientLayer?.removeFromSuperlayer()
layer.mask = nil
// Create a mask, and apply that to our background gradient.
if let gradientLayer = gradientLayer,
let outlineLayer = outlineLayer,
let filter = CIFilter(name: "CIMaskToAlpha") {
layer.addSublayer(gradientLayer)
let renderedCoreImage = CIImage(image: UIGraphicsImageRenderer(size: bounds.size).image { context in return outlineLayer.render(in: context.cgContext) })
filter.setValue(renderedCoreImage, forKey: "inputImage")
if let outputImage = filter.outputImage {
let coreGraphicsImage = CIContext().createCGImage(outputImage, from: outputImage.extent)
let maskLayer = CALayer()
maskLayer.frame = bounds
maskLayer.contents = coreGraphicsImage
layer.mask = maskLayer
}
}
}
}
class MyViewController : UIViewController {
var button1: Rcvrr_GradientTextMaskButton!
var button2: Rcvrr_GradientTextMaskButton!
override func loadView() {
let view = UIView()
view.backgroundColor = .yellow
button1 = Rcvrr_GradientTextMaskButton()
button1.frame = CGRect(x: 10, y: 100, width: 300, height: 50)
button1.setTitle("HI", for: .normal)
button1.gradientStartColor = .green
button1.gradientEndColor = .blue
button1.reversed = true
button1.addTarget(self, action: #selector(buttonHit), for: .primaryActionTriggered)
view.addSubview(button1)
button2 = Rcvrr_GradientTextMaskButton()
button2.frame = CGRect(x: 10, y: 200, width: 300, height: 50)
button2.setTitle("BYE", for: .normal)
button2.gradientStartColor = .green
button2.gradientEndColor = .blue
button2.reversed = false
button2.addTarget(self, action: #selector(buttonHit), for: .primaryActionTriggered)
view.addSubview(button2)
self.view = view
}
@objc func buttonHit(_ inButton: Rcvrr_GradientTextMaskButton) {
print("Button is\(inButton.reversed ? "" : " not") reversed.")
if button1 == inButton {
print("HI!")
button2.reversed = !inButton.reversed
} else {
print("Bye!")
button1.reversed = !inButton.reversed
}
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
出于某种原因,我无法让它与 CATextLayer 一起使用。我怀疑这很明显,我看不到树木的森林,但我需要做的是使用 CATextLayer 将“孔”掩盖到 CAGradientLayer 中(因此效果是渐变,带有文本“cut出来”)。
我的这个工作正常,反之,但我出现了蛇眼,试图掩盖渐变中的文本。
这是我正在使用的代码(它是一个 UIButton class,这是 layoutSubviews() 覆盖):
override func layoutSubviews() {
super.layoutSubviews()
layer.borderColor = UIColor.clear.cgColor
if let text = titleLabel?.text,
var dynFont = titleLabel?.font {
let minimumFontSizeInPoints = (dynFont.pointSize * 0.5)
let scalingStep = 0.025
while dynFont.pointSize >= minimumFontSizeInPoints {
let calcString = NSAttributedString(string: text, attributes: [.font: dynFont])
let cropRect = calcString.boundingRect(with: CGSize.init(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
if bounds.size.width >= cropRect.size.width {
break
}
guard let tempDynFont = UIFont(name: dynFont.fontName, size: dynFont.pointSize - (dynFont.pointSize * scalingStep)) else { break }
dynFont = tempDynFont
}
titleLabel?.font = dynFont
}
if let titleLabel = titleLabel,
let font = titleLabel.font,
let text = titleLabel.text {
let textLayer = CATextLayer()
textLayer.frame = titleLabel.frame
textLayer.rasterizationScale = UIScreen.main.scale
textLayer.contentsScale = UIScreen.main.scale
textLayer.alignmentMode = .left
textLayer.fontSize = font.pointSize
textLayer.font = font
textLayer.isWrapped = true
textLayer.truncationMode = .none
textLayer.string = text
self.textLayer = textLayer
titleLabel.textColor = .clear
let gradient = CAGradientLayer()
gradient.colors = [gradientStartColor.cgColor, gradientEndColor.cgColor]
gradient.startPoint = CGPoint(x: 0.5, y: 0)
gradient.endPoint = CGPoint(x: 0.5, y: 1.0)
var layerFrame = textLayer.frame
if !reversed {
if 0 < layer.borderWidth {
let outlineLayer = CAShapeLayer()
outlineLayer.frame = bounds
outlineLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius).cgPath
outlineLayer.lineWidth = layer.borderWidth
outlineLayer.strokeColor = UIColor.white.cgColor
outlineLayer.fillColor = UIColor.clear.cgColor
layerFrame = bounds
textLayer.masksToBounds = false
if let compositingFilter = CIFilter(name: "CIAdditionCompositing") {
textLayer.compositingFilter = compositingFilter
outlineLayer.addSublayer(textLayer)
}
layer.mask = outlineLayer
} else {
layer.mask = textLayer
}
} else {
let outlineLayer = CAShapeLayer()
outlineLayer.frame = bounds
textLayer.foregroundColor = UIColor.white.cgColor
outlineLayer.backgroundColor = UIColor.white.cgColor
layerFrame = bounds
textLayer.masksToBounds = false
if let compositingFilter = CIFilter(name: "CISourceOutCompositing") {
outlineLayer.compositingFilter = compositingFilter
outlineLayer.addSublayer(textLayer)
}
layer.mask = outlineLayer
}
gradient.frame = layerFrame
layer.addSublayer(gradient)
}
}
问题出在这部分代码:
let outlineLayer = CAShapeLayer()
outlineLayer.frame = bounds
textLayer.foregroundColor = UIColor.white.cgColor
outlineLayer.backgroundColor = UIColor.white.cgColor
layerFrame = bounds
textLayer.masksToBounds = false
if let compositingFilter = CIFilter(name: "CISourceOutCompositing") {
outlineLayer.compositingFilter = compositingFilter
outlineLayer.addSublayer(textLayer)
}
layer.mask = outlineLayer
!reversed 部分工作正常。我得到了一个被文本遮盖的渐变,可能还有一个轮廓。
我需要的是获取渐变色来填充按钮,并“剪掉”文本,以便显示背景。
就像我说的,这似乎很明显,我似乎有一个障碍。
对于我可能搞砸的事情有什么建议吗?
我可能会把它变成一个游乐场,但也许这就足够了。
谢谢!
更新:
这里是游乐场:
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
@IBDesignable
class Rcvrr_GradientTextMaskButton: UIButton {
/* ################################################################## */
/**
This contains our text
*/
var textLayer: CALayer?
/* ################################################################## */
/**
The starting color for the gradient.
*/
@IBInspectable var gradientStartColor: UIColor = .white
/* ################################################################## */
/**
The ending color.
*/
@IBInspectable var gradientEndColor: UIColor = .black
/* ################################################################## */
/**
The angle of the gradient. 0 (default) is top-to-bottom.
*/
@IBInspectable var gradientAngleInDegrees: CGFloat = 0
/* ################################################################## */
/**
If true, then the label is reversed, so the background is "cut out" of the foreground.
*/
@IBInspectable var reversed: Bool = false
}
/* ###################################################################################################################################### */
// MARK: Base Class Overrides
/* ###################################################################################################################################### */
extension Rcvrr_GradientTextMaskButton {
/* ################################################################## */
/**
If the button is "standard" (the text is filled with the gradient), then this method takes care of that.
*/
override func layoutSubviews() {
super.layoutSubviews()
layer.borderColor = UIColor.clear.cgColor
if let text = titleLabel?.text,
var dynFont = titleLabel?.font {
let minimumFontSizeInPoints = (dynFont.pointSize * 0.5)
let scalingStep = 0.025
while dynFont.pointSize >= minimumFontSizeInPoints {
let calcString = NSAttributedString(string: text, attributes: [.font: dynFont])
let cropRect = calcString.boundingRect(with: CGSize.init(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
if bounds.size.width >= cropRect.size.width {
break
}
guard let tempDynFont = UIFont(name: dynFont.fontName, size: dynFont.pointSize - (dynFont.pointSize * scalingStep)) else { break }
dynFont = tempDynFont
}
titleLabel?.font = dynFont
}
if let titleLabel = titleLabel,
let font = titleLabel.font,
let text = titleLabel.text {
let textLayer = CATextLayer()
textLayer.frame = titleLabel.frame
textLayer.rasterizationScale = UIScreen.main.scale
textLayer.contentsScale = UIScreen.main.scale
textLayer.alignmentMode = .left
textLayer.fontSize = font.pointSize
textLayer.font = font
textLayer.isWrapped = true
textLayer.truncationMode = .none
textLayer.string = text
self.textLayer = textLayer
titleLabel.textColor = .clear
let gradient = CAGradientLayer()
gradient.colors = [gradientStartColor.cgColor, gradientEndColor.cgColor]
gradient.startPoint = CGPoint(x: 0.5, y: 0)
gradient.endPoint = CGPoint(x: 0.5, y: 1.0)
var layerFrame = textLayer.frame
if !reversed {
if 0 < layer.borderWidth {
let outlineLayer = CAShapeLayer()
outlineLayer.frame = bounds
outlineLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius).cgPath
outlineLayer.lineWidth = layer.borderWidth
outlineLayer.strokeColor = UIColor.white.cgColor
outlineLayer.fillColor = UIColor.clear.cgColor
layerFrame = bounds
textLayer.masksToBounds = false
if let compositingFilter = CIFilter(name: "CIAdditionCompositing") {
textLayer.compositingFilter = compositingFilter
outlineLayer.addSublayer(textLayer)
}
layer.mask = outlineLayer
} else {
layer.mask = textLayer
}
} else {
let outlineLayer = CAShapeLayer()
outlineLayer.frame = bounds
textLayer.foregroundColor = UIColor.white.cgColor
outlineLayer.backgroundColor = UIColor.white.cgColor
layerFrame = bounds
textLayer.masksToBounds = false
if let compositingFilter = CIFilter(name: "CISourceOutCompositing") {
outlineLayer.compositingFilter = compositingFilter
outlineLayer.addSublayer(textLayer)
}
layer.mask = outlineLayer
}
gradient.frame = layerFrame
layer.addSublayer(gradient)
}
}
}
class MyViewController : UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .yellow
let button = Rcvrr_GradientTextMaskButton()
button.frame = CGRect(x: 10, y: 200, width: 300, height: 50)
button.setTitle("HI", for: .normal)
button.gradientStartColor = .green
button.gradientEndColor = .blue
button.reversed = true
view.addSubview(button)
self.view = view
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
眼前的问题是您将渐变层 放在 被遮罩层的前面。你的掩蔽是有效的,但你在掩盖它!如果你想看到这里发生的事情,那不是 self.layer
你想要掩盖的;是 gradient
。把layer.mask =
到处改成gradient.mask =
,你会看到实际可见的结果。
然后你可能会意识到你的面具本身有问题,但至少你不会只是看着纯粹的梯度想知道面具去了哪里!
我可能会为此做更多的工作,但这是现在的工作(并回答问题)。
谢谢@matt!
更新: 该项目现已集成到 a published SPM module
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
/* ###################################################################################################################################### */
// MARK: - A Special Button Class That Can Be Filled With A Gradient -
/* ###################################################################################################################################### */
/**
This class can be displayed with either the text filled with a gradient, or the background filled, and the text "cut out" of it.
All behavior is the same as any other UIButton.
This allows you to specify a border, which will be included in the gradient fill.
If the borderWidth value is anything greater than 0, there will be a border, with corners specified by cornerRadius.
The border will be filled with the gradient, as well as the text.
This is a very, very simple control. I'll probably gussy it up, down the line, but it fills a need, right now.
*/
@IBDesignable
class Rcvrr_GradientTextMaskButton: UIButton {
/* ################################################################## */
/**
This caches the gradient layer.
*/
private var _gradientLayer: CAGradientLayer?
/* ################################################################## */
/**
This caches the mask layer.
*/
private var _outlineLayer: CAShapeLayer?
/* ################################################################## */
/**
The starting color for the gradient.
*/
@IBInspectable var gradientStartColor: UIColor = .white
/* ################################################################## */
/**
The ending color.
*/
@IBInspectable var gradientEndColor: UIColor = .black
/* ################################################################## */
/**
The angle of the gradient. 0 (default) is top-to-bottom.
*/
@IBInspectable var gradientAngleInDegrees: CGFloat = 0
/* ################################################################## */
/**
If true, then the label is reversed, so the background is "cut out" of the foreground.
*/
@IBInspectable var reversed: Bool = false { didSet { setNeedsLayout() }}
}
/* ###################################################################################################################################### */
// MARK: Computed Properties
/* ###################################################################################################################################### */
extension Rcvrr_GradientTextMaskButton {
/* ################################################################## */
/**
This returns the background gradient layer, rendering it, if necessary.
*/
var gradientLayer: CALayer? { makeGradientLayer() }
/* ################################################################## */
/**
This returns the mask layer, rendering it, if necessary.
*/
var outlineLayer: CALayer? { makeOutlineLayer() }
}
/* ###################################################################################################################################### */
// MARK: Instance Methods
/* ###################################################################################################################################### */
extension Rcvrr_GradientTextMaskButton {
/* ################################################################## */
/**
This creates the gradient layer, using our specified start and stop colors.
*/
func makeGradientLayer() -> CALayer? {
guard nil == _gradientLayer else { return _gradientLayer }
_gradientLayer = CAGradientLayer()
_gradientLayer?.frame = bounds
_gradientLayer?.colors = [gradientStartColor.cgColor, gradientEndColor.cgColor]
_gradientLayer?.startPoint = CGPoint(x: 0.5, y: 0) // .rotated(around: CGPoint(x: 0.5, y: 0.5), byDegrees: gradientAngleInDegrees)
_gradientLayer?.endPoint = CGPoint(x: 0.5, y: 1.0) // .rotated(around: CGPoint(x: 0.5, y: 0.5), byDegrees: gradientAngleInDegrees)
return _gradientLayer
}
/* ################################################################## */
/**
This uses our text to generate a mask layer.
*/
func makeOutlineLayer() -> CALayer? {
guard nil == _outlineLayer else { return _outlineLayer }
if let text = titleLabel?.text,
var dynFont = titleLabel?.font {
let minimumFontSizeInPoints = (dynFont.pointSize * 0.5)
let scalingStep = 0.025
while dynFont.pointSize >= minimumFontSizeInPoints {
let calcString = NSAttributedString(string: text, attributes: [.font: dynFont])
let cropRect = calcString.boundingRect(with: CGSize.init(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
if bounds.size.width >= cropRect.size.width {
break
}
guard let tempDynFont = UIFont(name: dynFont.fontName, size: dynFont.pointSize - (dynFont.pointSize * scalingStep)) else { break }
dynFont = tempDynFont
}
titleLabel?.font = dynFont
}
let foreColor = reversed ? UIColor.black.cgColor : UIColor.white.cgColor
let backColor = reversed ? UIColor.white.cgColor : UIColor.black.cgColor
if let titleLabel = titleLabel,
let font = titleLabel.font,
let text = titleLabel.text {
let textLayer = CATextLayer()
textLayer.frame = titleLabel.frame
textLayer.rasterizationScale = UIScreen.main.scale
textLayer.contentsScale = UIScreen.main.scale
textLayer.alignmentMode = .left
textLayer.fontSize = font.pointSize
textLayer.font = font
textLayer.isWrapped = true
textLayer.truncationMode = .none
textLayer.string = text
textLayer.foregroundColor = foreColor
let outlineLayer = CAShapeLayer()
outlineLayer.frame = bounds
outlineLayer.strokeColor = foreColor
outlineLayer.fillColor = backColor
outlineLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius).cgPath
outlineLayer.lineWidth = layer.borderWidth
if let compositingFilter = CIFilter(name: "CIAdditionCompositing") {
textLayer.compositingFilter = compositingFilter
outlineLayer.addSublayer(textLayer)
self._outlineLayer = outlineLayer
}
}
return _outlineLayer
}
}
/* ###################################################################################################################################### */
// MARK: Base Class Overrides
/* ###################################################################################################################################### */
extension Rcvrr_GradientTextMaskButton {
/* ################################################################## */
/**
We call this, when it's time to layout the control.
We subvert the standard rendering, and replace it with our own rendering.
Some of this comes from [this SO answer](
*/
override func layoutSubviews() {
super.layoutSubviews()
// This sets up the baseline.
_outlineLayer = nil
backgroundColor = .clear
layer.borderColor = UIColor.clear.cgColor
tintColor = .clear
titleLabel?.textColor = .clear
_gradientLayer?.removeFromSuperlayer()
layer.mask = nil
// Create a mask, and apply that to our background gradient.
if let gradientLayer = gradientLayer,
let outlineLayer = outlineLayer,
let filter = CIFilter(name: "CIMaskToAlpha") {
layer.addSublayer(gradientLayer)
let renderedCoreImage = CIImage(image: UIGraphicsImageRenderer(size: bounds.size).image { context in return outlineLayer.render(in: context.cgContext) })
filter.setValue(renderedCoreImage, forKey: "inputImage")
if let outputImage = filter.outputImage {
let coreGraphicsImage = CIContext().createCGImage(outputImage, from: outputImage.extent)
let maskLayer = CALayer()
maskLayer.frame = bounds
maskLayer.contents = coreGraphicsImage
layer.mask = maskLayer
}
}
}
}
class MyViewController : UIViewController {
var button1: Rcvrr_GradientTextMaskButton!
var button2: Rcvrr_GradientTextMaskButton!
override func loadView() {
let view = UIView()
view.backgroundColor = .yellow
button1 = Rcvrr_GradientTextMaskButton()
button1.frame = CGRect(x: 10, y: 100, width: 300, height: 50)
button1.setTitle("HI", for: .normal)
button1.gradientStartColor = .green
button1.gradientEndColor = .blue
button1.reversed = true
button1.addTarget(self, action: #selector(buttonHit), for: .primaryActionTriggered)
view.addSubview(button1)
button2 = Rcvrr_GradientTextMaskButton()
button2.frame = CGRect(x: 10, y: 200, width: 300, height: 50)
button2.setTitle("BYE", for: .normal)
button2.gradientStartColor = .green
button2.gradientEndColor = .blue
button2.reversed = false
button2.addTarget(self, action: #selector(buttonHit), for: .primaryActionTriggered)
view.addSubview(button2)
self.view = view
}
@objc func buttonHit(_ inButton: Rcvrr_GradientTextMaskButton) {
print("Button is\(inButton.reversed ? "" : " not") reversed.")
if button1 == inButton {
print("HI!")
button2.reversed = !inButton.reversed
} else {
print("Bye!")
button1.reversed = !inButton.reversed
}
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()