簡體   English   中英

使用 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

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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM