學(xué)習(xí)了 FDTemplateLayoutCell 后斋日,我自己也寫(xiě)了一個(gè) TableView 行高自適應(yīng)加高度緩存的 Demo苞慢,本 Demo 研究實(shí)現(xiàn)了其中的最基本算高與緩存功能,僅供大家學(xué)習(xí)使用印机。
在開(kāi)始之前稀蟋,先讓我們了解一些 Runtime 的知識(shí)期吓,objc_setAssociatedObject
與objc_getAssociatedObject
這兩個(gè)函數(shù)乘盖。
讓我們來(lái)看一個(gè)例子
/*
object 要持有“別的對(duì)象”的對(duì)象
key 關(guān)聯(lián)關(guān)鍵字焰檩,是一個(gè)字符串常量,是一個(gè)地址(這里注意订框,地址必須是不變的析苫,地址不同但是內(nèi)容相同的也不算同一個(gè)key)
value 也就是值
policy 這是一個(gè)枚舉,你可以點(diǎn)進(jìn)去看看這個(gè)枚舉是什么:
OBJC_ASSOCIATION_ASSIGN
OBJC_ASSOCIATION_RETAIN_NONATOMIC
OBJC_ASSOCIATION_COPY_NONATOMIC
OBJC_ASSOCIATION_RETAIN
OBJC_ASSOCIATION_COPY
*/
//參數(shù)一:需要添加屬性的對(duì)象 參數(shù)二:關(guān)聯(lián)關(guān)鍵字(關(guān)聯(lián)關(guān)鍵字要與get方法中的關(guān)鍵字相同穿扳,是一個(gè)指針類型) 參數(shù)三:屬性名 參數(shù)四:枚舉與@property括號(hào)中相同
objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_COPY_NONATOMIC);
objc_setAssociatedObject
這個(gè)函數(shù)的意思就是通過(guò)一個(gè) key 為一個(gè)對(duì)象綁定另一個(gè)對(duì)象
/*
object 持有“別的對(duì)象”的對(duì)象衩侥,這里指a
key 關(guān)聯(lián)關(guān)鍵字
*/
objc_getAssociatedObject(self, @"name");
objc_getAssociatedObject
這個(gè)函數(shù)的意思是通過(guò)一個(gè) key 取到一個(gè)對(duì)象綁定的那個(gè)對(duì)象
在上面這個(gè)例子中,我們使用這兩個(gè)函數(shù)矛物,為self
所指的對(duì)象通過(guò)@"name"
這個(gè) key 綁定了一個(gè)值為name
的對(duì)象
Runtime 就說(shuō)這么多茫死,如果小伙伴們想要更為深入的了解,請(qǐng)自行搜尋相關(guān)資料履羞,至于為什么要說(shuō)這兩個(gè)函數(shù)峦萎,請(qǐng)小伙伴們繼續(xù)往下面看。
————前方高能預(yù)警————
下面就是本文的重點(diǎn)了
為 UITableViewCell 創(chuàng)建一個(gè) Category 目的是為其增加兩個(gè)屬性
為 Cell 添加兩個(gè)屬性忆首,一個(gè)用來(lái)標(biāo)志此 Cell 只用來(lái)計(jì)算高度爱榔,不進(jìn)行顯示,另一個(gè)屬性標(biāo)志是否使用約束來(lái)進(jìn)行計(jì)算糙及。添加這兩個(gè)屬性的目的是為了保證每一種類的 Cell 都有一個(gè)相應(yīng)的計(jì)算 Cell详幽,而且此種類的計(jì)算 Cell 有且只有一個(gè),如果你此時(shí)還有些懵逼浸锨,那請(qǐng)帶著你的疑問(wèn)繼續(xù)往下看妒潭。
什么?你說(shuō) Category 不能添加屬性揣钦?的確,Category 確實(shí)不能添加屬性漠酿,但是我們有萬(wàn)能的 Runtime 啊冯凹,來(lái)看看我們是怎么做的
@interface UITableViewCell (HeightCacheCell)
//添加兩個(gè)屬性
@property (assign, nonatomic)BOOL justForCalculate; //只用來(lái)計(jì)算的標(biāo)志
@property (assign, nonatomic)BOOL noAuotSizeing; //不依靠約束計(jì)算,只進(jìn)行自適應(yīng)
@end
@implementation UITableViewCell (HeightCacheCell)
#pragma mark ------ 綁定屬性
//justForCall
- (void)setJustForCalculate:(BOOL)justForCalculate{
objc_setAssociatedObject(self, @selector(justForCalculate), @(justForCalculate), OBJC_ASSOCIATION_RETAIN); //使用get方法名作為key
}
- (BOOL)justForCalculate{
return [objc_getAssociatedObject(self, _cmd) boolValue];
}
//noAuotSizeing
- (void)setNoAuotSizeing:(BOOL)noAuotSizeing{
objc_setAssociatedObject(self, @selector(noAuotSizeing), @(noAuotSizeing), OBJC_ASSOCIATION_RETAIN);
}
- (BOOL)noAuotSizeing{
return [objc_getAssociatedObject(self, _cmd) boolValue];
}
@end
重寫(xiě)這兩個(gè)屬性的 get set 方法炒嘲,并使用剛才學(xué)到的兩個(gè) Runtime 方法宇姚,為 UITableViewCell 綁定了兩個(gè)對(duì)象,這樣一來(lái)夫凸,我們就變相的為 UITableViewCell 添加了兩個(gè)屬性
創(chuàng)建一個(gè) Cache 類浑劳,用來(lái)緩存相應(yīng) Cell 的高度
@interface HeightCache : NSObject
@property (strong, nonatomic)NSMutableDictionary *heightCacheDicV; //豎直行高緩存字典
@property (strong, nonatomic)NSMutableDictionary *heightCacheDicH; //水平行高緩存字典
@property (strong, nonatomic)NSMutableDictionary *heightCacheDicCurrent; //當(dāng)前行高緩存字典
//制作key
- (NSString *)makeKeyWithIdentifier:(NSString *)identifier indexPath:(NSIndexPath *)indexPath;
//判斷高度是否存在
- (BOOL)existInCacheByKey:(NSString *)key;
//查找高度緩存
- (CGFloat)heightFromCacheWithKey:(NSString *)key;
//緩存
- (void)cacheHieght:(CGFloat)hieght key:(NSString *)key;
@end
創(chuàng)建 HeightCache 這樣一個(gè)類,為其添加了三個(gè)字典作為屬性夭拌,分別存儲(chǔ)在手機(jī)橫屏豎屏下的 Cell 緩存高度魔熏,Current 字典為當(dāng)前手機(jī)屏幕狀態(tài)下的緩存字典衷咽,在它的懶加載方法中,我們將判斷使用的是橫屏緩存字典還是豎屏緩存字典蒜绽。暴露四個(gè)方法镶骗,分別是“制作從緩存字典中取緩存高度的 key”、“判斷此 key 下是否有緩存高度”躲雅、“通過(guò) key 取出緩存高度”鼎姊、“通過(guò) key 將對(duì)應(yīng)高度緩存”這四個(gè)方法。
實(shí)現(xiàn)相當(dāng)簡(jiǎn)單相赁,這里直接貼上代碼相寇,不做過(guò)多解釋。
@implementation HeightCache
//制作key
- (NSString *)makeKeyWithIdentifier:(NSString *)identifier indexPath:(NSIndexPath *)indexPath{
return [NSString stringWithFormat:@"%@S%ldR%ld",identifier,indexPath.section,indexPath.row];
}
//判斷高度是否存在
- (BOOL)existInCacheByKey:(NSString *)key{
NSNumber * value = [self.heightCacheDicCurrent objectForKey:key];
return (value && ![value isEqualToNumber:@-1]);
}
//取出緩存的高度
- (CGFloat)heightFromCacheWithKey:(NSString *)key{
NSNumber *value = [self.heightCacheDicCurrent objectForKey:key];
return [value floatValue];
}
//緩存
- (void)cacheHieght:(CGFloat)hieght key:(NSString *)key{
[self.heightCacheDicCurrent setObject:@(hieght) forKey:key];
}
//lazy
- (NSMutableDictionary *)heightCacheDicH{
if (!_heightCacheDicH) {
_heightCacheDicH = [[NSMutableDictionary alloc] init];
}
return _heightCacheDicH;
}
- (NSMutableDictionary *)heightCacheDicV{
if (!_heightCacheDicV) {
_heightCacheDicV = [[NSMutableDictionary alloc] init];
}
return _heightCacheDicV;
}
//根據(jù)橫豎屏狀態(tài)選擇字典
- (NSMutableDictionary *)heightCacheDicCurrent{
return UIDeviceOrientationIsPortrait([UIDevice currentDevice].orientation)?self.heightCacheDicV:self.heightCacheDicH;
}
@end
重點(diǎn)钮科!創(chuàng)建 UITableView 的 Category 唤衫,計(jì)算 Cell 高度并緩存
我們首先為 UITableView 添加一個(gè) HeightCache 作為屬性,方便用來(lái)存儲(chǔ)高度緩存跺嗽,這里還是用 Runtime 的方法
#pragma mark ------ 綁定屬性
- (HeightCache *)heightCache{
HeightCache *cache = objc_getAssociatedObject(self, _cmd);
if (!cache) {
cache = [[HeightCache alloc] init];
objc_setAssociatedObject(self, _cmd, cache, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return cache;
}
- (void)setHeightCache:(HeightCache *)heightCache{
objc_setAssociatedObject(self, @selector(heightCache), heightCache, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
從復(fù)用池中獲取一個(gè)用于計(jì)算的 Cell
//獲取一個(gè)用于計(jì)算高度的Cell
- (__kindof UITableViewCell *)LLQ_CalculateCellWithIdentifier:(NSString *)identifier{
if (!identifier.length) {
return nil;
}
//runtime獲取一個(gè)存儲(chǔ)cell的字典
NSMutableDictionary <NSString *, UITableViewCell *> *dicForTheUniqueCalCell = objc_getAssociatedObject(self, _cmd);
//如果取不到战授,就綁定一個(gè)
if (!dicForTheUniqueCalCell) {
dicForTheUniqueCalCell = [[NSMutableDictionary alloc] init];
objc_setAssociatedObject(self, _cmd, dicForTheUniqueCalCell, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
//取出cell,從綁定的字典取用
UITableViewCell *cell = dicForTheUniqueCalCell[identifier];
if (!cell) {
cell = [self dequeueReusableCellWithIdentifier:identifier];
cell.contentView.translatesAutoresizingMaskIntoConstraints = NO; //設(shè)置為NO才能用代碼使用AutoLayout
cell.justForCalculate = YES; //設(shè)置只計(jì)算
dicForTheUniqueCalCell[identifier] = cell;
}
return cell;
}
此方法中為 UITableView 綁定了一個(gè)字典桨嫁,目的是存儲(chǔ)某一種類的 Cell植兰,而區(qū)分 Cell 種類的辦法就是通過(guò) Cell 的重用標(biāo)識(shí)符。通過(guò)重用標(biāo)識(shí)符從字典中獲取 Cell璃吧,如果獲取不到楣导,就從 TableView 的復(fù)用池中取出一個(gè)此種類的 Cell,并設(shè)置只計(jì)算屬性畜挨,存入綁定的字典筒繁,這樣一來(lái),我們就保證了每種類的 Cell 有且只有一個(gè)用來(lái)計(jì)算巴元。要注意的是毡咏,在實(shí)際項(xiàng)目使用中我們必須使用-registerClass:forCellReuseIdentifier:
或 -registerNib:forCellReuseIdentifier:
其中之一的方法對(duì) Cell 進(jìn)行注冊(cè)。
計(jì)算 Cell 的高度
//計(jì)算cell高度
- (CGFloat)LLQ_CalculateCellHeightWithCell:(UITableViewCell *)cell{
CGFloat width = self.bounds.size.width;
//根據(jù)輔助視圖逮刨,調(diào)整寬度
if (cell.accessoryView) {
width -= cell.accessoryView.bounds.size.width + 16;
}
else{
static const CGFloat accessoryWith[] = {
[UITableViewCellAccessoryNone] = 0,
[UITableViewCellAccessoryCheckmark] = 40,
[UITableViewCellAccessoryDetailButton] = 48,
[UITableViewCellAccessoryDisclosureIndicator] = 34,
[UITableViewCellAccessoryDetailDisclosureButton] = 68,
};
width -= accessoryWith[cell.accessoryType];
}
CGFloat height = 0;
//非自適應(yīng)模式呕缭,添加約束后計(jì)算約束后高度
if (!cell.noAuotSizeing && width>0) {
NSLayoutConstraint *widthConstraint = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:width];
[cell.contentView addConstraint:widthConstraint];
//根據(jù)約束計(jì)算高度
height = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
[cell.contentView removeConstraint:widthConstraint]; //移除約束
}
//如果約束添加錯(cuò)誤,可能導(dǎo)致計(jì)算結(jié)果為0修己,則采用自適應(yīng)模式計(jì)算約束
if (height == 0) {
height = [cell sizeThatFits:CGSizeMake(width, 0)].height;
}
//還是為0恢总,默認(rèn)高度
if (height == 0) {
height = 44;
}
if (self.separatorStyle != UITableViewCellSeparatorStyleNone) {
height += 1.0/[UIScreen mainScreen].scale;
}
return height;
}
首先計(jì)算 Cell 的 width,如果有輔助視圖睬愤,我們還要修正 width片仿,判斷是否是 AutoSizeing 模式,來(lái)決定使用哪種方式算 Cell 的高度尤辱,如果使用約束算高砂豌,就是通過(guò)添加一個(gè)我們算好的固定 width 的約束厢岂,從而得出 Cell 的高度。能夠這樣做的前提是我們?cè)?xib 中使用的 autolayout 約束正確奸鸯。在最后判斷一下有無(wú)分割線咪笑,做最后一次高度修正。
將上面兩個(gè)方法整合娄涩,給 Cell 填充數(shù)據(jù)后計(jì)算出當(dāng)前 Cell 的高度
//取出cell并對(duì)cell進(jìn)行操作窗怒,然后計(jì)算高度
- (CGFloat)LLQ_CalculateCellWithIdentifier:(NSString *)identifier configuration:(void(^)(id cell))configuration{
if (!identifier.length) {
return 0;
}
UITableViewCell *cell = [self LLQ_CalculateCellWithIdentifier:identifier];
[cell prepareForReuse]; //放回重用池
if (configuration) {
configuration(cell);
}
return [self LLQ_CalculateCellHeightWithCell:cell];
}
首先獲取一個(gè) Cell 然后將其放回復(fù)用池(因?yàn)槲覀冊(cè)谌?Cell 的方法中沒(méi)有將其放回),然后給 Cell 填充數(shù)據(jù)蓄拣,這里使用了 block 將 Cell 傳遞到外界扬虚,填充完數(shù)據(jù)后使用算高方法計(jì)算高度。
計(jì)算高度球恤,并將計(jì)算的高度緩存辜昵,本方法暴露給外界共外界調(diào)用
//供外部調(diào)用的方法
- (CGFloat)LLQ_CalculateCellWithIdentifer:(NSString *)identifier indexPath:(NSIndexPath *)indexPath configuration:(void(^)(id cell))configuration{
if (self.bounds.size.width != 0) {
if (!identifier.length || !indexPath) {
return 0;
}
NSString *key = [self.heightCache makeKeyWithIdentifier:identifier indexPath:indexPath];
if ([self.heightCache existInCacheByKey:key]) { //如果有緩存,就取出緩存
return [self.heightCache heightFromCacheWithKey:key]; //從字典中取出高度
}
//沒(méi)有緩存咽斧,計(jì)算緩存
CGFloat height = [self LLQ_CalculateCellWithIdentifier:identifier configuration:configuration];
//并進(jìn)行緩存
[self.heightCache cacheHieght:height key:key];
return height;
}
return 0;
}
首先使用重用標(biāo)識(shí)符和 IndexPath 制作高度緩存的 key堪置,這樣制作出的 key 就能保證種類、組张惹、行的唯一性舀锨,然后使用這個(gè) key 去取緩存的高度,若沒(méi)有緩存高度就進(jìn)行計(jì)算宛逗。
本 Demo 實(shí)現(xiàn)了 TableView 的行高自適應(yīng)與行高緩存坎匿,這只是 FDTemplateLayoutCell 的一部分主要功能,在項(xiàng)目復(fù)雜情況下不夠適用雷激,比如在移動(dòng)一個(gè)單元格替蔬,刪除一個(gè)單元格等情況時(shí)本 Demo 沒(méi)有相應(yīng)的處理實(shí)現(xiàn),如果各位小伙伴項(xiàng)目需要屎暇,請(qǐng)直接使用 FDTemplateLayoutCell承桥。
本 Demo 僅供學(xué)習(xí)使用。
最后根悼,我還是會(huì)按照慣例把 Demo 共享給大家
Demo點(diǎn)這里凶异!點(diǎn)這里!點(diǎn)這里番挺!