iOS App開發(fā)中使cell高度自適應(yīng)的黑魔法詳解
在使用 table view 的時(shí)侯經(jīng)常會(huì)遇到這樣的需求:table view 的 cell 中的內(nèi)容是動(dòng)態(tài)的,導(dǎo)致在開發(fā)的時(shí)候不知道一個(gè) cell 的高度具體是多少,所以需要提供一個(gè)計(jì)算 cell 高度的算法,在每次加載到這個(gè) cell 的時(shí)候計(jì)算出 cell 真正的高度。
在 iOS 8 之前
沒有使用 Autolayout 的情況下,需要實(shí)現(xiàn) table view delegate 的 tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat 方法,在這個(gè)方法中計(jì)算并返回 cell 的高度。比如,我有一個(gè)可以顯示任意行數(shù)的純文本 cell,計(jì)算 cell 的代碼可以是這樣:
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
let content = self.datas[indexPath.row] as String
let padding: CGFloat = 20
let width = tableView.frame.size.width - padding * 2;
let size = CGSizeMake(width, CGFloat.max)
let attributes = [NSFontAttributeName: UIFont(name: "Helvetica", size: 14)!]
let frame = content.boundingRectWithSize(size,
options: NSStringDrawingOptions.UsesLineFragmentOrigin,
attributes: attributes,
context: nil)
return frame.size.height+1;
}
上面的代碼是一個(gè)最簡(jiǎn)單的例子,這個(gè)例子看起來好像沒有什么問題。但是通過查看這個(gè) delegate 方法的文檔后,可以知道,在每次 reload tableview 的時(shí)候,程序會(huì)先計(jì)算出每一個(gè) cell 的高度,等所有高度計(jì)算完畢,確定了 tableview 的總的高度后,才開始渲染視圖并顯示在屏幕上。這意味著在顯示 table view 之前需要執(zhí)行一堆的計(jì)算,并且這是在主線程中進(jìn)行的,如果計(jì)算量太大程序就很有可能出現(xiàn)卡頓感。比如: table view 的數(shù)據(jù)有上千條,或者計(jì)算高度的代碼中還要先獲取圖片再根據(jù)圖片計(jì)算高度,這些操作都是非常慢的。
如果在 cell 中使用了 autolayout,在計(jì)算 cell 高度時(shí)會(huì)更麻煩。有興趣的可以看這里有篇關(guān)于如何在 autolayout 下動(dòng)態(tài)計(jì)算高度 的文章。
為什么不能等滾動(dòng)到某個(gè) cell 的時(shí)候,再調(diào)用計(jì)算這個(gè) cell 高度的 delegate 呢?原因是 tableview 需要獲得它的內(nèi)容的總高度,用這個(gè)高度去確定滾動(dòng)條的大小等。直到 iOS 7 UITableViewDelegate中添加了新的 API
tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat
這個(gè)方法用于返回一個(gè) cell 的預(yù)估高度,如果在程序中實(shí)現(xiàn)了這個(gè)方法,tableview 首次加載的時(shí)候就不會(huì)調(diào)用heightForRowAtIndexPath 方法,而是用 estimatedHeightForRowAtIndexPath 返回的預(yù)估高度計(jì)算 tableview 的總高度,然后 tableview 就可以顯示出來了,等到 cell 可見的時(shí)候,再去調(diào)用heightForRowAtIndexPath 獲取 cell 的正確高度。
通過使用estimatedHeightForRowAtIndexPath 這個(gè) Delegate 方法,解決了首次加載 table view 出現(xiàn)的性能問題。但還有一個(gè)麻煩的問題,就是在 cell 沒有被加載的時(shí)候計(jì)算 cell 的高度,上面給出的代碼中,僅僅是計(jì)算一個(gè) NSString 的高度,就需要不少代碼了。這種計(jì)算實(shí)際上是必須的,然而在 iOS 8 開始,你可能可以不用再寫這些煩人的計(jì)算代碼了!
iOS 8 的魔法
在 iOS 8 中,self size cell 提供了這樣一種機(jī)制:cell 如果有一個(gè)確定的寬度/高度,autolayout 會(huì)自動(dòng)根據(jù) cell 中的內(nèi)容計(jì)算出對(duì)應(yīng)的高度/寬度。
TableView 中的 cell 自適應(yīng)
要讓 table view 的 cell 自適應(yīng)內(nèi)容,有幾個(gè)要點(diǎn):
設(shè)置的 AutoLayout 約束必須讓 cell 的 contentView 知道如何自動(dòng)延展。關(guān)鍵點(diǎn)是 contentView 的 4 個(gè)邊都要設(shè)置連接到內(nèi)容的約束,并且內(nèi)容是會(huì)動(dòng)態(tài)改變尺寸的。
UITableView 的 rowHeight 的值要設(shè)置為 UITableViewAutomaticDimension
和 iOS 7 一樣,可以實(shí)現(xiàn) estimatedHeightForRowAtIndexPath 方法提升 table view 的第一次加載速度。
任何時(shí)候 cell 的 intrinsicContentSize 改變了(比如 table view 的寬度變了),都必須重新加載 table view 以更新 cell。
例子
在 Xcode 中新建一個(gè)項(xiàng)目,在 storyboard 中創(chuàng)建一個(gè) UITableViewController 的 IB,創(chuàng)建一個(gè)如下樣子的 cell:
這個(gè) cell 中有 3 個(gè)元素,其中 imageView 的 autoLayout 約束為:
- imageView 左邊離 contentView 左邊 0
- imageView 上邊離 contentView 上邊 0
- imageView 的 width 和 height 為 80
- imageView 下邊離 contentView 下邊大于等于 0(為了防止內(nèi)容太少,導(dǎo)致 cell 高度小于圖片高度)
titleLabel 的 autoLayout 約束為:
- titleLabel 左邊離 imageView 右邊 8
- titleLabel 上邊和 imageView 上邊在同一只線上
- titleLabel 右邊離 contentView 右邊 0
- titleLabel 下邊離 description 上邊 8
- titleLabel 的高度小于等于 22,優(yōu)先級(jí)為 250
descriptionLabel 的約束為:
- descriptionLabel 左邊和 titleLabel 左邊在同一直線上
- descriptionLabel 上邊里 titleLabel 8
- descriptionLabel 下邊里 contentView 下邊 0
- descriptionLabel 右邊離 contentView 右邊 0
然后在這個(gè) IB 對(duì)應(yīng)的 UITableViewController 中加載一些數(shù)據(jù)進(jìn)去,顯示效果如圖:
實(shí)現(xiàn)這個(gè)效果,我除了設(shè)置了 autoLayout,還設(shè)置了 tableView 的 rowHeight = UITableViewAutomaticDimension,然后就是這樣了。一點(diǎn)計(jì)算 cell 高度的代碼都沒有?。∥疫B heightForRowAtIndexPath都不用實(shí)現(xiàn),真的是….爽出味??!所以如果已經(jīng)在開發(fā) iOS 8 Only 的應(yīng)用了一定要用autolayout,把煩人的計(jì)算交給 autolayout 去吧。
CollectionView 中的 cell 自適應(yīng)
在 collection view 中也能讓 cell 自適應(yīng)內(nèi)容大小,如果 UICollectionView 的 layout 是一個(gè) UICollectionViewFlowLayout,只需要將 layout.itemSize = ... 改成 layout.estimatedItemSize = ...。 只要設(shè)置了 layout 的 estimatedItemSize,collection view 就會(huì)根據(jù) cell 里面的 autolayout 約束去確定cell 的大小。
原理:
- collection view 根據(jù) layout 的 estimatedItemSize 算出估計(jì)的 contentSize,有了 contentSize collection view 就開始顯示
- collection view 在顯示的過程中,即將被顯示的 cell 根據(jù) autolayout 的約束算出自適應(yīng)內(nèi)容的 size
- layout 從 collection view 里獲取更新過的 size attribute
- layout 返回最終的 size attribute 給 collection view
- collection 使用這個(gè)最終的 size attribute 展示 cell
UITextView 輸入內(nèi)容實(shí)時(shí)更新 cell 的高度
在一個(gè)動(dòng)態(tài)數(shù)據(jù)的 table view 中,cell 根據(jù) text view 內(nèi)容的輸入實(shí)時(shí)改變 cell 和 table view 的高度。自動(dòng)計(jì)算 cell 高度的功能使用 iOS 8 才支持的自適應(yīng) cell,先上圖,我們最終要實(shí)現(xiàn)的效果是這樣的:
實(shí)現(xiàn)上面效果的基本原理是:
- 在 cell 中設(shè)置好 text view 的 autolayout,讓 cell 可以根據(jù)內(nèi)容自適應(yīng)大小
- text view 中輸入內(nèi)容,根據(jù)內(nèi)容更新 textView 的高度
- 調(diào)用 tableView 的 beginUpdates 和 endUpdates,重新計(jì)算 cell 的高度
將 text view 更新后的數(shù)據(jù)保存,以免 table view 滾動(dòng)超過一屏再滾回來 text view 中的數(shù)據(jù)又不刷新成原來的數(shù)據(jù)了。
功能具體實(shí)現(xiàn)方法:
新建一個(gè)項(xiàng)目,拉出 TableViewController,在 cell 上添加一個(gè) UITextView。
首先設(shè)置 text view 的 autolayout,比較關(guān)鍵的 constraint 是要設(shè)置 textView 的高度大于等于一個(gè)值。如圖:
然后,設(shè)置 UITextView 的 scrollEnable 為 NO。這一點(diǎn)很關(guān)鍵,如果不設(shè)置為 NO,UITextView 在內(nèi)容超出 frame 后,重新設(shè)置 text view 的高度會(huì)失效,并出現(xiàn)滾動(dòng)條。
根據(jù)剛才在 storyboard 中創(chuàng)建的 cell,新建一個(gè) UITableViewCell 類。
#import <UIKit/UIKit.h>
@interface TextViewCell : UITableViewCell
@property (weak, nonatomic) IBOutlet UITextView *textView;
@end
創(chuàng)建 TableViewController 并初始化一些數(shù)據(jù)
#import "TableViewController.h"
#import "TextViewCell.h"
@interface TableViewController ()
@property (nonatomic, strong) NSArray *data;
@end
@implementation TableViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 支持自適應(yīng) cell
self.tableView.estimatedRowHeight = 100;
self.tableView.rowHeight = UITableViewAutomaticDimension;
self.data = @[@"Cell 1 ", @"Cell 2", @"Cell 3", @"Cell 4", @"Cell 5", @"Cell 6", @"Cell 7", @"Cell 8"];
}
#pragma mark - Table view data source
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [self.data count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
TextViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"TextViewCell" forIndexPath:indexPath];
cell.textView.text = self.data[indexPath.row];
return cell;
}
使用上面的代碼項(xiàng)目已經(jīng)可以運(yùn)行了,但是 text view 還不能自動(dòng)更新大小,下面來實(shí)現(xiàn) text view 根據(jù)內(nèi)容計(jì)算高度
先在 storyboard 中,將 UITextView 的 delegate 設(shè)置為 cell
在 TextViewCell.m 中實(shí)現(xiàn) - (void)textViewDidChange:(UITextView *)textView,每次 text view 內(nèi)容改變的時(shí)候,就重新計(jì)算一次 text view 的大小,并讓 table view 更新高度。
#import "TextViewCell.h"
@implementation TextViewCell
- (void)textViewDidChange:(UITextView *)textView
{
CGRect bounds = textView.bounds;
// 計(jì)算 text view 的高度
CGSize maxSize = CGSizeMake(bounds.size.width, CGFLOAT_MAX);
CGSize newSize = [textView sizeThatFits:maxSize];
bounds.size = newSize;
textView.bounds = bounds;
// 讓 table view 重新計(jì)算高度
UITableView *tableView = [self tableView];
[tableView beginUpdates];
[tableView endUpdates];
}
- (UITableView *)tableView
{
UIView *tableView = self.superview;
while (![tableView isKindOfClass:[UITableView class]] && tableView) {
tableView = tableView.superview;
}
return (UITableView *)tableView;
}
@end
這樣就已經(jīng)實(shí)現(xiàn)了 text view 改變內(nèi)容自動(dòng)更新 cell 高度的功能,這篇文章沒有涉及到計(jì)算 cell 高度的代碼,因?yàn)橛?jì)算 cell 高度的工作全部交給 iOS 8 的 autolayout 自動(dòng)計(jì)算了,這讓我們少寫了許多令人痛苦的代碼。
最后:為了防止 table view 過長(zhǎng),導(dǎo)致滾動(dòng)后重新加載 cell,會(huì)讓 text view 中的內(nèi)容還原的問題,我們應(yīng)該在更新了 text view 的內(nèi)容之后保存數(shù)據(jù)。(如果是在編輯狀態(tài)下,還需要考慮取消編輯后的回滾功能。 普通數(shù)組數(shù)據(jù),可以保存一個(gè)原始數(shù)據(jù)的副本,如果用戶取消編輯,就設(shè)置 data 為原始數(shù)據(jù)的副本。如果是 NSManagedObject 對(duì)象可以使用 NSUndoManage,不過這些已經(jīng)超出本篇文章的內(nèi)容范圍了。)
為了在 text view 更新后能讓 TableViewController 中的 data 更新,需要為 cell 添加一個(gè) delegate,在 text view 更新后調(diào)用 delegate,TableViewController 中收到 delegate 信息后更新 data。
修改后的 TextViewCell.h
#import <UIKit/UIKit.h>
@protocol TextViewCellDelegate;
@interface TextViewCell : UITableViewCell
@property (weak, nonatomic) IBOutlet UITextView *textView;
@property (weak, nonatomic) id<TextViewCellDelegate> delegate;
@end
@protocol TextViewCellDelegate <NSObject>
- (void)textViewCell:(TextViewCell *)cell didChangeText:(NSString *)text;
@end
在 TextView.m的 - (void)textViewDidChange:(UITextView *)textView 中添加 delegate 的調(diào)用
- (void)textViewDidChange:(UITextView *)textView
{
if ([self.delegate respondsToSelector:@selector(textViewCell:didChangeText:)]) {
[self.delegate textViewCell:self didChangeText:textView.text];
}
// 計(jì)算 text view 的高度
...
// 讓 table view 重新計(jì)算高度
...
}
最后在 TableViewController.m 的最后實(shí)現(xiàn) TextViewCellDelegate 的方法,更新 data
#pragma mark - TextViewCellDelegate
- (void)textViewCell:(TextViewCell *)cell didChangeText:(NSString *)text
{
NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
NSMutableArray *data = [self.data mutableCopy];
data[indexPath.row] = text;
self.data = [data copy];
}
相關(guān)文章
關(guān)于iOS GangSDK的使用 為App快速集成社群公會(huì)模塊
這篇文章主要介紹了iOS GangSDK的使用為App快速集成社群公會(huì)模塊功能的實(shí)現(xiàn)過程。2017-11-11詳解IOS開發(fā)之實(shí)現(xiàn)App消息推送(最新)
這篇文章主要介紹了詳解IOS開發(fā)之實(shí)現(xiàn)App消息推送(最新),具有一定的參考價(jià)值,有興趣的可以了解一下。2016-12-12移動(dòng)端頁(yè)面在ios中不顯示圖片的解決方法
下面小編就為大家?guī)硪黄苿?dòng)端頁(yè)面在ios中不顯示圖片的解決方法。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-11-11iOS開發(fā)實(shí)戰(zhàn)之Label全方位對(duì)齊的輕松實(shí)現(xiàn)
這篇文章主要給大家介紹了關(guān)于iOS開發(fā)實(shí)戰(zhàn)之輕松實(shí)現(xiàn)Label全方位對(duì)齊的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-10-10