UILabel实现UITextFiled效果

实现方案

在iOS6中, UILabel, UITextField, UITextView都是基于string Drawing 和 WebKit构建的



iOS7, 苹果引入了Textkit, TextKit位于Core Text之上, TextViews(指UILabel, UITextField, UITextView等文本控件)之下, 相比于TextViews直接提供的api, TextKit有更灵活的接口, 能够实现文字排版和渲染. 由图可以看出, UITextFiled是基于Textkit实现的, 完全可以用Textkit将UILabel配置成一个UITextFiled.


Textkit

Text Kit

Text Kit中的对象
  • Text View是用来显示文本内容的控件,主要包括UILabel、UITextView和UITextField。
  • Text containers对应着NSTextContainer类。NSTextContainer定义了文本可以排版的区域。一般来说,都是矩形区域,当然,也可以根据需求,通过子类化NSTextContainer来创建别的一些形状,例如圆形、不规则的形状等。NSTextContainer不仅可以创建文本可以填充的区域,它还维护着一个数组——该数组定义了一个区域,排版的时候文字不会填充该区域,因此,我们可以在排版文字的时候,填充非文本元素(例如图片,如图4所示)。
  • Layout manager对应着NSLayoutManager类。该类负责对文字进行编辑排版处理——通过将存储在NSTextStorage中的数据转换为可以在视图控件中显示的文本内容,并把统一的字符编码映射到对应的字形(glyphs)上,然后将字形排版到NSTextContainer定义的区域中。
  • Text storage对应着NSTextStorage类。该类定义了Text Kit扩展文本处理系统中的基本存储机制。NSTextStorage继承自NSmutableAttributedString,主要用来存储文本的字符和相关属性。另外,当NSTextStorage中的字符或属性发生了改变,会通知NSLayoutManager,进而做到文本内容的显示更新。

这其实就是一个MVC模型, 通常情况下, NSTextStorage、NSLayoutManager和NSTextContainer是一一对应的, 当然在需要分页排版的时候, 也可以一对多.

Core Text

从实现上来看, Text Kit 和Core Text都可以实现将UILabel配置成UITextFiled那样的控件, Core Text是将文本直接渲染到图形上下文, 功能更加强大, 性能更好, 能够完全控制每个字型(CTRun对象)的渲染, 但是接口也相对复杂. Text Kit是对Core Text的一层封装, 能够解决一些简单文字的排版问题. 考虑到项目需求, TextKit足以完成.


Core Text中的对象

字型(Glyphs)

字符 + 字体 = 字型

功能实现

文本显示

配置NSTextStorage、NSLayoutManager和NSTextContainer的依赖关系:

 [_textStorage addLayoutManager:_layoutManager];
 [_layoutManager addTextContainer:_textContainer];

自定义UILabel 的drawTextInRect:方法

- (void)drawTextInRect:(CGRect)rect {
     NSRange range = NSMakeRange(0, self.textStorage.length);

    //绘制背景
    [_layoutManager drawBackgroundForGlyphRange:range atPoint:CGPointMake(kLeftPadding, kTopPadding)];
    //绘制文字
    if (_textStorage.length > 0) {
        [_layoutManager drawGlyphsForGlyphRange:range atPoint:CGPointMake(kLeftPadding, kTopPadding)];
    }
}

文本滚动

当文本超过输入框的宽度时, 继续编辑文字需要有文字滚动, 其实就是在绘制的时候, 修改绘制起点, 以达到滚动的效果

    [_layoutManager drawGlyphsForGlyphRange:range atPoint:CGPointMake(kLeftPadding, kTopPadding)];

文本编辑

insert

    [_textStorage insertAttributedString:attrString atIndex:_glyphIndex];
  1. 文本不超过输入框的 insert, 此时的 insert 只需修改NSTextStorage中的字符, 然后刷新光标位置即可:
image.png

2.文本超过输入框大小, 最后一个字符可见, 即左边有隐藏文字, 右边无隐藏文字, 此时仍然是修改NSTextStorage中的字符, 还需要计算滚动量(表示了左边隐藏的宽度):


image.png

3.文本超过输入框大小, 最后一个字符不可见, 即左边有隐藏文字, 右边也有隐藏文字, 此时需要判断: 在该位置insert 了新的文本之后, 新的光标是否在输入框范围之内. 如果还在输入框范围之内, 那么就是修改NSTextStorage中的字符即可.


image.png

否则, 还需要计算滚动量:


image.png

综上: 其实可以合并成两种情况:
1.插入点追加了文字之后, 小于label的MaxX, 则直接在后面追加文字, 更新光标位置即可
2.插入点追加了文字之后, 大于Label的MaxX, 则右边界固定, 光标移动到最右端, 文字向左滚动

Delete

[_textStorage deleteCharactersInRange:range];

1.文本不超过输入框的情况下的删除, 直接修改 NSTextStorage 中的字符即可:

image.png

2.文本超过输入框 delete, 最后一个字符可见, 即左边有隐藏文字, 右边无隐藏文字. 这种情况需要判断删除后的文本有没有超过输入框, 如果仍然超过了, 删除当前字符, 保持最后一个字符位置不变(文字右边固定), 计算新的滚动量, 表现为文字向右滚动. 如果没有超过, 删除当前字符, 将文字全部显示出来, 表现为向右滚动:


image.png

3.文本超过输入框 delete, 最后一个字符不可见, 即左边有隐藏文字, 右边也有隐藏文字. 这种情况需要判断删除了当前文字之后, 光标位置有没有超出文本框的范围(到了左边界以左), 如果仍在输入框范围, 则只需要修改 NSTextStorage 的字符即可:


image.png

否则, 需要计算滚动量, 使得滚动后的光标正好在输入框的起始位置(数值为0)


image.png

综上: 根据最后一个字符是否可见, 分为左端固定与右端固定的情况

Touch

touch 文本后, 需要计算出 touch 的字符, 以及该字符的 frame, 然后将光标更新到这个位置, 根据touch坐标获取被 touch 的字符 index:

NSInterge index=  [_layoutManager glyphIndexForPoint:point inTextContainer:_textContainer];

根据字符获取字符的 frame:

CGRect bounding =[_layoutManager boundingRectForGlyphRange:range inTextContainer:_textContainer];

粘贴菜单

粘贴操作就是读取系统粘贴板的字符串, insert 操作.


点击前

点击后

手势

主要参考了UITextField 的行为:

  1. 当光标在某一位置时, 再次点击这个位置, 弹出菜单
  2. 长按光标, 可左右移动光标, 停止移动时, 弹出菜单

自定义属性

  • set text: 先clear文本, 再insert 新的文本
  • set attributeText: 调用[_textStorage setAttributedString:attributedText]方法
    注意: 这里不能clear 之后 insert, 因为 insert 的参数是 NSString
  • set font: 设置 font 会影响显示区域---textContainer, 文本属性---textStorage的属性, 还要考虑以下情况:
    1 文本已经超过输入框, 这时改变了 font大小, 那么左边隐藏的宽度需要重新计算
    2 placeholder 情况下, 改变了 font 大小, 导致 placeholder 超过了输入框宽度, 这个时候要使用自适应的大小
  • set placeholder: 需要注意已经是 placeholder 的状态下, 要更换新的 placeholder, 然后刷新显示
  • set textColor: 会影响文本属性---textStorage的属性
  • set numberOfLines: 此版本目前只支持单行
  • set leftView: 设置后, 要刷新输入框的布局
  • set leftViewInsets: 下边距和右边距是固定的
  • set clearButtonMode: 设置后, 要刷新输入框的布局
  • set placeholderColor: 需要注意当前有可能是 placeholder 状态, 那么会影响文本属性---textStorage的属性
    总结: 每个属性的设置, 都要考虑是否会影响(刷新)当前显示, 显示包括文本属性(颜色, 大小, 绘制起点等)和布局

遇到的问题

  • 关于 attributeText, 输入框是基于 UILabel 的, 一旦设置了 text, attributeText 也会被改变, 同样的, 一旦设置了 attributeText, text 也被改变了.
  • 当 textstorage 的文本为空的时候, 执行 setAttributeString 和 insertAttributeString后, text为空(不会被设置), 但是 textstorage 不为空的时候, 执行这些操作后, text会被自动设置
  • NSLayoutManager 的方法- (CGRect)boundingRectForGlyphRange:(NSRange)glyphRange inTextContainer:(NSTextContainer *)container 的行为很奇怪, 这个方法是获取某一段文字的矩形边框的 frame, 但是返回值的 width, height 的值会不准确, 尤其是包含 emoji 的时候, 使用过程中发现, minX 是准确的.
    注意: NSString 的sizeWithAttributes方法计算的数值和这个方法是一致的, 即使有 emoji 也是准确的, 可以考虑用这个方法替代
  • NSLayoutManager 的方法- (NSUInteger)glyphIndexForPoint:(CGPoint)point inTextContainer:(NSTextContainer *)container是根据 point 获取字符的 index, 如果文本中含有花漾字, 会计算错误.
  • 当文本中含有中文字符的时候, 显示区域会扩大一点点, 但是使用察觉不到, Stack Overflow 上看到有人提过, 疑似系统 bug.
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,377评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,390评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,967评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,344评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,441评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,492评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,497评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,274评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,732评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,008评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,184评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,837评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,520评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,156评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,407评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,056评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,074评论 2 352

推荐阅读更多精彩内容