iOSApp开发中使cell高度自适应的黑魔法详解
这篇文章主要介绍了iOSApp开发中使cell高度自适应的黑魔法详解,作者利用iOS8以后的新特性讲解了TableView、CollectionView中的cell高度自适应以及UITextView输入内容实时更新cell高度的方法,需要的朋友可以参考下
在使用tableview的时侯经常会遇到这样的需求:tableview的cell中的内容是动态的,导致在开发的时候不知道一个cell的高度具体是多少,所以需要提供一个计算cell高度的算法,在每次加载到这个cell的时候计算出cell真正的高度。
在iOS8之前
没有使用Autolayout的情况下,需要实现tableviewdelegate的tableView(tableView:UITableView,heightForRowAtIndexPathindexPath:NSIndexPath)->CGFloat方法,在这个方法中计算并返回cell的高度。比如,我有一个可以显示任意行数的纯文本cell,计算cell的代码可以是这样:
复制代码代码如下:
overridefunctableView(tableView:UITableView,heightForRowAtIndexPathindexPath:NSIndexPath)->CGFloat{letcontent=self.datas[indexPath.row]asString
letpadding:CGFloat=20letwidth=tableView.frame.size.width-padding2;
letsize=CGSizeMake(width,CGFloat.max)letattributes=[NSFontAttributeName:UIFont(name:"Helvetica",size:14)!]letframe=content.boundingRectWithSize(size,options:NSStringDrawingOptions.UsesLineFragmentOrigin,attributes:attributes,context:nil)returnframe.size.height+1;}
上面的代码是一个最简单的例子,这个例子看起来好像没有什么问题。但是通过查看这个delegate方法的文档后,可以知道,在每次reloadtableview的时候,程序会先计算出每一个cell的高度,等所有高度计算完毕,确定了tableview的总的高度后,才开始渲染视图并显示在屏幕上。这意味着在显示tableview之前需要执行一堆的计算,并且这是在主线程中进行的,如果计算量太大程序就很有可能出现卡顿感。比如:tableview的数据有上千条,或者计算高度的代码中还要先获取图片再根据图片计算高度,这些操作都是非常慢的。
如果在cell中使用了autolayout,在计算cell高度时会更麻烦。有兴趣的可以看这里有篇关于如何在autolayout下动态计算高度的文章。
为什么不能等滚动到某个cell的时候,再调用计算这个cell高度的delegate呢?原因是tableview需要获得它的内容的总高度,用这个高度去确定滚动条的大小等。直到iOS7UITableViewDelegate中添加了新的API
tableView(tableView:UITableView,estimatedHeightForRowAtIndexPathindexPath:NSIndexPath)->CGFloat这个方法用于返回一个cell的预估高度,如果在程序中实现了这个方法,tableview首次加载的时候就不会调用heightForRowAtIndexPath方法,而是用estimatedHeightForRowAtIndexPath返回的预估高度计算tableview的总高度,然后tableview就可以显示出来了,等到cell可见的时候,再去调用heightForRowAtIndexPath获取cell的正确高度。
通过使用estimatedHeightForRowAtIndexPath这个Delegate方法,解决了首次加载tableview出现的性能问题。但还有一个麻烦的问题,就是在cell没有被加载的时候计算cell的高度,上面给出的代码中,仅仅是计算一个NSString的高度,就需要不少代码了。这种计算实际上是必须的,然而在iOS8开始,你可能可以不用再写这些烦人的计算代码了!
iOS8的魔法
在iOS8中,selfsizecell提供了这样一种机制:cell如果有一个确定的宽度/高度,autolayout会自动根据cell中的内容计算出对应的高度/宽度。
TableView中的cell自适应
要让tableview的cell自适应内容,有几个要点:
设置的AutoLayout约束必须让cell的contentView知道如何自动延展。关键点是contentView的4个边都要设置连接到内容的约束,并且内容是会动态改变尺寸的。UITableView的rowHeight的值要设置为UITableViewAutomaticDimension和iOS7一样,可以实现estimatedHeightForRowAtIndexPath方法提升tableview的第一次加载速度。任何时候cell的intrinsicContentSize改变了(比如tableview的宽度变了),都必须重新加载tableview以更新cell。例子
在Xcode中新建一个项目,在storyboard中创建一个UITableViewController的IB,创建一个如下样子的cell:
这个cell中有3个元素,其中imageView的autoLayout约束为:
imageView左边离contentView左边0
imageView上边离contentView上边0
imageView的width和height为80
imageView下边离contentView下边大于等于0(为了防止内容太少,导致cell高度小于图片高度)
titleLabel的autoLayout约束为:
titleLabel左边离imageView右边8
titleLabel上边和imageView上边在同一只线上
titleLabel右边离contentView右边0
titleLabel下边离description上边8
titleLabel的高度小于等于22,优先级为250
descriptionLabel的约束为:
descriptionLabel左边和titleLabel左边在同一直线上
descriptionLabel上边里titleLabel8
descriptionLabel下边里contentView下边0
descriptionLabel右边离contentView右边0
然后在这个IB对应的UITableViewController中加载一些数据进去,显示效果如图:
实现这个效果,我除了设置了autoLayout,还设置了tableView的rowHeight=UITableViewAutomaticDimension,然后就是这样了。一点计算cell高度的代码都没有!!我连heightForRowAtIndexPath都不用实现,真的是….爽出味啊!所以如果已经在开发iOS8Only的应用了一定要用autolayout,把烦人的计算交给autolayout去吧。
CollectionView中的cell自适应
在collectionview中也能让cell自适应内容大小,如果UICollectionView的layout是一个UICollectionViewFlowLayout,只需要将layout.itemSize=...改成layout.estimatedItemSize=...。只要设置了layout的estimatedItemSize,collectionview就会根据cell里面的autolayout约束去确定cell的大小。
原理:
collectionview根据layout的estimatedItemSize算出估计的contentSize,有了contentSizecollectionview就开始显示
collectionview在显示的过程中,即将被显示的cell根据autolayout的约束算出自适应内容的size
layout从collectionview里获取更新过的sizeattribute
layout返回最终的sizeattribute给collectionview
collection使用这个最终的sizeattribute展示cell
UITextView输入内容实时更新cell的高度在一个动态数据的tableview中,cell根据textview内容的输入实时改变cell和tableview的高度。自动计算cell高度的功能使用iOS8才支持的自适应cell,先上图,我们最终要实现的效果是这样的:
实现上面效果的基本原理是:
在cell中设置好textview的autolayout,让cell可以根据内容自适应大小
textview中输入内容,根据内容更新textView的高度
调用tableView的beginUpdates和endUpdates,重新计算cell的高度
将textview更新后的数据保存,以免tableview滚动超过一屏再滚回来textview中的数据又不刷新成原来的数据了。
功能具体实现方法:
新建一个项目,拉出TableViewController,在cell上添加一个UITextView。
首先设置textview的autolayout,比较关键的constraint是要设置textView的高度大于等于一个值。如图:
然后,设置UITextView的scrollEnable为NO。这一点很关键,如果不设置为NO,UITextView在内容超出frame后,重新设置textview的高度会失效,并出现滚动条。
根据刚才在storyboard中创建的cell,新建一个UITableViewCell类。
复制代码代码如下:
#import
@interfaceTextViewCell:UITableViewCell
@property(weak,nonatomic)IBOutletUITextViewtextView;
@end
创建TableViewController并初始化一些数据
复制代码代码如下:
#import"TableViewController.h"#import"TextViewCell.h"
@interfaceTableViewController()
@property(nonatomic,strong)NSArraydata;
@end
复制代码代码如下:
@implemenwww.visa158.comtationTableViewController
-(void)viewDidLoad{[superviewDidLoad];
//支持自适应cellself.tableView.estimatedRowHeight=100;self.tableView.rowHeight=UITableViewAutomaticDimension;
self.data=@[@"Cell1",@"Cell2",@"Cell3",@"Cell4",@"Cell5",@"Cell6",@"Cell7",@"Cell8"];}
#pragmamark-Tableviewdatasource
-(NSInteger)tableView:(UITableView)tableViewnumberOfRowsInSection:(NSInteger)section{return[self.datacount];}
-(UITableViewCell)tableView:(UITableView)tableViewcellForRowAtIndexPath:(NSIndexPath)indexPath{TextViewCellcell=[tableViewdequeueReusableCellWithIdentifier:@"TextViewCell"forIndexPath:indexPath];cell.textView.text=self.data[indexPath.row];returncell;}
使用上面的代码项目已经可以运行了,但是textview还不能自动更新大小,下面来实现textview根据内容计算高度
先在storyboard中,将UITextView的delegate设置为cell
在TextViewCell.m中实现-(void)textViewDidChange:(UITextView)textView,每次textview内容改变的时候,就重新计算一次textview的大小,并让tableview更新高度。
复制代码代码如下:
#import"TextViewCell.h"
@implementationTextViewCell
-(void)textViewDidChange:(UITextView)textView{CGRectbounds=textView.bounds;
//计算textview的高度CGSizemaxSize=CGSizeMake(bounds.size.width,CGFLOAT_MAX);CGSizenewSize=[textViewsizeThatFits:maxSize];bounds.size=newSize;
textView.bounds=bounds;
//让tableview重新计算高度UITableViewtableView=[selftableView];[tableViewbeginUpdates];[tableViewendUpdates];}
-(UITableView)tableView{UIViewtableView=self.superview;
while(![tableViewisKindOfClass:[UITableViewclass]]&&tableView){tableView=tableView.www.hunanwang.netsuperview;}
return(UITableView)tableView;}
@end
这样就已经实现了textview改变内容自动更新cell高度的功能,这篇文章没有涉及到计算cell高度的代码,因为计算cell高度的工作全部交给iOS8的autolayout自动计算了,这让我们少写了许多令人痛苦的代码。
最后:为了防止tableview过长,导致滚动后重新加载cell,会让textview中的内容还原的问题,我们应该在更新了textview的内容之后保存数据。(如果是在编辑状态下,还需要考虑取消编辑后的回滚功能。普通数组数据,可以保存一个原始数据的副本,如果用户取消编辑,就设置data为原始数据的副本。如果是NSManagedObject对象可以使用NSUndoManage,不过这些已经超出本篇文章的内容范围了。)
为了在textview更新后能让TableViewController中的data更新,需要为cell添加一个delegate,在textview更新后调用delegate,TableViewController中收到delegate信息后更新data。
修改后的TextViewCell.h
复制代码代码如下:
#import
@protocolTextViewCellDelegate;
@interfaceTextViewCell:UITableViewCell
@property(weak,nonatomic)IBOutletUITextViewtextView;
@property(weak,nonatomic)iddelegate;
@end
复制代码代码如下:
@protocolTextViewCellDelegate
-(void)textViewCell:(TextViewCell)celldidChangeText:(NSString)text;
@end
在TextView.m的-(void)textViewDidChange:(UITextView)textView中添加delegate的调用
复制代码代码如下:
-(void)textViewDidChange:(UITextView)textView{if([self.delegaterespondsToSelector:@selector(textViewCell:didChangeText:)]){[self.delegatetextViewCell:selfdidChangeText:textView.text];}
//计算textview的高度...//让tableview重新计算高度...}
最后在TableViewController.m的最后实现TextViewCellDelegate的方法,更新data
复制代码代码如下:
#pragmamark-TextViewCellDelegate
-(void)textViewCell:(TextViewCell)celldidChangeText:(NSString)text{NSIndexPathindexPath=[self.tableViewindexPathForCell:cell];
NSMutableArraydata=[self.datamutableCopy];data[indexPath.row]=text;self.data=[datacopy];}
|
|