Core Text 雕虫小技: 建立指示行

505 阅读3分钟

本文使用 Core Text 实现这个效果,滚动文本,中间那一行变红

IMG_0031.PNG

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

前文等,已经实现了,

使用 CoreText, 可以自定义的控制每一行的位置

思路

滚动的时候,使用一个计时器,不停的重绘,setNeedsDisplay()

找出中间的那一行,变红

其它行,维持原样

实现

父视图, 滚动视图



// 滚动视图
class ReadScrollV: UIScrollView {
    
    //  滚动视图,放置一个文本绘制视图
    fileprivate lazy var ccc = TxtViewCustom()  

    var timer: Timer?
   
    func setup(){
        let t: TimeInterval = 0.1
        timer = Timer.scheduledTimer(timeInterval: t , target: self, selector: #selector(ReadScrollV.loops), userInfo: nil, repeats: true)
        timer?.fire()
        if timer != nil{
            RunLoop.main.add( timer! , forMode: RunLoop.Mode.common)
        }
    }
    
    
    @objc func loops(){
        if isDragging || isTracking{
            // 滚动的时候,重绘
        
            ccc.criteria = contentOffset.y + TxtCustomConst.kLnTop
            ccc.setNeedsDisplay()
        }
    }
}

子视图,文本绘制

  • 找到需要高亮的那一行 CTLine

直接操作,改颜色刷新,暂无好办法

  • 找到高亮行 CTLine,

通过 CTLineGetStringRange,获取在那一帧 frame 的范围,

再操作那一帧 frame 原始文本,

绕一圈,就可以高亮了

class TxtViewCustom: UIView{
    
    var frameRef:CTFrame?   // 文本,转化为一帧

    
    var criteria = TxtCustomConst.kLnTop     // 滚动的时候,在变
    
    
    var contentPageX: NSAttributedString?    // 富文本

    
 
    // 绘制文本
    override func draw(_ rect: CGRect){
       guard let ctx = UIGraphicsGetCurrentContext(), let f = frameRef, let lineIndex = txtRenderX 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 final: CGFloat = 0
        var first: CGFloat? = nil
        var lastY: CGFloat = -16
        var toRender:Bool? = 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 = TxtCustomConst.padding + lineOrigin.x
                
                lineOrigin.y += lastY
                
                if i == lineIndex{
                    let yOffset = lineOrigin.y - lineDescent - 20
                    ctx.line(draw: yOffset)
                }
                if i <= lineIndex{
                    lastY -= 11
                }
                else{
                    lastY -= 7
                }
                ctx.textPosition = lineOrigin
                // 前面是一些,
                // 每一行的 y 坐标控制
                if first == nil{
                    first = lineOrigin.y
                }
                let typoH = lineAscent + lineDescent
                final = lineOrigin.y - typoH
                let oneX: CGFloat = first ?? 0
                
                
                // 这个判断依据,靠经验
                // 效果还可以
                if toRender == nil, oneX - final + typoH * 2 - 10 >= criteria{
                    toRender = true
                    // 绘制两条辅助线
                    ctx.line(red: final + typoH * 2)
                    ctx.line(red: final )
                }
        
                if let re = toRender, re{
                    // 高亮绘制
                    toRender = false
                    // 找到了所在行
                    let lineRange = CTLineGetStringRange(line)
                    
                    let range = NSMakeRange(lineRange.location == kCFNotFound ? NSNotFound : lineRange.location, lineRange.length)
                    // 找到对应的文本范围
                    if let content = contentPageX{
                        let sub = content.string[range.location..<(range.location + range.length)]
                        let new = String(sub)
                        // 新建文本,新建高亮行
                        let lnTwo = CTLineCreateWithAttributedString(new.highLn)
                        // 高亮绘制
                        CTLineDraw(lnTwo, ctx)
                    }
                }
                else{
                    // 普通绘制
                    CTLineDraw(line, ctx)
                }
       }
        // ...
    }
    

}




补充:



// 滚动视图
class ReadScrollV: UIScrollView {
    
   // 滚动的时候,需要高亮的那一行,在变动
   // 需要加上 contentOffset.y
   
    @objc func loops(){
            ccc.criteria = contentOffset.y + TxtCustomConst.kLnTop
            ccc.setNeedsDisplay()
    }
}

复习

为了顺利绘制那一帧,

将富文本传入子视图,

子视图使用固定的文本宽,计算出一个合适的文本高

合适的文本高乘上安全系数 ( 这里取 3 ),

父视图设置子视图的 frame 和自个的 contentSize

再去绘制


class ReadScrollV: UIScrollView {
    
    fileprivate lazy var ccc = TxtViewCustom()
    
    var s: CGSize?
    
    var contentPage: NSAttributedString?{
        didSet{
        
            /*
            将富文本传入子视图,

            子视图使用固定的文本宽,计算出一个合适的文本高

            合适的文本高乘上安全系数 ( 这里取 3 )
            
            */
            ccc.contentPageX = contentPage
            s = ccc.s
            
            /*
            父视图设置子视图的 frame 和自个的 `contentSize`

            再去绘制
            
            */
            if let sCont = s{
                let f = CGRect(x: 0, y: 0, width: UI.std.width, height: sCont.height)
                ccc.frame = f
                contentSize = f.size
            }
            ccc.setNeedsDisplay()
        }
    }
    
}

绘制完成后,拿到子视图中 func draw(_ rect: CGRect) 方法中,

计算出来的切合实际的高度,调整父视图的 contentSize


extension ReadScrollV: DrawDoneProxy{
    func done(height h: CGFloat){
        let cccS = ccc.frame.size
        contentSize = CGSize(width: cccS.width, height: max(h + 400, UI.std.height - CGFloat(64 * 2) - 40 + 8))
    }
}


此时不能再,调整子视图的 frame

调小了,这一帧,就像一幅画被压缩了

默认是,这一帧,就像一幅很长的画,被截取了有字的部分,正常展示

github repo