图文混排版可以说在很多地方都常用,但真正想实现一些高级的效果还是要耗费不少的精力去系统的学习的。图文混排实现的方式一般有两种方式:基于Html5和苹果提供的框架(TextKit或者CoreText)。。
先简单说下第一种方式,一般是通过OC和JS相互调用实现的,其中OC和JS相互调用可以有三种实现方式分别是UIWebVIew,WKWebView,以及JavaScriptCore三种方式。UIWebView虽然比较稳定但是占用的资源比较多,WKWebView占用资源相对于UIWebView来说很小,但是目前来看依然没有得到较为广泛的应用,原因在于WKWebView在稳定性方面依然存在一些细小的问题。三种简单的调用方式直接提供给源码给大家看一下https://github.com/ZhengYaWei1992/1.JSAndOC,后续的话会在之后的文章中更新。Tips:更新介绍的内容含有JS和OC相互调用传参问题、第三方模板框架使用问题、以及类似网易新闻这种图文混排界面是如何实现的。之前有些过一个小小的实现类似网易新闻详情界面实现的小Demo,之后一并分享给大家。这和不是今天的最主要话题,扯的有点多了。
接下来先说下第二种实现方式,TextKit或者CoreText。注意是或者,其实之前有段时间非常强烈的认真学习过一写和CoreText相关的东西,但是学者学着真是无力吐槽了。发现实现各简单的图文混排效果实在是太繁琐了,后来果断放弃。不过现在发现,以前放弃学习CoreText确实是明智之举,因为iOS7之后有个比它更好使用,更容易实现图文混排效果的框架TextKit(后来才知道这个框架,之前走了不少的弯路)。
先来展示下,之前自己封装的一个图文混排效果图。可以自动识别表情,URL和昵称,当然还可以识别更多(最主要的是正则表达式的应用)。使用起来也十分简单,直接继承与ZWLabel这个类就可以。暂时不放代码,后续再继续更新,先看看效果。
本文先从实现一个简单的自定义识别网址的图文混排开始。初体验之效果图:
学习TextKit的第一步:是需要认识里面的三个类NSTextStorage、NSLayoutManager、NSTextContainer分别的作用和职责。三者的职责:NSTextStorage负责文本存储,NSLayoutManager负责文本字形布局,NSTextContainer设定文本绘制的范围(一般开发很少用,定义一个矩形区域,也可以定义一个排除路径)。第二步:要认清实现自定义识别网址的Label的本质是:使用textKit接替label的底层实现。第三步:实现自定义识别网址URL的步骤:1.使用textKit接替label的底层实现 ---绘制textStorage的文本内容。2.使用正则表达式过滤URL。3.解决交互问题。
开始上主菜代码了,先来个简单的Swift代码。具体Demo链接,请在文章末尾查看。
1.ZWLabel.swift中
```
import UIKit
class ZWLabel: UILabel {
lazy var textStorage = NSTextStorage()
lazy var layoutManager = NSLayoutManager()
lazy var textContainer = NSTextContainer()
override var text: String?{
didSet{
prepareTextContent()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.orange
prepareTextSystem()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
backgroundColor = UIColor.orange
prepareTextSystem()
}
override func drawText(in rect: CGRect){
//消除父类的绘制,这里我们自己绘制 // super.drawText(in: rect)
let range = NSRange(location: 0, length: textStorage.length)
layoutManager.drawBackground(forGlyphRange: range, at: CGPoint())
//绘制Glyph字形 CGPoint()表示从原点开始绘制
layoutManager.drawGlyphs(forGlyphRange: range, at: CGPoint())
}
override func layoutSubviews() {
super.layoutSubviews()
//指定文本的绘制区域
textContainer.size = bounds.size
}
override func touchesBegan(_ touches: Set, with event: UIEvent?) {
//1.获取用户点击的位置
//first?相当于OC的anyObject
guard let location = touches.first?.location(in: self)else{
return
}
print("点我了\(location)")
//2.记录当前点中字符的索引
let idx = layoutManager.glyphIndex(for: location, in: textContainer)
print("点我了\(idx)")
//3.判断idx是否在url的range范围内,如果在就高亮
for r in urlRanges ?? []{
//NSRanege跳入头文件可以看见次方法 NSLocationInRange
if NSLocationInRange(idx, r){
print("需要高亮")
//修改文本的字体属性
textStorage.addAttributes([NSForegroundColorAttributeName:UIColor.blue], range: r)
//如果需要重新绘制,需要调用setNeedsDisplay方法
setNeedsDisplay()
}else{
print("没点到")
}
}
}
}
// MARK: - 设置textKit核心对象
private extension ZWLabel{
///准备文本系统
func prepareTextSystem(){
//0.开启用户交互
isUserInteractionEnabled = true
//1.准备文本内容
prepareTextContent()
//2.设置对象的关系 ---一般都是固定的写法
textStorage.addLayoutManager(layoutManager)
layoutManager.addTextContainer(textContainer)
}
/// 准备文本内容,接管label内容
func prepareTextContent(){
if let attributedText = attributedText{
textStorage.setAttributedString(attributedText)
}else if let text = text{
textStorage.setAttributedString(NSAttributedString(string: text))
}else{
textStorage.setAttributedString(NSAttributedString(string: ""))
}
// print(urlRanges)
//遍历范围数组,设置URL文字的属性
for r in urlRanges ?? []{
textStorage.addAttributes([NSForegroundColorAttributeName:UIColor.red,NSBackgroundColorAttributeName:UIColor.init(white: 0.9, alpha: 1.0)], range: r)
}
}
}
// MARK: - 正则表达式获取url范围
extension ZWLabel{
///返回textStorage中的URL range数组
var urlRanges: [NSRange]?{
let pattern = "[a-zA-Z]*://[a-zA-Z0-9/\\.]*"
guard let regx = try? NSRegularExpression(pattern: pattern, options: [])else{
return nil
}
//2.多重匹配,因为一个字符串中可能包含多个url
let matches = regx.matches(in: textStorage.string, options: [], range:NSRange(location: 0, length: textStorage.length))
//3.遍历数组,生成range数组
var ranges = [NSRange]()
for m in matches{
ranges.append(m.rangeAt(0))
}
return ranges
}
}
```
2.外部调用形式
```
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var label: ZWLabel!
override func viewDidLoad() {
super.viewDidLoad()
label.text = "请点击http://www.baidu.com"
}
}
```
当然以上代码只是简单的认识一下TextKit这个框架,实际上想真正的完成一个自动识别URL,添加表情图片的图文混排label还是有很多事情要做的,比如还要考虑行高、字体间距、点击时显示的效果等因素。实际上添加上表情就是相对比较简单的事情了,主要是通过属性文本,代码如下(具体实现自行添加):
```
//0.图片附件
let attachment = NSTextAttachment()
attachment.image = #imageLiteral(resourceName: "d_aini.png")
let height = label.font.lineHeight
attachment.bounds = CGRect(x: 0, y: -4, width: height, height: height)
//1.属性文本
let imageStr = NSAttributedString(attachment:attachment )
//2.可变的图文字符串 ----注意:这里可变的要用NSMutableAttributedString,用var不起作用
let attStrM = NSMutableAttributedString(string: "我")
attStrM.append(imageStr)
attStrM.append(NSAttributedString(string: "99!"))
//3.设置label
label.attributedText = attStrM
```
Demo参考链接地址:https://github.com/ZhengYaWei1992/BaseTextKit