UILabel实现UITextFiled效果

实现方案

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


UILabel实现UITextFiled效果_第1张图片

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


UILabel实现UITextFiled效果_第2张图片
Textkit

Text Kit

UILabel实现UITextFiled效果_第3张图片
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足以完成.


UILabel实现UITextFiled效果_第4张图片
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.

你可能感兴趣的:(UILabel实现UITextFiled效果)