Core Text: 不规则文本的省略号,处理思路

本文详细介绍了如何使用 Core Text 在 iOS 中处理不规则文本形状的省略号显示,包括当文本超过三行时如何添加省略号和全文展开按钮。文章通过具体的实现思路、补充细节和完善功能等方面进行阐述,并提供了场景加强和GitHub仓库链接以供深入研究。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文介绍下,使用 Core Text, 处理不规则文本的省略号,

换一种说话,

本文介绍下,使用 Core Text, 展示奇特样式的文本效果

哪里奇特了?

看下面两张图,

第一张中间的描述信息, 文本不超过三行,就都给展示了

截屏2021-07-21 上午9.31.18.png


第 2 张中间的描述信息, 文本超过三行,

就打省略号,加一个全文展开按钮

截屏2021-07-21 上午9.31.52.png


奇特在于,不规则的文本形状

全文展开按钮,挡住了一部分的文本,

第三行的文本展示宽度,窄于前两行的文本展示宽度


实现思路:

处理的有点绕,

拿到前两行文本展示宽 ( UI.std.width - CGFloat( 16 * 2 ) ),

算出一个文本帧, CTFrame

  • 如果文本不超过三行,

拿到文本帧的每一行,都给绘制了

  • 文本超过三行,

拿到文本帧的前两行,直接绘制了

拿到文本帧的第 3 行,取得 line range,

找出第三行对应的的文本,new

拿到第三行文本展示宽 ( UI.std.width 屏幕宽 - 16 左边距 - offsetRhs 右边距 ),

右边距 let offsetRhs: CGFloat = 28 按钮宽 + 29 按钮的右边距 + 10 按钮的左边距 + 5 留给省略号的空间

建立第二文本帧 frameInner,

拿到第二文本帧 frameInner 的第一行 CTLine, 找到其对应的文本,subSecond, 同样是通过 line range

subSecond, 添加省略号,建立 CTLine, 并绘制


class FrameZeroLabel: UIView{

    // 完整的文本帧
    var frameRef: CTFrame?
    
    // 完整的文本
    var contentInfo: String?
    
    
    // 展示省略号
    var showDot = false
    
    
    init() {
        super.init(frame: CGRect.zero)
        backgroundColor = UIColor.white

    }
    
    
    
    override func draw(_ rect: CGRect) {
        
        
        guard let ctx = UIGraphicsGetCurrentContext(), let f = frameRef, let content = contentInfo else{
            return
        }
         
        let xHigh = bounds.size.height
        ctx.textMatrix = CGAffineTransform.identity
        ctx.translateBy(x: 0, y: xHigh)
        ctx.scaleBy(x: 1.0, y: -1.0)
        guard let lines = CTFrameGetLines(f) as? [CTLine] else{
            return
        }
        let lineCount = lines.count
        guard lineCount > 0 else {
            return
        }
        
        
        let total = max(lineCount, 3)
        
        var originsArray = [CGPoint](repeating: CGPoint.zero, count: lineCount)
        //用于存储每一行的坐标
        CTFrameGetLineOrigins(f, CFRangeMake(0, 0), &originsArray)
         var lastY: CGFloat = 0
         
         var frameY:CGFloat              = 0
         
        for i in 0..<total{
                 var lineAscent:CGFloat      = 0
                 var lineDescent:CGFloat     = 0
                 var lineLeading:CGFloat     = 0
                 CTLineGetTypographicBounds(lines[i] , &lineAscent, &lineDescent, &lineLeading)
                 
                 var lineOrigin = originsArray[i]
                 
                 switch i {
                 case 0:
                     frameY = lineOrigin.y
                 default:
                     lastY -= 1
                     frameY = frameY - (lineAscent + lineDescent)
                     //减去一个行间距,再减去第二行,字形的上部分 (循环)
                     lineOrigin.y = frameY
                 }
                         
                 lineOrigin.y += lastY
                // 调整成所需要的坐标
                 ctx.textPosition = lineOrigin
                 
                 // 以上是常规处理
                 // 以前的博客,写过不少
                 switch i {
                 case 0, 1:
                    // 绘制前两行
                    CTLineDraw(lines[i], ctx)
                    
                 default:
                    // 绘制第 3 行
                    // 2
                    if showDot{
                        // 有省略号
                        let lineRange = CTLineGetStringRange(lines[i])
                                            
                        let range = NSMakeRange(lineRange.location == kCFNotFound ? NSNotFound : lineRange.location, lineRange.length)
                        // 找到对应的文本范围
                        
                        let sub = content[range.location..<(range.location + range.length)]
                        let new = String(sub)
                        
                        ///
                        
                        let page = new.plainX
                        
                        let calculatedSize = page.height(bound: 1000)
                        let offsetRhs: CGFloat = 28 + 29 + 10 + 5
                        let siZ = CGSize(width: UI.std.width - 16 - offsetRhs, height: calculatedSize.height * 3)
                        // 第二帧
                        let framesetter = CTFramesetterCreateWithAttributedString(page)
                        let path = CGPath(rect: CGRect(origin: CGPoint.zero, size: siZ), transform: nil)
                        let frameInner = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, nil)
                        
                        if let lns = CTFrameGetLines(frameInner) as? [CTLine], lns.count > 0{
                            let lineRangeSecond = CTLineGetStringRange(lns[0])
                            let rangeSecond = NSMakeRange(lineRangeSecond.location == kCFNotFound ? NSNotFound : lineRangeSecond.location, lineRangeSecond.length)
                            // 找到对应的文本范围
                            
                            let subSecond = new[rangeSecond.location..<(rangeSecond.location + rangeSecond.length)]
                            let newSecond = String(subSecond) + "..."
                            
                            let lnSecond = CTLineCreateWithAttributedString(newSecond.plainX)
                            CTLineDraw(lnSecond, ctx)
                        }
                        
                    }
                    else{
                        // 无省略号
                        
                        CTLineDraw(lines[i], ctx)
                    }
                    
                    
                    
                }
        }
    }
    

}

补充细节:

怎样计算,有没有省略号?

从文本帧 CTFrame 里面取行 CTLine 的列表, 看其数目


     if let intro = m.introduction{
            
            let page = intro.plainX
            let calculatedSize = page.height(bound: 3000)
            let siZ = CGSize(width: UI.std.width - CGFloat( 16 * 2 ), height: calculatedSize.height * 3)
            
            // 建立 core text 文本
            let framesetter = CTFramesetterCreateWithAttributedString(page)
            let path = CGPath(rect: CGRect(origin: CGPoint.zero, size: siZ), transform: nil)
            
            // 前面说过的,建立第一帧
            let frameRef = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, nil)
            
            if let lines = CTFrameGetLines(frameRef) as? [CTLine], lines.count > 2{
                    //       实际的业务,更加精彩,
                    //    展示 2 行,一个文本宽
                    //    展示 3 行,一个文本宽
                    //    具体见 github repo, 这里略
                    topX_bottomY += 20
                    top_theBottomConstraint?.constraint.update(offset: topX_bottomY.neg)
                
                    let toHid = (  lines.count <= 3  )
                    // 计算出了,有没有省略号
                    midTxt.zero.showDot = (  toHid == false  )
                    expandB.isHidden = toHid   // 隐藏,展示更多文本的按钮
                    //...
            }
            // ...
            
        }

完善功能:

展开按钮,把文本展开和收起

  • 1, 需要两张视图,一张是删节版,如两张上图

一张是完整版,容易理解

不贴图了

  • 2, 第二张视图的完整呈现,

需要拿到完整文本帧的实际高度

这里是,渲染后,就拿到了

( 因为渲染的时候,可以自定义文本的间距 )

class FrameOneLabel: UIView {

    var frameRef: CTFrame?
    
    weak var delegate: DrawDoneProxy?
    
    init() {
        super.init(frame: CGRect.zero)
        
        isHidden = true
        backgroundColor = UIColor.white
        
    }
    
   
    override func draw(_ rect: CGRect){
       guard let ctx = UIGraphicsGetCurrentContext(), let f = frameRef else{
           return
       }
        
       let xHigh = bounds.size.height
       ctx.textMatrix = CGAffineTransform.identity
       ctx.translateBy(x: 0, y: xHigh)
       ctx.scaleBy(x: 1.0, y: -1.0)
       guard let lines = CTFrameGetLines(f) as? [CTLine] else{
           return
       }
       let lineCount = lines.count
       guard lineCount > 0 else {
           return
       }

       var originsArray = [CGPoint](repeating: CGPoint.zero, count: lineCount)
       //用于存储每一行的坐标
       CTFrameGetLineOrigins(f, CFRangeMake(0, 0), &originsArray)
       // 以上为,常规操作
       
       
       // 这个是,文本的间距控制
   
        var lastY: CGFloat = 0
        
        
        var final: CGFloat = 0
        var first: CGFloat? = nil
        
        var frameY:CGFloat              = 0
        
       for (i,line) in lines.enumerated(){
                var lineAscent:CGFloat      = 0
                var lineDescent:CGFloat     = 0
                var lineLeading:CGFloat     = 0
                CTLineGetTypographicBounds(line , &lineAscent, &lineDescent, &lineLeading)
                
                var lineOrigin = originsArray[i]

                
                switch i {
                case 0:
                    frameY = lineOrigin.y
                default:
                    lastY -= 1
                    frameY = frameY - (lineAscent + lineDescent)
                    //减去一个行间距,再减去第二行,字形的上部分 (循环)
                    lineOrigin.y = frameY
                }
                        
                lineOrigin.y += lastY
               // 调整成所需要的坐标
                ctx.textPosition = lineOrigin
                
                CTLineDraw(line, ctx)
                
                if first == nil{
                    // 第一行的 y 坐标
                    first = lineOrigin.y
                }
                let typoH = lineAscent + lineDescent
                // 最后一行的 y 坐标
                final = lineOrigin.y - typoH
       }
        let one: CGFloat = first ?? 0
        // 文本帧的高度 = 第一行 - 最后一行
        let h = one - final
        
        // 拿到文本帧的高度
        delegate?.done(height: h)
    }
}

复习重点:

FrameZeroLabel 容器视图的重要性

容器视图的布局,依赖外部的变化

CTFrame 绘制的视图,特别是第二张,必须依赖自己算出来的 frame

  • CTFrame 绘制的视图, 如果依赖外部的变化,

就很容易拉伸和压缩

  • FrameZeroLabel 容器视图,clipsToBounds

就可以按照需求展示了

场景加强

上面的功能,添加下面的效果

请添加图片描述

  • 记录前后高度

  • 初始化效果

使用上一步,记录的高度

具体见 GitHub repo

本来号的博客


github repo

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值