如何实现多行UILabel最后一行末尾淡出 适配UITableViewCell场景
UILabel 任意行数仅最后一行末尾淡出效果实现
现存方案问题
需要实现兼容UILabel 1/2/3等任意行数场景的文本末尾淡出效果,现有两类方案都存在缺陷:
- 单行场景下常用的直接给UILabel图层添加CAGradientLayer遮罩的方案,无法适配多行场景:遮罩会覆盖整个图层,导致所有行的文本末尾都出现淡出效果,不符合预期。
- 自行添加带渐变遮罩的子图层、估算定位到最后一行位置的方案,静态场景下可正常生效,但放在UITableViewCell中使用时,cell点击高亮变灰的场景下,自定义添加的图层会直接显露。问题根源有两点:一是自定义子图层硬编码了白色背景,系统给cell设置高亮色时不会修改这个子图层的颜色;二是用「总高度/行数」的方式估算最后一行位置,没有考虑行间距、自定义字体行高的差异,位置计算存在偏差。
原有存在缺陷的代码如下:
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) } }
目标效果如下图:
需要满足的核心要求:
- 支持任意行数的多行文本显示
- 仅最后一行的末尾实现淡出渐变效果
- 适配UITableViewCell使用场景,cell点击高亮、选中整体变色时不会出现图层显露异常
正确实现方案
核心思路是放弃添加额外带背景色的子图层,通过TextKit框架精准计算最后一行文本的实际渲染位置,直接给UILabel的layer设置mask遮罩:遮罩除最后一行末尾的渐变区域外全部为不透明状态,既不会影响其他行的显示,也不会因为额外子图层干扰系统的高亮色渲染。
完整实现代码如下:
import UIKit extension UILabel { private struct AssociatedKeys { static var fadeMaskKey = "fadeMaskKey" } /// 仅对最后一行末尾应用淡出渐变效果 /// - Parameter gradientWidth: 渐变区域宽度,默认32pt func applyLastLineFade(gradientWidth: CGFloat = 32) { // 移除旧遮罩避免重复叠加 if let oldMask = objc_getAssociatedObject(self, &AssociatedKeys.fadeMaskKey) as? CALayer { oldMask.removeFromSuperlayer() layer.mask = nil } guard bounds.width > 0, let font = font else { return } let contentAttrText = attributedText ?? NSAttributedString( string: text ?? "", attributes: [.font: font] ) guard !contentAttrText.string.isEmpty else { return } // 通过TextKit计算最后一行文本的实际渲染frame let textStorage = NSTextStorage(attributedString: contentAttrText) let layoutManager = NSLayoutManager() let textContainer = NSTextContainer(size: CGSize( width: bounds.width, height: .greatestFiniteMagnitude )) textContainer.lineBreakMode = lineBreakMode textContainer.maximumNumberOfLines = numberOfLines textContainer.lineFragmentPadding = 0 layoutManager.addTextContainer(textContainer) textStorage.addLayoutManager(layoutManager) let glyphRange = layoutManager.glyphRange(for: textContainer) var lastLineRect: CGRect = .zero layoutManager.enumerateLineFragments(forGlyphRange: glyphRange) { _, usedRect, _, _, _ in lastLineRect = usedRect } guard !lastLineRect.isEmpty else { return } // 构建遮罩层:整体不透明,仅最后一行末尾做alpha渐变 let maskLayer = CALayer() maskLayer.frame = bounds maskLayer.backgroundColor = UIColor.white.cgColor // 白色对应mask不透明 let gradientMask = CAGradientLayer() // 渐变仅修改alpha值,不改变颜色 gradientMask.colors = [ UIColor.white.withAlphaComponent(1).cgColor, UIColor.white.withAlphaComponent(0).cgColor ] gradientMask.startPoint = CGPoint(x: 0, y: 0.5) gradientMask.endPoint = CGPoint(x: 1, y: 0.5) // 渐变区域对齐最后一行末尾,适配文本未占满整行的场景 let gradientStartX = max(lastLineRect.maxX - gradientWidth, lastLineRect.minX) gradientMask.frame = CGRect( x: gradientStartX, y: lastLineRect.minY, width: min(gradientWidth, lastLineRect.width), height: lastLineRect.height ) maskLayer.addSublayer(gradientMask) layer.mask = maskLayer // 关联保存遮罩层,方便下次更新时清理 objc_setAssociatedObject( self, &AssociatedKeys.fadeMaskKey, maskLayer, .OBJC_ASSOCIATION_RETAIN_NONATOMIC ) } }
使用注意事项
- 方法需要在布局完成后调用,推荐重写UILabel的
layoutSubviews方法,在super.layoutSubviews()之后调用applyLastLineFade()即可自动适配布局变化。 - 在UITableViewCell中使用时,在cell的
layoutSubviews方法中调用对应label的该方法即可,系统处理cell高亮、选中状态时不会出现图层显露问题。 - 自动布局、手动布局、自定义行高、带行间距的富文本场景都可兼容,TextKit会返回和系统渲染完全一致的最后一行位置,不会出现偏移。
内容的提问来源于stack exchange,提问作者N. Rysin
相关产品推荐
相关产品推荐

