<原>DTCoreText学习(三)-自定义DTAttributedTextCell

原创博文,未经作者允许,不允许转载

DTCoreText自带的DTAttributedTextCell在显示html的时候  会占用整个cell的大小,当我们需要的形式比较灵活的时候,或者想在cell上自定义添加更多的东西的时候  DTAttributedTextCell 就会变的不够用 需要我门根据DTAttributedTextCell的原理,自己写一个cell  

例如 我们希望cell左边是一个图片,然后右边剩下的区域是一个DTAttributedTextContentView用来显示html 这个图片在点击cell的时候会改变

步骤


1.首先 仍然是将DTCoreText添加到我们自己的工程文件中

2.创建UITableViewCell的子类 MyCell.h MyCell.m

MyCell.h

 1 @interface MyCell : UITableViewCell
 2 {
 3    
 4      IBOutlet UIImageView *imageView;
 5      IBOutlet DTAttributedTextContentView *_attributedTextContextView;
 6 }
 7 
 8 @property(nonatomic,retain)UIImageView *imageView;
 9 
10 @property (nonatomic, strong) NSAttributedString *attributedString;
11 @property (nonatomic, readonly) DTAttributedTextContentView *attributedTextContextView;
12 - (id)initWithReuseIdentifier:(NSString *)reuseIdentifier accessoryType:(UITableViewCellAccessoryType)accessoryType;
13 
14 - (void)setHTMLString:(NSString *)html;
15 
16 - (CGFloat)requiredRowHeightInTableView:(UITableView *)tableView;
17 
18 @end

storyboard中 左边是图片  右边是UIView  将其class设为DTAttributedTextContentView  

将cell的class设置为MyCell

并且 都与MyCell进行连接

这样当我们调用mycell的 setHTMLString:(NSString*)html方法时候  实际上是在设置右边的attributedTextContextView.attributedString  为我们解析过后的string  然后显示出来

MyCell.m


直接将DTAttributedTextCell.m中的所有代码代码复制过来即可 但是要更改几处地方

 1 #import "MyCell.h"
 2 #import "DTCoreText.h"
 3 #import "DTAttributedTextCell.h"
 4 @implementation MyCell
 5 {
 6     
 7     NSAttributedString *_attributedString;
 8     //DTAttributedTextContentView *_attributedTextContextView;   //改动1
 9     
10     NSUInteger _htmlHash; // preserved hash to avoid relayouting for same HTML
11 }
12 @synthesize attributedString = _attributedString;
13 @synthesize attributedTextContextView = _attributedTextContextView;
14 
15 @synthesize imageView;   //添加这一句  改动2

在.m文件中 {}中的属性是私有属性  由于我们在MyCell.h中声明了 DTAttributedTextContentView *_attributedTextContextView;   所以这里不用再次声明

 

 1 - (id)initWithReuseIdentifier:(NSString *)reuseIdentifier accessoryType:(UITableViewCellAccessoryType)accessoryType
 2 {
 3     self = [super initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reuseIdentifier];
 4     if (self) 
 5     {
 6         // don't know size jetzt because there's no string in it
 7         
 8         //_attributedTextContextView = [[DTAttributedTextContentView alloc] init];
 9         //_attributedTextContextView.frame=CGRectMake(100, 100, 40, 50);
10         
11         _attributedTextContextView.edgeInsets = UIEdgeInsetsMake(5, 5, 5, 5);
12         
13     //    [self.contentView addSubview:_attributedTextContextView];
14        
15         
16         
17         
18     }
19     return self;
20 }

这里面注释掉的代码是 原来的cell  由于DTAttributedTextCell 没有用storyboard或者 xib 所以 它上面的

DTAttributedTextContentView 是用代码  在初始化的时候 addSubView上去的  我们的MyCell 是用storyboard显式创建的

所以这里面不用这些初始化代码  

这时需要更改的几个地方 其他的直接复制过来即可

 

3.在tableView中引用MyCell

这里只贴出关键部分代码

 1 - (void)configureCell:(MyCell *)cell forIndexPath:(NSIndexPath *)indexPath
 2 {
 3     
 4     NSString *html=[array objectAtIndex:indexPath.row];
 5     [cell setHTMLString:html];
 6     cell.imageView.image=[UIImage imageNamed:@"XX"];
 7     
 8     cell.attributedTextContextView.shouldDrawImages = YES;
 9 }
10 
11 - (MyCell *)tableView:(UITableView *)tableView preparedCellForIndexPath:(NSIndexPath *)indexPath
12 {
13         //DTCoreTest Demo 中的源代码  这里将 其注释掉 
14     //static NSString *cellIdentifier = @"name";
15     
16     if (!cellCache)
17     {
18         cellCache = [[NSCache alloc] init];
19     }
20     
21     // workaround for iOS 5 bug
22     NSString *key = [NSString stringWithFormat:@"%d-%d", indexPath.section, indexPath.row];
23     
24     MyCell *cell = [cellCache objectForKey:key];
25     
26     if (!cell)
27     { 
28         // reuse does not work for variable height
29         //cell = (DTAttributedTextCell *)[tableView dequeueReusableCellWithIdentifier:cellIdentifier];
30         
31         if (!cell)
32         {
33                         //DTCoreTest Demo 中的源代码  这里将 其注释掉 
34             //cell = [[MyCell alloc] initWithReuseIdentifier:cellIdentifier accessoryType:UITableViewCellAccessoryDisclosureIndicator];
35             //这一句是引用我们自定义的cell的重点代码
36             cell=[tableView dequeueReusableCellWithIdentifier:@"name"];
37            
38             
39         }
40         
41         // cache it
42         [cellCache setObject:cell forKey:key];
43     }
44     
45     [self configureCell:cell forIndexPath:indexPath];
46     
47     return cell;
48 }
49 
50 // disable this method to get static height = better performance
51 - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
52 {
53     MyCell *cell = (MyCell *)[self tableView:tableView preparedCellForIndexPath:indexPath];
54     //下面一句是非常重要的一句代码
55     return  cell.attributedTextContextView.frame.size.height+10;
56    //DTCoreTest Demo 中的源代码  这里将 其注释掉 改为上面一句
57     //return [cell requiredRowHeightInTableView:tableView];
58 }
59 
60 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 
61 {
62     MyCell *cell = (MyCell *)[self tableView:tableView preparedCellForIndexPath:indexPath];
63     
64     return cell;
65 }

填充数据方面的代码比较简单  这里就不贴出来了

这时后点击运行  显示如下图所示

很明显出现了错误,  DTAttributedTextContentView 仍然占据整个cell   而imageView  就是那个A  显示的位置正确 但却因为

DTAttributedTextContentView 占据整个cell  而导致其覆盖在上面  并没有按照我们设计布局的  左边显示 A  右边显示html

但是有一点是确定的     自适应高度没有问题


我们先来分析一下tableView中 那些关键代码的  运行流程

首先调用

1 - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
2 {
3     MyCell *cell = (MyCell *)[self tableView:tableView preparedCellForIndexPath:indexPath];
4      
5     return  cell.attributedTextContextView.frame.size.height+10;
6    
7     //return [cell requiredRowHeightInTableView:tableView];
8 }

来确定每一个cell的高度  

在这里面继续调用

 1 - (MyCell *)tableView:(UITableView *)tableView preparedCellForIndexPath:(NSIndexPath *)indexPath
 2 {
 3     //static NSString *cellIdentifier = @"name";
 4     
 5     if (!cellCache)
 6     {
 7         cellCache = [[NSCache alloc] init];
 8     }
 9     
10     // workaround for iOS 5 bug
11     NSString *key = [NSString stringWithFormat:@"%d-%d", indexPath.section, indexPath.row];
12     
13     MyCell *cell = [cellCache objectForKey:key];
14     
15     if (!cell)
16     {
17         // reuse does not work for variable height
18         //cell = (DTAttributedTextCell *)[tableView dequeueReusableCellWithIdentifier:cellIdentifier];
19         
20         if (!cell)
21         {
22             //cell = [[MyCell alloc] initWithReuseIdentifier:cellIdentifier accessoryType:UITableViewCellAccessoryDisclosureIndicator];
23             cell=[tableView dequeueReusableCellWithIdentifier:@"name"];
24            
25             
26         }
27         
28         // cache it
29         [cellCache setObject:cell forKey:key];
30     }
31     
32     [self configureCell:cell forIndexPath:indexPath];
33     
34     return cell;
35 }

这个方法中 有两句核心代码  其他的都是一些缓存相关 看一下代码很好理解

cell=[tableView dequeueReusableCellWithIdentifier:@"name"];

[self configureCell:cell forIndexPath:indexPath];

第一句  是在缓存中没有可取的 cell的时候 从tableView的可重用队列中取出一个cell实例(可参考《自定义UITableViewCell的理解一文》)

第二句 是配置cell  接下来看看  这个方法的代码

1 - (void)configureCell:(MyCell *)cell forIndexPath:(NSIndexPath *)indexPath
2 {
3     
4     NSString *html=[array objectAtIndex:indexPath.row];
5     [cell setHTMLString:html];
6     cell.imageView.image=[UIImage imageNamed:@"xxx"];7     
8     cell.attributedTextContextView.shouldDrawImages = YES;
9 }

这里面是初始化cell上的图片 以及DTAttributedTextContentView

核心代码是[cell setHTMLString:html];  前面有介绍 

通过单步跟踪调试  跟踪以后的调用流程

这里先进入setHTMLString

 1 - (void)setHTMLString:(NSString *)html
 2 {
 3     // we don't preserve the html but compare it's hash
 4     NSUInteger newHash = [html hash];
 5     
 6     if (newHash == _htmlHash)
 7     {
 8         return;
 9     }
10     
11     _htmlHash = newHash;
12     
13     NSData *data = [html dataUsingEncoding:NSUTF8StringEncoding];
14     NSAttributedString *string = [[NSAttributedString alloc] initWithHTML:data documentAttributes:NULL];
15     self.attributedString = string;
16 }

这一段的核心代码是最后一句   前面大部分都是对html的处理 包括解析等等 如果单步跟踪进入

会发现  

NSAttributedString *string = [[NSAttributedString alloc] initWithHTML:data documentAttributes:NULL];调用的很

深 很深 里面包括对html的各种处理  解析 等等  

在跟踪的时候发现了设置 显示的html的字体大小的方法  

在DTHTMLAttributedStringBuilder.m中 的 buildString方法中 找到textScale  变量  更改其值变能更改显示在cell上的字体的大小 

接着调用最后一句 self.attributedString = string  由于MyCell.m重写了setAttributedString 方法所以调用之

 1 - (void)setAttributedString:(NSAttributedString *)attributedString
 2 {
 3     if (_attributedString != attributedString)
 4     {
 5         _attributedString = attributedString;
 6         
 7         // passthrough
 8         _attributedTextContextView.attributedString = _attributedString;
 9     }
10 }

很显然 这里面的最后一句是最重要的代码  只要设置DTAttributedTextContentView 的attributedString 为解析好的要显示的

html   便能直接显示出来

这里继续单步跟踪进入  会进入DTAttributedTextContentView 的一系列方法中  而这些方法是揭开 为什么没有按照我门设计的布局显示的线索

DTAttributedTextContentView.m

 1 - (void)setAttributedString:(NSAttributedString *)string
 2 {
 3     if (_attributedString != string)
 4     {
 5         
 6         _attributedString = [string copy];
 7         
 8         // new layout invalidates all positions for custom views
 9         [self removeAllCustomViews];
10         
11         [self relayoutText];
12     }
13 }

我门关心的是为什么我们设计的DTAttributedTextContentView  是在左边的区域  但是 运行的时候却充满正的cell  这肯定与

最后一句代码有关   relayoutText


继续跟踪  进入了最重要的方法  也是解决问题的关键方法  relayoutText

 

 1 - (void)relayoutText
 2 {
 3     // Make sure we actually have a superview before attempting to relayout the text.
 4     if (self.superview) {
 5         // need new layouter
 6         self.layouter = nil;
 7         self.layoutFrame = nil;
 8         
 9         // remove all links because they might have merged or split
10         [self removeAllCustomViewsForLinks];
11         
12         if (_attributedString)
13         {
14             // triggers new layout
15             
16             CGSize neededSize = [self sizeThatFits:self.bounds.size];
17            
18             // set frame to fit text preserving origin
19             // call super to avoid endless loop
20             [self willChangeValueForKey:@"frame"];
21             super.frame = CGRectMake(self.frame.origin.x, self.frame.origin.y, neededSize.width, neededSize.height);
22             
23             
24             [self didChangeValueForKey:@"frame"];
25         }
26         
27         [self setNeedsDisplay];
28         [self setNeedsLayout];
29     }
30 }

分析这段代码 我们看出 

CGSize neededSize = [self sizeThatFits:self.bounds.size];

super.frame = CGRectMake(self.frame.origin.x, self.frame.origin.y, neededSize.width, neededSize.height);

这两句是关键代码    设置尺寸的话应该是在这两句完成

首先第句话  得到了  neededSize(需求的尺寸  即显示自己的完整html需要的尺寸) 

第二句 利用得到的neededSize 设置 super.frame  出问题 应该是在  第一句得到的 neededSize 有问题

经过NSlog 打印 neededSize 的值后  确实  是得到的neededSize.width为整个cell的 宽度 进而 设置super.frame 为整个cell的

宽度   

跟踪第一句代码  进入sizeThatFits方法

 1 - (CGSize)sizeThatFits:(CGSize)size
 2 {
 3     if (size.width==0)
 4     {
 5         size.width = self.bounds.size.width;
 6     }
 7     
 8     CGSize neededSize = CGSizeMake(size.width, CGRectGetMaxY(self.layoutFrame.frame) + edgeInsets.bottom);
 9     
10     
11     return neededSize;
12 }

我们注意到 sizeThatFits 这个方法 需要传入一个参数(CGSize)   这里面  利用这个参数size.width 作为 

DTAttributedTextContentView 的宽度  也就是用来显示html的DTAttributedTextContentView的宽度为size.width

然后根据这个宽度 以及html  计算出DTAttributedTextContentView的高度    计算高度的所有代码全部在

CGRectGetMaxY(self.layoutFrame.frame) + edgeInsets.bottom  中   

我们只需要知道   如果我们传入的size 的宽度越宽 计算出来的 高度会越小,反之亦然,最后得到的neededSize 包含了我们规定的宽度  以及计算的高度

在这里NSLog size.width 发现 传入的宽度是 整个cell的宽度

回到上一层方法relayoutText 

CGSize neededSize = [self sizeThatFits:self.bounds.size];   传入的值是 self.bounds.size

NSlog后确实 self.bounds.size.width 为整个cell的宽度

所以得到的 neededSize  宽度为整个cell的宽度  

最后设置super.frame  的时候 便充满了整个cell


解决办法:CGSize neededSize = [self sizeThatFits:self.bounds.size];这一句代码传入正确的size  可以去storyboard中看一下 自己设置的 DTAttributedTextContentView 的宽度  以及高度  然后创建一个size 传入 方法中

最后要注意的一点

super.frame = CGRectMake(self.frame.origin.x, self.frame.origin.y, neededSize.width, neededSize.height);

改为

self.frame = CGRectMake(self.frame.origin.x, self.frame.origin.y, neededSize.width, neededSize.height);

 

方法中的self.frame.origin.x =0;  所以 要想实现  我们设计的效果  这里不能为0   为0的话会紧挨着右边   比如cell宽度

为1024    DTAttributedTextContentView 的宽度为 824   那么 用200  替代self.frame.origin.x即可

修改后的代码

 1 - (void)relayoutText
 2 {
 3     // Make sure we actually have a superview before attempting to relayout the text.
 4     if (self.superview) {
 5         // need new layouter
 6         self.layouter = nil;
 7         self.layoutFrame = nil;
 8         //NSLog(@"%f",self.frame.size.width);
 9         // remove all links because they might have merged or split
10         [self removeAllCustomViewsForLinks];
11         
12         if (_attributedString)
13         {
14             // triggers new layout
15             CGSize size=CGSizeMake(612, 44);
16             
17             CGSize neededSize = [self sizeThatFits:size];
18            
19             // set frame to fit text preserving origin
20             // call super to avoid endless loop
21             [self willChangeValueForKey:@"frame"];
22           
23             self.frame=CGRectMake(156, self.frame.origin.y, neededSize.width, neededSize.height);
24             
25             [self didChangeValueForKey:@"frame"];
26         }
27         
28         [self setNeedsDisplay];
29         [self setNeedsLayout];
30     }
31 }

 

这样便实现了我们的需求

 

 

截至到目前分析的这么多方法  只是从

1 - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
2 {
3     MyCell *cell = (MyCell *)[self tableView:tableView preparedCellForIndexPath:indexPath];
4      
5     return  cell.attributedTextContextView.frame.size.height+10;
6    
7     //return [cell requiredRowHeightInTableView:tableView];
8 }

 

中第一句代码一路调用 深入分析的   

当第一句代码最后深入调用结束  回到这里的时候 很显然  我们已经计算出了neededSize 并且将

DTAttributedTextContentView 的frame的大小也设置为合适的大小  然后  下一句

return cell.attributedTextContextView.frame.size.height+10;

通过直接读取 便可获得合适的高度给cell  

到目前为止  甚至没有执行

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 

方法 但是 已经把所有的html显示了一遍 

当tableView 运行到

1 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 
2 {
3     MyCell *cell = (MyCell *)[self tableView:tableView preparedCellForIndexPath:indexPath];
4     
5     return cell;
6 }

方法时   与heightForRowAtIndexPath:  方法是记本相同的   只不过 这次在

MyCell *cell = (MyCell *)[self tableView:tableView preparedCellForIndexPath:indexPath];

方法中 cell不是创建出来的  而是直接从缓存中提取 的  这也是为什么  不会出现加载延迟的原因

 

原创博文,未经作者允许,不允许转载

posted @ 2012-09-02 22:27  不曾拥有  阅读(4118)  评论(3编辑  收藏  举报