iOS 的 UITextView 显示含有网络图片的 NSAttributedString 时,怎么样接管网络图片的下载过程?

2014-11-26 18:59:00 +08:00
 q84629462
NSString *html = @"<img src=\"http://cdn.v2ex.com/site/logo@2x.png\"/>";
NSDictionary *options = @{NSDocumentTypeDocumentAttribute : NSHTMLTextDocumentType};
NSAttributedString *string = [[NSAttributedString alloc] initWithData:[html dataUsingEncoding:NSUnicodeStringEncoding] options:options documentAttributes:nil error:&error];
//textView是UITextView
[text setAttributedText:string];
想在上面这一个过程中,实现:
把html中的img替换成小的缩略图显示,并使用TMCache缓存缩略图,在UITextView中点击缩略图后,显示原图。
搜索了两三天,依然找不到图片的下载是在NSAttributedString/UITextView这两个玩意的哪个方法里,倒是实现了点击后识别是否图片。
32132 次点击
所在节点    iDev
80 条回复
q84629462
2014-11-28 23:12:36 +08:00
@jox @ld0891
<a href="javascript:;" dataitem="name_付之一笑" >@付之一笑</a>
直接被当成是textnode了。。。
我去翻翻hpple的源码看看怎么改
jox
2014-11-28 23:17:51 +08:00
@q84629462 hpple就是把所有的html节点都解码成一个个的objective c对象,原来是什么样的,转化完就是什么样的,hpple本身不会识别任何类型的html tag,它做的工作只是解码,或者叫parse,真正做翻译或者识别的是开发者自己的工作。

就好像解析json一样,你得到一个json字符串,丢给json的parser让它解码,具体这个json有什么东西parser是不关心的,它只关心你丢给它的输入是否是合法的json字符串,如果不合法就会报错,合法就返回给你一个json的数据结构。hpple也是一样的道理,你得到一个html字符串,你丢给hpple,hpple返回html的dom给你,你得自己遍历这个dom才能得到你想要的数据。你想让hpple来识别就好像在要求json的parser识别某个特殊的json key一样,一般情况下是不可能的。

hpple算是用起来很简单的了,你看看tfhppleelement.h里面都有什么方法,一般就用children得到子节点列表,objectforkey得到某个特殊的html属性,tagname是tag的名字。
jox
2014-11-28 23:19:54 +08:00
@q84629462 不是吧,应该是这样的结构:

{
tagName:a,
{
href:javascript,
dataitem:name_付之一笑
},
children:{
tagName:text,
content:@付之一笑
}
}

你再好好看看
jox
2014-11-28 23:26:47 +08:00
hpple返回的数据结构里,好像只有text和br是没有子节点的,img有没有子节点忘记了,你可以看看,就这三个比较特殊,其他的全部都有子节点,

我拿你的例子试了一下,这是代码:

--

>>>> html = @"<a href=\"javascript:;\" dataitem=\"name_付之一笑\" >@付之一笑</a>";
>>>> TFHpple *parser = [TFHpple hppleWithHTMLData:[html dataUsingEncoding:NSUTF8StringEncoding]];
>>>> TFHppleElement *body = [[parser searchWithXPathQuery:@"//body"] firstObject];

--

这是截图:
jox
2014-11-28 23:37:19 +08:00
@ld0891 我的意思是regex不能匹配递归定义的语法,而html的语法就是递归定义的。当你需要将使用html标记的数据转化为一般的字符串或者像浏览器那样显示的话,regex没办法解决,必须使用专门的parser,举个例子,你要写一个论坛的客户端,该论坛的API返回的数据是html,有的时候会遇到这种结构:

<div>hello <div>@ld0891</div></div>

有的时候是这种:

<div>hello <div>@ld0891</div> how are you doing <div><b><i>today?</i></b></div></div>

甚至是更复杂的结构,你不可能使用regex来匹配这样的字符串,但是如果这个论坛返回的html只有两种结构,一种是只包含text的:

hello @ld0891

一种是带img标签的:

hi @ld0891 have an upvote: <img src="upvote.gif">

这样的情况你用regex就无所谓了,但是这种情况并不多见
q84629462
2014-12-05 01:26:30 +08:00
首先感谢@jox,已经按照@jox的方法自行用TFHpple组建成NSAttributedString,包含的网络图片标签(<img src="http://巴拉巴拉巴拉">)也用AFNetWorking自行获取图片并用TMDIsk缓存起来了
但是由于是要放去UITableViewCell里用的,所以又有一个新问题:高度计算不正确
简单举例:
UITextView *textView;
[textView setDelegate:self];
NSTextAttachment *attachment = 长宽都是60的NSTextAttachment图片;
NSAttributedString *string = [NSAttributedString attributedStringWithAttachment:attachment];
NSLog(@"string.size:%@", NSStringFromCGSize(string.size));
//string.size:{60, 60}
CGSize size = CGSizeMake(textView.contentSize.width, CGFLOAT_MAX);
CGRect rect = [string boundingRectWithSize:size options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading | NSStringDrawingUsesDeviceMetrics) context:nil];
NSLog(@"string rect:%@", NSStringFromCGRect(rect));
//string rect:{{0, 0}, {60, 60}}
[textView setAttributedText:string];
然后就是把当前的UITableViewCell的高度设为60了,结果是图片显示不全,症状是UITableViewCell的高度不足以把图片显示全
就不说多行的NSAttributedString文字,一样的症状。
@PrideChung @gonghao @jox @ld0891 各种求。
jox
2014-12-05 05:26:52 +08:00
用tableview的话你需要提前计算每个cell的高度并缓存计算结果,方法很简单,数据到位之后触发的callback里用一组textkit对象进行排版,然后使用layoutmanager获取排版之后的文字frame,高度就有了,因为排版是lazy方式触发的,你需要强制让排版触发,使用layoutmanager的ensure。。开头的几个方法之一,就可以强制要求layoutmanager排版,记住textkit只是个排版引擎,所以这里渲染不会发生,要搞清楚这点,这里只有排版的开销,真正需要渲染的时候依然要进行排版,但是因为你已经计算好了渲染文字需要的尺寸,到时候直接修改textview的frame就行。

睡觉热醒了,手机不方便贴代码,你要不明白回头我拿电脑的时候可以贴示例
jox
2014-12-05 05:32:15 +08:00
另外你不需要自己做缓存,如果你使用nsurlsession的话会自动缓存图片数据,用image io的话会自动缓存解码过的图片数据到内存,你可以用nscache缓存解码过的图片对象,不必担心内存,nscache在内存不够的时候会自动释放内存,这样你只不过需要重新解码,不会重新下载图片,因为nsurlsession会缓存图片到硬盘上,只要应用不关,系统是不会删除你的缓存数据的
q84629462
2014-12-05 16:42:27 +08:00
@jox 求代码
jox
2014-12-05 16:59:41 +08:00
static NSTextStorage *textStorage;
static NSTextContainer *textContainer;
static NSLayoutManager *layoutManager;;
static CGFloat contentWidth;
static dispatch_once_t oncePredicate;

UIViewController * __weak weakSelf = self;
dispatch_once(&oncePredicate, ^{
contentWidth = CGRectGetWidth(weakSelf.tableView.frame);
textStorage = [[NSTextStorage alloc] init];
layoutManager = [[NSLayoutManager alloc] init];
[textStorage addLayoutManager:layoutManager];
textContainer = [[NSTextContainer alloc] initWithSize:CGSizeMake(contentWidth, FLT_MAX)];
textContainer.lineFragmentPadding = 0;
[layoutManager addTextContainer:textContainer];
contentOrigin = CGPointZero;
});

[textStorage setAttributedString:@"the content"];
[layoutManager ensureLayoutForTextContainer:textContainer];
CGRect frame = [layoutManager usedRectForTextContainer:textContainer];
frame.size.width = contentWidth;

// frame should be the frame of the target text view, and you can calculate the height of the cell based on this
jox
2014-12-05 17:03:14 +08:00
上面

[textStorage setAttributedString:]

这个应该是你得到的NSAttributedString,不是NSString,这是基于我写的程序里的代码改的,我改的时候忘记了。
q84629462
2014-12-05 22:19:31 +08:00
@jox
找到三种计算富文本字符串高度的方法,三种方法计算出来的高度是一样的
但与shouldInteractWithTextAttachment(UITextViewDelegate点击富文本里的图片后的回调)输出的UITextView内容高度始终不同,本文最后附上了NSLog记录
ps:如果富文本字符串只包含图片,例如图片是高50的,那UITextView.contentSize.height就是66,图片是高60的,那UITextView.contentSize.height就是76,必然相差了16,求解
再ps:下文三个width参数我明明赋值了320,但NSLog出来只有318.62,求解
- (void)Calculating_Text_Height_1_Width:(CGFloat)width WithString:(NSAttributedString *)string {
NSTextStorage *textStorage = [[NSTextStorage alloc] init];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
[textStorage addLayoutManager:layoutManager];
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:CGSizeMake(width, FLT_MAX)];
[textContainer setLineFragmentPadding:0.0];
[layoutManager addTextContainer:textContainer];
[textStorage setAttributedString:string];
[layoutManager ensureLayoutForTextContainer:textContainer];
CGRect frame = [layoutManager usedRectForTextContainer:textContainer];
NSLog(@"1:%@", NSStringFromCGRect(frame));
/*
http://www.v2ex.com/t/149498
*/
}

- (void)Calculating_Text_Height_2_Width:(CGFloat)width WithString:(NSAttributedString *)string {
CGRect frame = [string boundingRectWithSize:CGSizeMake(width, FLT_MAX) options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading) context:nil];
NSLog(@"2:%@", NSStringFromCGRect(frame));
/*
http://blog.csdn.net/iunion/article/details/12185077
*/
}

- (void)Calculating_Text_Height_3_Width:(CGFloat)width WithString:(NSAttributedString *)string {
NSTextStorage *textStorage = [[NSTextStorage alloc] init];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
[textStorage addLayoutManager:layoutManager];
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:CGSizeMake(width, FLT_MAX)];
[textContainer setLineFragmentPadding:0.0];
[layoutManager addTextContainer:textContainer];
[textStorage setAttributedString:string];
[layoutManager glyphRangeForTextContainer:textContainer];
CGRect frame = [layoutManager usedRectForTextContainer:textContainer];
NSLog(@"3:%@", NSStringFromCGRect(frame));
/*
https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/TextLayout/Tasks/StringHeight.html
http://www.cocoachina.com/b/?p=160
*/
}

- (BOOL)textView:(UITextView *)textView shouldInteractWithTextAttachment:(NSTextAttachment *)textAttachment inRange:(NSRange)characterRange {
NSLog(@"img:%@", NSStringFromCGRect(textAttachment.bounds));
NSLog(@"total:%@", NSStringFromCGSize(textView.contentSize));
return true;
}

========
2014-12-05 22:04:05.640 test[3673:112783] View Controller Bounds:{{0, 0}, {320, 568}}
2014-12-05 22:04:05.641 test[3673:112783] UITextView Controller Bounds:{{0, 0}, {600, 600}}
2014-12-05 22:04:05.671 test[3673:112783] 1:{{0, 0}, {318.62, 637.36000000000001}}
2014-12-05 22:04:05.672 test[3673:112783] 2:{{0, 0}, {318.62, 637.36000000000001}}
2014-12-05 22:04:05.673 test[3673:112783] 3:{{0, 0}, {318.62, 637.36000000000001}}
2014-12-05 22:04:08.612 test[3673:112783] img:{{0, 0}, {50, 50}}
2014-12-05 22:04:08.612 test[3673:112783] total:{320, 664}
===========
jox
2014-12-05 22:46:25 +08:00
。。。。。。

你忘记设置text view的textContainerInset了,这个默认上下各是8points,所以会有16的差距,你要把这个弄成UIEdgeZero,这样算出来的就对了,你算出来后把这个inset考虑进去。至于你说的有一两点point的差距是正常的,不可能每一行都放下同样数量的glyph,这个不用理会,你只需要按照你需要的尺寸设定宽度就行了,layout manger和text container配合排版的时候会考虑到你设定的宽度,最终排版完高度不就有了么?话说你算宽度干啥?你贴了太多东西了,我有点累了,没仔细看
jox
2014-12-05 22:54:53 +08:00
还有你折腾text view的contentSize干啥,这个属性应该是不需要理会的,我用text kit的时候不用text view来渲染文字,为了追求性能,我是渲染完文字之后直接输出位图的,用text view的话不是很简单么,你设定text container的宽度,排版,然后query高度,这样你就得到了text view渲染一定数量文字需要的尺寸,如果后面还要往里面贴图,就用boundingRect。。。的那个方法找到图片的位置把图片贴进去或者画进去,如果是静态图片的画也可以直接赋给attachment的image,这样layout manager会自动帮你渲染图片。
q84629462
2014-12-05 22:59:50 +08:00
@jox 求输出位图的方法。。。反正里面的gif表情我不知道怎么样弄成动态的
jox
2014-12-05 23:08:46 +08:00
gif跟位图无关,你需要读取gif的所有帧数(使用 Image IO),然后使用uiimage的那个生成动画的方法创建个uiimage,然后直接用uiimageview就能渲染gif了,这个是最简单的,也可以使用更复杂的办法,使用flipboard的开源的那段代码,输出位图也很简单,直接使用text kit排版然后使用off-screen render技术渲染,然后得到bitmap图片,然后赋给layer的contents属性,如果你看不懂我在说什么,那么你需要看Core Animation的文档,以及Drawing and Printing的文档,以及Core Graphics的文档,大概看看了解一下iOS的应用是怎样把数据转化成屏幕的像素的,可以使用哪些技术来draw(core graphics,open GL),calayer是怎么回事,Core Animation是怎样工作的,哪些工作由CPU完成,哪些工作由GPU完成,lots of documentations to read, my friend
q84629462
2014-12-06 01:23:18 +08:00
@jox
截图里,左边是iOS模拟器,右边是论坛帖子网页(UITextView的textContainerInset的16已经计上了)

由于计算出来的高度不够,最后一段被吃了显示不出来。
这情况我也不知道该怎么做高度补偿。。。
jox
2014-12-06 16:46:27 +08:00
我用text kit算高度有的时候也会遇到高度稍微差一点导致文字显示不全的时候,但是没有你差得这么多啊,这个具体的原因我没有深入研究,不过我的解决办法是找到个合适的magic number做为padding,一般就是10points到20points之间,把textcontainerInset设成0,然后在layout manager算出来的高度基础上再加上padding,这样的画text view最下面的空白高度会随着数据发生一点点浮动,大概也就5points以内,不过基本不影响整体的效果,我想也不会有人在意这个的吧,你可以试试。
q84629462
2014-12-06 18:27:54 +08:00
@jox 因为都是比较复杂的html内容,Discuz的帖子内容,都懂的。。。
好蛋疼啊,进度就卡在这了。。。
q84629462
2014-12-20 02:37:47 +08:00
@jox 大神我又来问问题了,如何知道一个nssattributedstring里的nstextattachment的x,y坐标?我想用UIimage显示GIf图片
只能先把NSAttributedString显示到UITextView后才能知道NSAttributedString里面的nsTextAttachment的显示坐标吧?

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/149498

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX