Core Text 实践 +:文字随心所欲摆放

本文介绍如何使用 Core Text 在 iOS 中实现复杂的文字布局,通过创建富文本并拆分文本,使得文字可以在指定区域内自由摆放。讨论了计算行数的方法,并提供了模型优化和实现细节,同时给出了相关往期博客链接。

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

实现下面的效果

Simulator Screen Shot - iPhone 12 mini - 2021-04-01 at 11.50.51.png

可以使用很多控件,

左边红框内,一个 UILabel;

右边红框内,一个 UILabel

很多个 UILabel 码起来


使用 CoreText 可以用一个控件,实现绘制

思路:

先把原文,转化为下面的

截屏2021-04-01 下午2.39.51.png

再把第二张,转化为最初的效果


先要弄出一个类似结构的富文本

保证每一行的 Y 坐标,可控

( 保证非右边红框的内容,与最终一致)

( 保证右边红框的内容,其行数和 y 坐标,与最终一致)

  • 富文本,转化为 CTFrame 一, 出现第二张图,( 把右边红框内的文本,处理掉 )

关键是,把右边红框内的富文本,合理拆分出来

CTFrame 一,宽度 = 右边红框内的富文本的宽度 + 50

  • 右边红框富文本,转化为 CTFrame 二 , 糊到上一步空出的地方

右边红框内的文字,自然不能用 CTFrame 一

一个右边的红框,就是一个 CTFrame 二

给一个字符串,怎么知道它有多少行

字符串,转化为富文本,

富文本,转化为 CTLine,

拿到 CTLine 的宽度 lineWidth,

知道文本框的宽度 containerWidth,

行数 = Int(ceil(lineWidth / containerWidth))

实现:

例子数据

[
      {
        "string" : "世界が終るまでは",
        "type" : 6
      },
      {
        "string" : "世界が終わる前に 聞かせておくれよ",
        "title" : "歌詞:",
        "type" : 7
      },
      {
        "string" : "在世界尽头之前告诉我",
        "title" : "注釈:",
        "type" : 8
      },
      {
        "string" : "世界が終わるまでは 離れる事もない  .  そう願ってた 幾千の夜と  .  戻らない時だけが 何故輝いては",
        "title" : "歌詞:",
        "type" : 7
      },
      {
        "string" : "我要走到世界尽头 . 我希望成千上万的夜晚 . 为什么只在不回来时才闪耀",
        "title" : "注釈:",
        "type" : 8
      }
    ]

转化为

用于右边红框内的占位,和非右边红框区域的渲染

  • 右边红框内的占位

拆分技术,前面有写

从内容 string,拆分出 subList


  ▿ 0 : Coupling
    - string : "世界が終るまでは"
    - type : 6
  ▿ 1 : Coupling
    - string : "世界が終わる前に 聞かせておくれよ"
    - type : 7
    ▿ title : Optional<String>
      - some : "歌詞:"
    ▿ subList : Optional<Array<String>>   // 用于占位, 一个字符串,拆出了两行
      ▿ some : 2 elements
        - 0 : "ha ha ha"
        - 1 : "ha ha ha"
  ▿ 2 : Coupling
    - string : "在世界尽头之前告诉我"
    - type : 102
    ▿ title : Optional<String>
      - some : "注釈:"
  ▿ 3 : Coupling
    - string : "世界が終わるまでは 離れる事もない  .  そう願ってた 幾千の夜と  .  戻らない時だけが 何故輝いては"
    - type : 7
    ▿ title : Optional<String>
      - some : "歌詞:"
    ▿ subList : Optional<Array<String>>  // 用于占位, 一个字符串,拆出了3行
      ▿ some : 3 elements
        - 0 : "ha ha ha"
        - 1 : "ha ha ha"
        - 2 : "ha ha ha"
  ▿ 4 : Coupling
    - string : "我要走到世界尽头 . 我希望成千上万的夜晚 . 为什么只在不回来时才闪耀"
    - type : 8
    ▿ title : Optional<String>
      - some : "注釈:"
    ▿ subList : Optional<Array<String>>  // 用于占位, 一个字符串,拆出了3行
      ▿ some : 3 elements
        - 0 : "ha ha ha"
        - 1 : "ha ha ha"
        - 2 : "ha ha ha"

和红框内绘制信息


struct Renderer{
     // 红框内,每一段的绘制信息
    let paragraph: [ParagraphRenderer]
}


struct ParagraphRenderer{
    // 对应实际绘制的开始行
    let lineIdx: Int
    // 右边红框对应的文本
    let content: String
    // 右边红框对应的行数
    let cnt: Int
    // 左框对应的文本
    let t: String
    // 确定样式
    let beBlack: Bool
    
    // 右边红框用于绘制的信息
    var lines = [CTLine]()
}

模型计算部分,见底部 github repo

计算思路,可参考之前的博客,见底部

绘制
        //        y 坐标控制
        // Core Text 坐标,原点在左下,
        // 先对 UIKit 坐标系翻转,
        // lastY, 是行距的累计
        var lastY: CGFloat = 0

        var index = 0
        let limit = info.paragraph.count
        // 段落开始行距的记录
        var startIndex: Int? = nil
       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]
                lineOrigin.x = TextContentConst.padding + lineOrigin.x
                // ...
                // y 坐标控制
                
                let yOffset = lineOrigin.y - lineDescent - 20
                if i == info.lineIndex{
                    ctx.draw(line: yOffset)
                }
                ctx.textPosition = lineOrigin
                // 绘制记录,
                // 如果绘制了红框内的内容,
                // 就不要绘制 CTFrame 一 的内容了 
                var toDraw = false
                // 继续处理段落的剩余行
                if let f = startIndex{
                    if i >= f{
                        // 段落画完了
                        startIndex = nil
                    }
                    else{
                        //  绘制段落的剩余行
                        let biscuit = info.paragraph[index - 1]
                        ctx.textPosition = CGPoint(x: lineOrigin.x + TextContentConst.indentSecond, y: lineOrigin.y)
                        //  startIndex = i + biscuit.cnt 的时机,
                        //  biscuit.cnt + i - f = 初始行
                        // i 增加,对应段落的剩余行
                        CTLineDraw(biscuit.lines[biscuit.cnt - (f - i)], ctx)
                        toDraw = true
                    }
                }
                
                
                // 逻辑的入口,
                // 找到了第一段的第一行
                if index < limit, info.paragraph[index].lineIdx == i{
                
                    let biscuit = info.paragraph[index]
                    // 左边红框的信息
                    let attributedStr: NSAttributedString = biscuit.t.seven(toBreak: false)
                    
                    let ln = CTLineCreateWithAttributedString(attributedStr)
                    // 画左边红框
                    CTLineDraw(ln, ctx)
                    ctx.textPosition = CGPoint(x: lineOrigin.x + TextContentConst.indentSecond, y: lineOrigin.y)
                    // 画右边红框的第一行
                    CTLineDraw(biscuit.lines[0], ctx)
                    toDraw = true
                    startIndex = i + biscuit.cnt
                    index += 1
                }
                if toDraw == false{
                    CTLineDraw(line, ctx)
                }
       }
模型优化

case 7:   // 只是一个记号
                // ... 
                let attributedStr = NSAttributedString()
                let lnTwo = CTLineCreateWithAttributedString(attributedStr)
                let w = lnTwo.lnSize.width
                let cnt = w / TextContentConst.widthBritain
                var m = model
                let len = Int(ceil(cnt))
                if len > 1{
                    // 右边红框超过一行,放在 Frame 2
                    (1..<len).forEach {
                        acht.append($0 + enLineIdx)
                    }
                    m.subList = [String](repeating: product, count: len)
                    infoEn.append(ParagraphRenderer(lineIdx: enLineIdx, content: model.string, cnt: len, t: m.title ?? "", beBlack: true))
                }
                else{
                    // 右边红框等于一行,放在 Frame 1 中
                    m.type = 101   // 只是一个记号
                }
                info.append(m)
                enLineIdx += len
往期博客:

Core Text 实践 +:自动换行,与字级别的控制

Core Text 实践:自定义每个字的位置

github repo
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值