自动版式行高为错估NSAttributedString [英] AutoLayout row height miscalculating for NSAttributedString

查看:609
本文介绍了自动版式行高为错估NSAttributedString的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我的应用从一个API拉HTML,将其转换成 NSAttributedString (以允许tappable链接),它在自动版式表中写入一行。麻烦的是,任我调用这个类型的细胞,高度失算和内容被切断。我曾尝试行高度的计算,没有一个正常工作的不同的实现。

我怎样才能准确,动态,计算这些行之一的高度,同时仍保持挖掘HTML链接的能力?

意外的行为的示例

我的code如下。

   - (UITableViewCell的*)的tableView:(UITableView的*)的tableView的cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    开关(indexPath.section){
        ...
        案例kContent:
        {
            FlexibleTextViewTableViewCell *电池=(FlexibleTextViewTableViewCell *)[TableFactory getCellForIdentifier:@内容cellClass:FlexibleTextViewTableViewCell.class舒服:的tableView withStyle:UITableViewCellStyleDefault];            [个体经营configureContentCellForIndexPath:细胞atIndexPath:indexPath];
            [cell.contentView setNeedsLayout]
            [cell.contentView layoutIfNeeded]
            cell.selectionStyle = UITableViewCellSelectionStyleNone;
            cell.desc.font = [UIFont fontWithName:[StringFactory defaultFontType]尺寸:14.0f];            返回细胞;
        }
        ...
        默认:
            回零;
    }
}

   - (CGFloat的)的tableView:(UITableView的*)的tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    UIFont * contentFont = [UIFont fontWithName:[StringFactory defaultFontType]尺寸:14.0f];
    开关(indexPath.section){
        ...
        案例kContent:
            返回[自我textViewHeightForAttributedText:[个体经营convertHTMLtoAttributedString:myHTMLString] andFont:contentFont andWidth:self.tappableCell.width];
            打破;
        ...
        默认:
            返回0.0;
    }
}

   - (NSAttributedString *)convertHTMLtoAttributedString:(* NSString的)HTML {
    返回[NSAttributedString页头] initWithData:[HTML dataUsingEncoding:NSUTF8StringEncoding]
                                            选项​​:@ {NSDocumentTypeDocumentAttribute:NSHTMLTextDocumentType,
                                                      NSCharacterEncodingDocumentAttribute:@(NSUTF8StringEncoding)}
                                 documentAttributes:无
                                              错误:无];
}

   - (CGFloat的)textViewHeightForAttributedText:(NSAttributedString *)文本andFont:(UIFont *)字体andWidth:(CGFloat的)宽度{
    NSMutableAttributedString * mutableText = [[NSMutableAttributedString页头] initWithAttributedString:文]。    [mutableText的addAttribute:NSFontAttributeName值:字体范围:NSMakeRange(0,text.length)];    UITextView的* calculationView = [[UITextView的页头]初始化];
    [calculationView setAttributedText:mutableText];    CGSize大小= [个体经营的文字:mutableText.string sizeWithFont:字体constrainedToSize:CGSizeMake(宽度,FLT_MAX)];
    CGSize sizeThatFits = [calculationView sizeThatFits:CGSizeMake(宽度,FLT_MAX)];    返回sizeThatFits.height;
}


在我工作的应用程序,该应用程序就会从其他人写的一个蹩脚的API低劣的HTML字符串和HTML字符串转换为 NSAttributedString 的对象。我没有选择,只能使用这个蹩脚的API。很伤心。任何人谁拥有解析HTML低劣的字符串,知道我的痛苦。我使用文本工具。方法如下:


  1. 解析HTML字符串来获得DOM对象。我使用的libxml用轻包装, hpple 。这个组合是超快速和易于使用。强烈推荐。

  2. 遍历DOM对象递归构建 NSAttributedString 对象,使用自定义属性来标记链接,请​​使用 NSTextAttachment 标记图片。我把它叫做富文本。

  3. 创建或重用初级文本工具的对象。即 NSLayoutManager NSTextStorage NSTextContainer 。其中挂钩分配后了。

  4. 布局过程

    1. 转至第2步到第3步 NSTextStorage 对象构造的富文本与 [NSTextStorage setAttributedString:]

    2. 使用方法 [NSLayoutManager ensureLayoutForTextContainer:] 来强制布局发生


  5. 计算来绘制方法的富文本所需的帧 [NSLayoutManager usedRectForTextContainer:] 。如果需要添加填充或保证金。

  6. 渲染过程

    1. 返回第5步中计算出的高度[的tableView:heightForRowAtIndexPath:]

    2. 绘制步骤2中的富文本[NSLayoutManager drawGlyphsForGlyphRange:atPoint:] 。我在这里使用离屏绘制技术,所以结果是的UIImage 对象。

    3. 使用的UIImageView 渲染的最终结果图像。或结果传递图像对象的内容的属性图层的属性内容查看的UITableViewCell 对象的属性> [的tableView:的cellForRowAtIndexPath:]


  7. 事件处理

    1. 捕捉触摸事件。我用附表视图水龙头手势识别。

    2. 获取触摸事件的位置。在这个位置可检查用户是否窃听了链接或图片 [NSLayoutManager glyphIndexForPoint:inTextContainer:fractionOfDistanceThroughGlyph] [NSAttributedString属性:atIndex:effectiveRa​​nge: ]


事件处理code片断:

  CGPoint位置= [自来水locationInView:self.tableView];
//自来水是自来水手势识别NSIndexPath * indexPath = [self.tableView indexPathForRowAtPoint:地点];
如果(!indexPath){
    返回;
}CustomDataModel *后= [自我getPostWithIndexPath:indexPath];
// CustomDataModel是NSObject类的子类。* UITableViewCell的细胞= [self.tableView的cellForRowAtIndexPath:indexPath];
位置= [自来水locationInView:cell.contentView];
//丰富的文本被绘制成位图背景和渲染
// cell.contentView.layer.contents//了`文本Kit`对象可以使用模型对象进行访问。
NSUInteger指数= [post.layoutManager
                        glyphIndexForPoint:位置
                           inTextContainer:post.textContainer
            fractionOfDistanceThroughGlyph:NULL];CustomLinkAttribute *链接= [post.content.richText
                                 属性:CustomLinkAttributeName
                                   atIndex:指数
                            effectiveRa​​nge:NULL];
// CustomLinkAttributeName在其他文件中定义的字符串常量
// CustomLinkAttribute是NSObject类的子类。实例
//这个类包含一个链接的信息
如果(联系){
    //链路柄敲击
}//同样的技术可以用于处理图像抽头

这种方法要比更快,更可自定义[NSAttributedString initWithData:选项:documentAttributes:错误:] 渲染相同的HTML字符串时。即使没有分析我可以告诉文本工具办法更快。它的速度非常快,但我必须解析HTML和建造归功于自己串满足均匀。在 NSDocumentTypeDocumentAttribute 办法是太慢因而是不能接受的。随着文本工具,我还可以创建复杂的布局就像变缩进,边框文本块,任何深度嵌套的文本块等,但它确实需要多写code,构建 NSAttributedString 键,来控制布局的过程。我不知道如何计算与 NSDocumentTypeDocumentAttribute 创建一个属性字符串的边界正确。我相信,随着 NSDocumentTypeDocumentAttribute 创建归因字符串由网​​页工具包的处理方式而不是文字工具。因此,并不意味着可变高度表视图细胞。

编辑:
如果必须使用 NSDocumentTypeDocumentAttribute ,我认为你必须要弄清楚的布局过程是如何发生的。也许你可以设置一些断点,看看有什么对象负责布局的过程。那么也许你可以查询对象或使用其他方法来模拟布局过程中得到的布局信息。有些人使用一个特设的细胞或的UITextView 对象来计算高度,我认为是不是一个好办法。由于这种方式,应用程序有布局的文本块一样,至少两次。不管你知道与否,你的应用程序的某个地方,某个对象具有布局的文本,所以您可以像边界矩形布局获取信息。既然你提到 NSAttributedString 类,最好的解决办法就是文本工具的iOS 7或核心后,文字如果您的应用程序是针对较早的IOS版本。

我强烈建议文本工具因为这样,从API拉到每一个HTML字符串,布局过程只发生一次和布局信息,如边界正确,而且每个位置字形是由 NSLayoutManager 对象缓存。只要文本工具对象被保留,你总是可以重新使用它们。使用表格视图来渲染任意长度的文字,因为文字都摆出来只有一次,绘制每次需要单元格中显示的时间时,这是非常有效的。我还建议使用文本工具没有的UITextView 作为苹果官方文档建议。因为必须缓存每个的UITextView 如果他想重用正文套件连接与对象 UITextView的。附加文本工具物体像我这样做,只有更新 NSTextStorage 和力 NSLayoutManager <模型对象/ code>来布局时,一个新的HTML字符串是从API拉动。如果表视图的行数是固定的,人们也可以使用占位符模型对象的固定列表,以避免重复分配和配置。而由于的drawRect:引起核心动画以创建必须避免无用的支持位,不使用的UIView 的drawRect:。要么使用的CALayer 绘画技法或绘制文本为位图上下文。我用的是后一种方法,因为可以在后台线程来完成与 GCD ,因此主线程是免费对用户的操作做出回应。在我的应用程序,结果真的是令人满意的,它的速度快,排版是好的,表视图的滚动非常流畅(60 fps)的,因为所有的绘制过程在后台线程与 GCD 。每一个应用程序需要绘制一些文字与表视图应该使用文本工具

My app pulls HTML from an API, converts it into a NSAttributedString (in order to allow for tappable links) and writes it to a row in an AutoLayout table. Trouble is, any time I invoke this type of cell, the height is miscalculated and the content is cut off. I have tried different implementations of row height calculations, none of which work correctly.

How can I accurately, and dynamically, calculate the height of one of these rows, while still maintaining the ability to tap HTML links?

Example of undesired behavior

My code is below.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    switch(indexPath.section) {
        ...
        case kContent:
        {
            FlexibleTextViewTableViewCell* cell = (FlexibleTextViewTableViewCell*)[TableFactory getCellForIdentifier:@"content" cellClass:FlexibleTextViewTableViewCell.class forTable:tableView withStyle:UITableViewCellStyleDefault];

            [self configureContentCellForIndexPath:cell atIndexPath:indexPath];
            [cell.contentView setNeedsLayout];
            [cell.contentView layoutIfNeeded];
            cell.selectionStyle = UITableViewCellSelectionStyleNone;
            cell.desc.font = [UIFont fontWithName:[StringFactory defaultFontType] size:14.0f];

            return cell;
        }
        ...
        default:
            return nil;
    }
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    UIFont *contentFont = [UIFont fontWithName:[StringFactory defaultFontType] size:14.0f];
    switch(indexPath.section) {
        ...
        case kContent:
            return [self textViewHeightForAttributedText:[self convertHTMLtoAttributedString:myHTMLString] andFont:contentFont andWidth:self.tappableCell.width];
            break;
        ...
        default:
            return 0.0f;
    }
}

-(NSAttributedString*) convertHTMLtoAttributedString: (NSString *) html {
    return [[NSAttributedString alloc] initWithData:[html dataUsingEncoding:NSUTF8StringEncoding]
                                            options:@{NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType,
                                                      NSCharacterEncodingDocumentAttribute: @(NSUTF8StringEncoding)}
                                 documentAttributes:nil
                                              error:nil];
}

- (CGFloat)textViewHeightForAttributedText:(NSAttributedString*)text andFont:(UIFont *)font andWidth:(CGFloat)width {
    NSMutableAttributedString *mutableText = [[NSMutableAttributedString alloc] initWithAttributedString:text];

    [mutableText addAttribute:NSFontAttributeName value:font range:NSMakeRange(0, text.length)];

    UITextView *calculationView = [[UITextView alloc] init];
    [calculationView setAttributedText:mutableText];

    CGSize size = [self text:mutableText.string sizeWithFont:font constrainedToSize:CGSizeMake(width,FLT_MAX)];
    CGSize sizeThatFits = [calculationView sizeThatFits:CGSizeMake(width, FLT_MAX)];

    return sizeThatFits.height;
}

解决方案

In the app I'm working on, the app pulls shitty HTML strings from a crappy API written by other people and converts HTML strings to NSAttributedString objects. I have no choice but to use this crappy API. Very sad. Anyone who has to parse shitty HTML string knows my pain. I use Text Kit. Here is how:

  1. parse html string to get DOM object. I use libxml with a light wrapper, hpple. This combination is super fast and easy to use. Strongly recommended.
  2. traverse the DOM object recursively to construct NSAttributedString object, use custom attribute to mark links, use NSTextAttachment to mark images. I call it rich text.
  3. create or reuse primary Text Kit objects. i.e. NSLayoutManager, NSTextStorage, NSTextContainer. Hook them up after allocation.
  4. layout process

    1. Pass the rich text constructed in step 2 to the NSTextStorage object in step 3. with [NSTextStorage setAttributedString:]
    2. use method [NSLayoutManager ensureLayoutForTextContainer:] to force layout to happen

  5. calculate the frame needed to draw the rich text with method [NSLayoutManager usedRectForTextContainer:]. Add padding or margin if needed.
  6. rendering process

    1. return the height calculated in step 5 in [tableView: heightForRowAtIndexPath:]
    2. draw the rich text in step 2 with [NSLayoutManager drawGlyphsForGlyphRange:atPoint:]. I use off-screen drawing technique here so the result is an UIImage object.
    3. use an UIImageView to render the final result image. Or pass the result image object to the contents property of layer property of contentView property of UITableViewCell object in [tableView:cellForRowAtIndexPath:].

  7. event handling

    1. capture touch event. I use a tap gesture recognizer attached with the table view.
    2. get the location of touch event. Use this location to check if user tapped a link or an image with [NSLayoutManager glyphIndexForPoint:inTextContainer:fractionOfDistanceThroughGlyph] and [NSAttributedString attribute:atIndex:effectiveRange:].

Event handling code snippet:

CGPoint location = [tap locationInView:self.tableView];
// tap is a tap gesture recognizer

NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:location];
if (!indexPath) {
    return;
}

CustomDataModel *post = [self getPostWithIndexPath:indexPath];
// CustomDataModel is a subclass of NSObject class.

UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
location = [tap locationInView:cell.contentView];
// the rich text is drawn into a bitmap context and rendered with 
// cell.contentView.layer.contents

// The `Text Kit` objects can  be accessed with the model object.
NSUInteger index = [post.layoutManager 
                        glyphIndexForPoint:location 
                           inTextContainer:post.textContainer 
            fractionOfDistanceThroughGlyph:NULL];

CustomLinkAttribute *link = [post.content.richText 
                                 attribute:CustomLinkAttributeName 
                                   atIndex:index 
                            effectiveRange:NULL];
// CustomLinkAttributeName is a string constant defined in other file
// CustomLinkAttribute is a subclass of NSObject class. The instance of 
// this class contains information of a link
if (link) {
    // handle tap on link
}

// same technique can be used to handle tap on image

This approach is much faster and more customizable than [NSAttributedString initWithData:options:documentAttributes:error:] when rendering same html string. Even without profiling I can tell the Text Kit approach is faster. It's very fast and satisfying even though I have to parse html and construct attributed string myself. The NSDocumentTypeDocumentAttribute approach is too slow thus is not acceptable. With Text Kit, I can also create complex layout like text block with variable indentation, border, any-depth nested text block, etc. But it does need to write more code to construct NSAttributedString and to control layout process. I don't know how to calculate the bounding rect of an attributed string created with NSDocumentTypeDocumentAttribute. I believe attributed strings created with NSDocumentTypeDocumentAttribute are handled by Web Kit instead of Text Kit. Thus is not meant for variable height table view cells.

EDIT: If you must use NSDocumentTypeDocumentAttribute, I think you have to figure out how the layout process happens. Maybe you can set some breakpoints to see what object is responsible for layout process. Then maybe you can query that object or use another approach to simulate the layout process to get the layout information. Some people use an ad-hoc cell or a UITextView object to calculate height which I think is not a good solution. Because in this way, the app has to layout the same chunk of text at least twice. Whether you know or not, somewhere in your app, some object has to layout the text just so you can get information of layout like bounding rect. Since you mentioned NSAttributedString class, the best solution is Text Kit after iOS 7. Or Core Text if your app is targeted on earlier iOS version.

I strongly recommend Text Kit because in this way, for every html string pulled from API, the layout process only happens once and layout information like bounding rect and positions of every glyph are cached by NSLayoutManager object. As long as the Text Kit objects are kept, you can always reuse them. This is extremely efficient when using table view to render arbitrary length text because text are laid out only once and drawn every time a cell is needed to display. I also recommend use Text Kit without UITextView as the official apple docs suggested. Because one must cache every UITextView if he wants to reuse the Text Kit objects attached with that UITextView. Attach Text Kit objects to model objects like I do and only update NSTextStorage and force NSLayoutManager to layout when a new html string is pulled from API. If the number of rows of table view is fixed, one can also use a fixed list of placeholder model objects to avoid repeat allocation and configuration. And because drawRect: causes Core Animation to create useless backing bitmap which must be avoided, do not use UIView and drawRect:. Either use CALayer drawing technique or draw text into a bitmap context. I use the latter approach because that can be done in a background thread with GCD, thus the main thread is free to respond to user's operation. The result in my app is really satisfying, it's fast, the typesetting is nice, the scrolling of table view is very smooth (60 fps) since all the drawing process are done in background threads with GCD. Every app needs to draw some text with table view should use Text Kit.

这篇关于自动版式行高为错估NSAttributedString的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆