本文介绍下,使用 Core Text
, 处理不规则文本的省略号,
换一种说话,
本文介绍下,使用 Core Text
, 展示奇特样式的文本效果
哪里奇特了?
看下面两张图,
第一张中间的描述信息, 文本不超过三行,就都给展示了
第 2 张中间的描述信息, 文本超过三行,
就打省略号,加一个全文展开按钮
奇特在于,不规则的文本形状
全文展开按钮,挡住了一部分的文本,
第三行的文本展示宽度,窄于前两行的文本展示宽度
实现思路:
处理的有点绕,
拿到前两行文本展示宽 ( 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
和 本来号的博客