<原>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不是创建出来的 而是直接从缓存中提取 的 这也是为什么 不会出现加载延迟的原因
原创博文,未经作者允许,不允许转载