原創博文,未經作者允許,不允許轉載
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不是創建出來的 而是直接從緩存中提取 的 這也是為什么 不會出現加載延遲的原因
原創博文,未經作者允許,不允許轉載