如何在多元标签中淡出最后一行的末端?

发布于 2025-02-08 07:17:42 字数 1754 浏览 2 评论 0原文

请注意,它必须在Uilabel -1,2,3等中使用不同数量的线路。 我已经找到了解决方案对于1行标签,您可以在其中掩盖。 uilabel与Cagradientlayer的图层,但它不起作用对于多行式标签分层并淡入所有线条。

我试图使另一个具有所需宽度的位置的Calayer位于最后一行的位置,并使用Cagradientlayer作为掩膜,并将此层添加为Uilabel的Sublayer,它用于静态对象,但我在UitableViewCell中使用此uilabel,何时在uilable中使用。敲击 - 它会将颜色变成灰色,我可以看到我的图层,因为它使用了背景颜色uilabel查看布局的子视图时,x位置计算也有问题:

extension UILabel {
    func fadeOutLastLineEnd() { //Call in layoutSubviews
        guard bounds.width > 0 else { return }

        lineBreakMode = .byCharWrapping
        let tmpLayer = CALayer()
        let gradientWidth: CGFloat = 32
        let numberOfLines = CGFloat(numberOfLines)
        tmpLayer.backgroundColor = UIColor.white.cgColor
        tmpLayer.frame = CGRect(x: layer.frame.width - gradientWidth,
                                y: layer.frame.height / numberOfLines,
                                width: gradientWidth,
                                height: layer.frame.height / numberOfLines)
        
        let tmpGrLayer = CAGradientLayer()

        tmpGrLayer.colors     = [UIColor.white.cgColor, UIColor.clear.cgColor]
        tmpGrLayer.startPoint = CGPoint(x: 1, y: 0)
        tmpGrLayer.endPoint   = CGPoint(x: 0, y: 0)
        tmpGrLayer.frame = tmpLayer.bounds
        
        tmpLayer.mask = tmpGrLayer
        layer.addSublayer(tmpLayer)
    }
}

“ uilabel”>:

  • 因此,我需要
  • 最后一行的结尾需要褪色(渐变?)
  • 在uitableviewcell中起作用,当整个对象更改颜色时

Note, that it must work with different number of lines in UILabel - 1,2,3 etc.
I've already found solution for 1 line label, where you mask UILabel's layer with CAGradientLayer, but it doesn't work for multiline labels, as it masks the whole layer and fades out all lines.

I tried to make another CALayer with position calculated to be in the position of last line with desired width and used CAGradientLayer as mask and add this layer as sublayer of UILabel, it worked for static objects, but i use this UILabel in UITableViewCell and when it's tapped - it changes color to gray and i can see my layer, because it uses background color of UILabel when view layout its subviews, and also something wrong with x position calculation:

extension UILabel {
    func fadeOutLastLineEnd() { //Call in layoutSubviews
        guard bounds.width > 0 else { return }

        lineBreakMode = .byCharWrapping
        let tmpLayer = CALayer()
        let gradientWidth: CGFloat = 32
        let numberOfLines = CGFloat(numberOfLines)
        tmpLayer.backgroundColor = UIColor.white.cgColor
        tmpLayer.frame = CGRect(x: layer.frame.width - gradientWidth,
                                y: layer.frame.height / numberOfLines,
                                width: gradientWidth,
                                height: layer.frame.height / numberOfLines)
        
        let tmpGrLayer = CAGradientLayer()

        tmpGrLayer.colors     = [UIColor.white.cgColor, UIColor.clear.cgColor]
        tmpGrLayer.startPoint = CGPoint(x: 1, y: 0)
        tmpGrLayer.endPoint   = CGPoint(x: 0, y: 0)
        tmpGrLayer.frame = tmpLayer.bounds
        
        tmpLayer.mask = tmpGrLayer
        layer.addSublayer(tmpLayer)
    }
}

So, i need UILabel:

  • which can be multiline
  • end of last line needs to be faded out (gradient?)
  • works in UITableViewCell, when the whole object changes color

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

话少心凉 2025-02-15 07:17:42

有多种方法可以做到这一点 - 这是一种方法。

我们可以通过设置layer.mask来掩盖视图。面具的不透明将显示出透明的区域,透明区域不会。

因此,我们需要的是一个自定义层子类,看起来像这样:

”在此处输入图像描述

这是一个示例,我将调用InvertedGradientLayer

class InvertedGradientLayer: CALayer {
    
    public var lineHeight: CGFloat = 0
    public var gradWidth: CGFloat = 0
    
    override func draw(in inContext: CGContext) {
        
        // fill all but the bottom "line height" with opaque color
        inContext.setFillColor(UIColor.gray.cgColor)
        var r = self.bounds
        r.size.height -= lineHeight
        inContext.fill(r)

        // can be any color, we're going from Opaque to Clear
        let colors = [UIColor.gray.cgColor, UIColor.gray.withAlphaComponent(0.0).cgColor]
        
        let colorSpace = CGColorSpaceCreateDeviceRGB()
        
        let colorLocations: [CGFloat] = [0.0, 1.0]
        
        let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: colorLocations)!
        
        // start the gradient "grad width" from right edge
        let startPoint = CGPoint(x: bounds.maxX - gradWidth, y: 0.5)
        // end the gradient at the right edge, but
        // probably want to leave the farthest-right 1 or 2 points
        //  completely transparent
        let endPoint = CGPoint(x: bounds.maxX - 2.0, y: 0.5)

        // gradient rect starts at the bottom of the opaque rect
        r.origin.y = r.size.height - 1
        // gradient rect height can extend below the bounds, becuase it will be clipped
        r.size.height = bounds.height
        inContext.addRect(r)
        inContext.clip()
        inContext.drawLinearGradient(gradient, start: startPoint, end: endPoint, options: .drawsBeforeStartLocation)

    }
    
}

接下来,我们将使我们制作a <代码> uilabel 实现InvertedGradientLayer作为图层蒙版的子类:

class CornerFadeLabel: UILabel {
    let ivgLayer = InvertedGradientLayer()
    override func layoutSubviews() {
        super.layoutSubviews()
        guard let f = self.font, let t = self.text else { return }
        // we only want to fade-out the last line if
        //  it would be clipped
        let constraintRect = CGSize(width: bounds.width, height: .greatestFiniteMagnitude)
        let boundingBox = t.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font : f], context: nil)
        if boundingBox.height <= bounds.height {
            layer.mask = nil
            return
        }
        layer.mask = ivgLayer
        ivgLayer.lineHeight = f.lineHeight
        ivgLayer.gradWidth = 60.0
        ivgLayer.frame = bounds
        ivgLayer.setNeedsDisplay()
    }
}

这是使用它在使用中的示例视图控制器:

class FadeVC: UIViewController {
    
    let wordWrapFadeLabel: CornerFadeLabel = {
        let v = CornerFadeLabel()
        v.numberOfLines = 1
        v.lineBreakMode = .byWordWrapping
        return v
    }()
    
    let charWrapFadeLabel: CornerFadeLabel = {
        let v = CornerFadeLabel()
        v.numberOfLines = 1
        v.lineBreakMode = .byCharWrapping
        return v
    }()
    
    let normalLabel: UILabel = {
        let v = UILabel()
        v.numberOfLines = 1
        return v
    }()
    
    let numLinesLabel: UILabel = {
        let v = UILabel()
        v.textAlignment = .center
        return v
    }()
    
    var numLines: Int = 0
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemBackground
        
        let sampleText = "This is some example text that will wrap onto multiple lines and fade-out the bottom-right corner instead of truncating or clipping a last line."
        wordWrapFadeLabel.text = sampleText
        charWrapFadeLabel.text = sampleText
        normalLabel.text = sampleText
        
        let stack: UIStackView = {
            let v = UIStackView()
            v.axis = .vertical
            v.spacing = 8
            v.translatesAutoresizingMaskIntoConstraints = false
            return v
        }()
        
        let bStack: UIStackView = {
            let v = UIStackView()
            v.axis = .horizontal
            v.spacing = 8
            v.translatesAutoresizingMaskIntoConstraints = false
            return v
        }()
        
        let btnUP: UIButton = {
            let v = UIButton()
            let cfg = UIImage.SymbolConfiguration(pointSize: 28.0, weight: .bold, scale: .large)
            let img = UIImage(systemName: "chevron.up.circle.fill", withConfiguration: cfg)
            v.setImage(img, for: [])
            v.tintColor = .systemGreen
            v.widthAnchor.constraint(equalTo: v.heightAnchor).isActive = true
            v.addTarget(self, action: #selector(btnUpTapped), for: .touchUpInside)
            return v
        }()
        
        let btnDown: UIButton = {
            let v = UIButton()
            let cfg = UIImage.SymbolConfiguration(pointSize: 28.0, weight: .bold, scale: .large)
            let img = UIImage(systemName: "chevron.down.circle.fill", withConfiguration: cfg)
            v.setImage(img, for: [])
            v.tintColor = .systemGreen
            v.widthAnchor.constraint(equalTo: v.heightAnchor).isActive = true
            v.addTarget(self, action: #selector(btnDownTapped), for: .touchUpInside)
            return v
        }()
        
        bStack.addArrangedSubview(btnUP)
        bStack.addArrangedSubview(numLinesLabel)
        bStack.addArrangedSubview(btnDown)
        
        let v1 = UILabel()
        v1.text = "Word-wrapping"
        v1.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
        
        let v2 = UILabel()
        v2.text = "Character-wrapping"
        v2.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
        
        let v3 = UILabel()
        v3.text = "Normal Label (Truncate Tail)"
        v3.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
        
        stack.addArrangedSubview(bStack)
        stack.addArrangedSubview(v1)
        stack.addArrangedSubview(wordWrapFadeLabel)
        stack.addArrangedSubview(v2)
        stack.addArrangedSubview(charWrapFadeLabel)
        stack.addArrangedSubview(v3)
        stack.addArrangedSubview(normalLabel)

        stack.setCustomSpacing(20, after: bStack)
        stack.setCustomSpacing(20, after: wordWrapFadeLabel)
        stack.setCustomSpacing(20, after: charWrapFadeLabel)

        view.addSubview(stack)
        
        // dashed border views so we can see the lable frames
        let wordBorderView = DashedView()
        let charBorderView = DashedView()
        let normalBorderView = DashedView()
        wordBorderView.translatesAutoresizingMaskIntoConstraints = false
        charBorderView.translatesAutoresizingMaskIntoConstraints = false
        normalBorderView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(wordBorderView)
        view.addSubview(charBorderView)
        view.addSubview(normalBorderView)

        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            
            stack.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
            stack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 60.0),
            stack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -60.0),
            
            wordBorderView.topAnchor.constraint(equalTo: wordWrapFadeLabel.topAnchor, constant: 0.0),
            wordBorderView.leadingAnchor.constraint(equalTo: wordWrapFadeLabel.leadingAnchor, constant: 0.0),
            wordBorderView.trailingAnchor.constraint(equalTo: wordWrapFadeLabel.trailingAnchor, constant: 0.0),
            wordBorderView.bottomAnchor.constraint(equalTo: wordWrapFadeLabel.bottomAnchor, constant: 0.0),
            
            charBorderView.topAnchor.constraint(equalTo: charWrapFadeLabel.topAnchor, constant: 0.0),
            charBorderView.leadingAnchor.constraint(equalTo: charWrapFadeLabel.leadingAnchor, constant: 0.0),
            charBorderView.trailingAnchor.constraint(equalTo: charWrapFadeLabel.trailingAnchor, constant: 0.0),
            charBorderView.bottomAnchor.constraint(equalTo: charWrapFadeLabel.bottomAnchor, constant: 0.0),
            
            normalBorderView.topAnchor.constraint(equalTo: normalLabel.topAnchor, constant: 0.0),
            normalBorderView.leadingAnchor.constraint(equalTo: normalLabel.leadingAnchor, constant: 0.0),
            normalBorderView.trailingAnchor.constraint(equalTo: normalLabel.trailingAnchor, constant: 0.0),
            normalBorderView.bottomAnchor.constraint(equalTo: normalLabel.bottomAnchor, constant: 0.0),
            
        ])
        
        // set initial number of lines to 1
        btnUpTapped()
        
    }
    @objc func btnUpTapped() {
        numLines += 1
        numLinesLabel.text = "Num Lines: \(numLines)"
        wordWrapFadeLabel.numberOfLines = numLines
        charWrapFadeLabel.numberOfLines = numLines
        normalLabel.numberOfLines = numLines
    }
    @objc func btnDownTapped() {
        if numLines == 1 { return }
        numLines -= 1
        numLinesLabel.text = "Num Lines: \(numLines)"
        wordWrapFadeLabel.numberOfLines = numLines
        charWrapFadeLabel.numberOfLines = numLines
        normalLabel.numberOfLines = numLines
    }
}

运行时,看起来像这样:

红色虚线的边界就在那里,以便我们可以看到标签的框架。敲击上/向下箭头将增加/减少每个标签中要显示的最大线数。

There are various ways to do this -- here's one approach.

We can mask a view by setting the layer.mask. The opaque areas of the mask will show-through, and the transparent areas will not.

So, what we need is a custom layer subclass that will look like this:

enter image description here

This is an example that I'll call InvertedGradientLayer:

class InvertedGradientLayer: CALayer {
    
    public var lineHeight: CGFloat = 0
    public var gradWidth: CGFloat = 0
    
    override func draw(in inContext: CGContext) {
        
        // fill all but the bottom "line height" with opaque color
        inContext.setFillColor(UIColor.gray.cgColor)
        var r = self.bounds
        r.size.height -= lineHeight
        inContext.fill(r)

        // can be any color, we're going from Opaque to Clear
        let colors = [UIColor.gray.cgColor, UIColor.gray.withAlphaComponent(0.0).cgColor]
        
        let colorSpace = CGColorSpaceCreateDeviceRGB()
        
        let colorLocations: [CGFloat] = [0.0, 1.0]
        
        let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: colorLocations)!
        
        // start the gradient "grad width" from right edge
        let startPoint = CGPoint(x: bounds.maxX - gradWidth, y: 0.5)
        // end the gradient at the right edge, but
        // probably want to leave the farthest-right 1 or 2 points
        //  completely transparent
        let endPoint = CGPoint(x: bounds.maxX - 2.0, y: 0.5)

        // gradient rect starts at the bottom of the opaque rect
        r.origin.y = r.size.height - 1
        // gradient rect height can extend below the bounds, becuase it will be clipped
        r.size.height = bounds.height
        inContext.addRect(r)
        inContext.clip()
        inContext.drawLinearGradient(gradient, start: startPoint, end: endPoint, options: .drawsBeforeStartLocation)

    }
    
}

Next, we'll make a UILabel subclass that implements that InvertedGradientLayer as a layer mask:

class CornerFadeLabel: UILabel {
    let ivgLayer = InvertedGradientLayer()
    override func layoutSubviews() {
        super.layoutSubviews()
        guard let f = self.font, let t = self.text else { return }
        // we only want to fade-out the last line if
        //  it would be clipped
        let constraintRect = CGSize(width: bounds.width, height: .greatestFiniteMagnitude)
        let boundingBox = t.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font : f], context: nil)
        if boundingBox.height <= bounds.height {
            layer.mask = nil
            return
        }
        layer.mask = ivgLayer
        ivgLayer.lineHeight = f.lineHeight
        ivgLayer.gradWidth = 60.0
        ivgLayer.frame = bounds
        ivgLayer.setNeedsDisplay()
    }
}

and here is a sample view controller showing it in use:

class FadeVC: UIViewController {
    
    let wordWrapFadeLabel: CornerFadeLabel = {
        let v = CornerFadeLabel()
        v.numberOfLines = 1
        v.lineBreakMode = .byWordWrapping
        return v
    }()
    
    let charWrapFadeLabel: CornerFadeLabel = {
        let v = CornerFadeLabel()
        v.numberOfLines = 1
        v.lineBreakMode = .byCharWrapping
        return v
    }()
    
    let normalLabel: UILabel = {
        let v = UILabel()
        v.numberOfLines = 1
        return v
    }()
    
    let numLinesLabel: UILabel = {
        let v = UILabel()
        v.textAlignment = .center
        return v
    }()
    
    var numLines: Int = 0
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemBackground
        
        let sampleText = "This is some example text that will wrap onto multiple lines and fade-out the bottom-right corner instead of truncating or clipping a last line."
        wordWrapFadeLabel.text = sampleText
        charWrapFadeLabel.text = sampleText
        normalLabel.text = sampleText
        
        let stack: UIStackView = {
            let v = UIStackView()
            v.axis = .vertical
            v.spacing = 8
            v.translatesAutoresizingMaskIntoConstraints = false
            return v
        }()
        
        let bStack: UIStackView = {
            let v = UIStackView()
            v.axis = .horizontal
            v.spacing = 8
            v.translatesAutoresizingMaskIntoConstraints = false
            return v
        }()
        
        let btnUP: UIButton = {
            let v = UIButton()
            let cfg = UIImage.SymbolConfiguration(pointSize: 28.0, weight: .bold, scale: .large)
            let img = UIImage(systemName: "chevron.up.circle.fill", withConfiguration: cfg)
            v.setImage(img, for: [])
            v.tintColor = .systemGreen
            v.widthAnchor.constraint(equalTo: v.heightAnchor).isActive = true
            v.addTarget(self, action: #selector(btnUpTapped), for: .touchUpInside)
            return v
        }()
        
        let btnDown: UIButton = {
            let v = UIButton()
            let cfg = UIImage.SymbolConfiguration(pointSize: 28.0, weight: .bold, scale: .large)
            let img = UIImage(systemName: "chevron.down.circle.fill", withConfiguration: cfg)
            v.setImage(img, for: [])
            v.tintColor = .systemGreen
            v.widthAnchor.constraint(equalTo: v.heightAnchor).isActive = true
            v.addTarget(self, action: #selector(btnDownTapped), for: .touchUpInside)
            return v
        }()
        
        bStack.addArrangedSubview(btnUP)
        bStack.addArrangedSubview(numLinesLabel)
        bStack.addArrangedSubview(btnDown)
        
        let v1 = UILabel()
        v1.text = "Word-wrapping"
        v1.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
        
        let v2 = UILabel()
        v2.text = "Character-wrapping"
        v2.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
        
        let v3 = UILabel()
        v3.text = "Normal Label (Truncate Tail)"
        v3.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
        
        stack.addArrangedSubview(bStack)
        stack.addArrangedSubview(v1)
        stack.addArrangedSubview(wordWrapFadeLabel)
        stack.addArrangedSubview(v2)
        stack.addArrangedSubview(charWrapFadeLabel)
        stack.addArrangedSubview(v3)
        stack.addArrangedSubview(normalLabel)

        stack.setCustomSpacing(20, after: bStack)
        stack.setCustomSpacing(20, after: wordWrapFadeLabel)
        stack.setCustomSpacing(20, after: charWrapFadeLabel)

        view.addSubview(stack)
        
        // dashed border views so we can see the lable frames
        let wordBorderView = DashedView()
        let charBorderView = DashedView()
        let normalBorderView = DashedView()
        wordBorderView.translatesAutoresizingMaskIntoConstraints = false
        charBorderView.translatesAutoresizingMaskIntoConstraints = false
        normalBorderView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(wordBorderView)
        view.addSubview(charBorderView)
        view.addSubview(normalBorderView)

        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            
            stack.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
            stack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 60.0),
            stack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -60.0),
            
            wordBorderView.topAnchor.constraint(equalTo: wordWrapFadeLabel.topAnchor, constant: 0.0),
            wordBorderView.leadingAnchor.constraint(equalTo: wordWrapFadeLabel.leadingAnchor, constant: 0.0),
            wordBorderView.trailingAnchor.constraint(equalTo: wordWrapFadeLabel.trailingAnchor, constant: 0.0),
            wordBorderView.bottomAnchor.constraint(equalTo: wordWrapFadeLabel.bottomAnchor, constant: 0.0),
            
            charBorderView.topAnchor.constraint(equalTo: charWrapFadeLabel.topAnchor, constant: 0.0),
            charBorderView.leadingAnchor.constraint(equalTo: charWrapFadeLabel.leadingAnchor, constant: 0.0),
            charBorderView.trailingAnchor.constraint(equalTo: charWrapFadeLabel.trailingAnchor, constant: 0.0),
            charBorderView.bottomAnchor.constraint(equalTo: charWrapFadeLabel.bottomAnchor, constant: 0.0),
            
            normalBorderView.topAnchor.constraint(equalTo: normalLabel.topAnchor, constant: 0.0),
            normalBorderView.leadingAnchor.constraint(equalTo: normalLabel.leadingAnchor, constant: 0.0),
            normalBorderView.trailingAnchor.constraint(equalTo: normalLabel.trailingAnchor, constant: 0.0),
            normalBorderView.bottomAnchor.constraint(equalTo: normalLabel.bottomAnchor, constant: 0.0),
            
        ])
        
        // set initial number of lines to 1
        btnUpTapped()
        
    }
    @objc func btnUpTapped() {
        numLines += 1
        numLinesLabel.text = "Num Lines: \(numLines)"
        wordWrapFadeLabel.numberOfLines = numLines
        charWrapFadeLabel.numberOfLines = numLines
        normalLabel.numberOfLines = numLines
    }
    @objc func btnDownTapped() {
        if numLines == 1 { return }
        numLines -= 1
        numLinesLabel.text = "Num Lines: \(numLines)"
        wordWrapFadeLabel.numberOfLines = numLines
        charWrapFadeLabel.numberOfLines = numLines
        normalLabel.numberOfLines = numLines
    }
}

When running, it looks like this:

enter image description here

The red dashed borders are there just so we can see the frames of the labels. Tapping the up/down arrows will increment/decrement the max number of lines to show in each label.

吲‖鸣 2025-02-15 07:17:42

您应该使用与uilabel相同的文本属性创建catextlayer

用您希望褪色的文字结尾填充它。

然后计算uilabel中此文本段的位置。

终于覆盖了这两个。

以下是解释的一些方面。

You should create a CATextLayer with the same text properties as your UILabel.

Fill it with the end of your text you wish to fade.

Then calculate the position of this text segment in your UILabel.

Finally overlay the two.

Here are some aspect explained.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文