[英]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
The.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 =
無處不在,您將看到實際可見的結果。
然后你可能會意識到你的面具本身有問題,但至少你不會只是看着純粹的梯度想知道面具去了哪里!
我可能會為此做更多的工作,但這是現在的工作(並回答問題)。
謝謝@馬特!
更新:該項目現已集成到已發布的 SPM 模塊中
//: 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](https://stackoverflow.com/questions/42238603/reverse-a-calayer-mask/42238699#42238699)
*/
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()
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.