學習了 FDTemplateLayoutCell 后,我自己也寫了一個 TableView 行高自適應加高度緩存的 Demo,本 Demo 研究實現了其中的最基本算高與緩存功能,僅供大家學習使用。
在開始之前,先讓我們了解一些 Runtime 的知識,objc_setAssociatedObject與objc_getAssociatedObject這兩個函數。
讓我們來看一個例子
/*
object 要持有“別的對象”的對象
key 關聯關鍵字,是一個字符串常量,是一個地址(這里注意,地址必須是不變的,地址不同但是內容相同的也不算同一個key)
value 也就是值
policy 這是一個枚舉,你可以點進去看看這個枚舉是什么:
OBJC_ASSOCIATION_ASSIGN
OBJC_ASSOCIATION_RETAIN_NONATOMIC
OBJC_ASSOCIATION_COPY_NONATOMIC
OBJC_ASSOCIATION_RETAIN
OBJC_ASSOCIATION_COPY
*/
//參數一:需要添加屬性的對象 參數二:關聯關鍵字(關聯關鍵字要與get方法中的關鍵字相同,是一個指針類型) 參數三:屬性名 參數四:枚舉與@property括號中相同
objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_COPY_NONATOMIC);
objc_setAssociatedObject這個函數的意思就是通過一個 key 為一個對象綁定另一個對象
/*
object 持有“別的對象”的對象,這里指a
key 關聯關鍵字
*/
objc_getAssociatedObject(self, @"name");
objc_getAssociatedObject這個函數的意思是通過一個 key 取到一個對象綁定的那個對象
在上面這個例子中,我們使用這兩個函數,為self所指的對象通過@"name"這個 key 綁定了一個值為name的對象
Runtime 就說這么多,如果小伙伴們想要更為深入的了解,請自行搜尋相關資料,至于為什么要說這兩個函數,請小伙伴們繼續(xù)往下面看。
————前方高能預警————
下面就是本文的重點了
為 UITableViewCell 創(chuàng)建一個 Category 目的是為其增加兩個屬性
為 Cell 添加兩個屬性,一個用來標志此 Cell 只用來計算高度,不進行顯示,另一個屬性標志是否使用約束來進行計算。添加這兩個屬性的目的是為了保證每一種類的 Cell 都有一個相應的計算 Cell,而且此種類的計算 Cell 有且只有一個,如果你此時還有些懵逼,那請帶著你的疑問繼續(xù)往下看。
什么?你說 Category 不能添加屬性?的確,Category 確實不能添加屬性,但是我們有萬能的 Runtime 啊,來看看我們是怎么做的
@interface UITableViewCell (HeightCacheCell)
//添加兩個屬性
@property (assign, nonatomic)BOOL justForCalculate; //只用來計算的標志
@property (assign, nonatomic)BOOL noAuotSizeing; //不依靠約束計算,只進行自適應
@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
重寫這兩個屬性的 get set 方法,并使用剛才學到的兩個 Runtime 方法,為 UITableViewCell 綁定了兩個對象,這樣一來,我們就變相的為 UITableViewCell 添加了兩個屬性
創(chuàng)建一個 Cache 類,用來緩存相應 Cell 的高度
@interface HeightCache : NSObject
@property (strong, nonatomic)NSMutableDictionary *heightCacheDicV; //豎直行高緩存字典
@property (strong, nonatomic)NSMutableDictionary *heightCacheDicH; //水平行高緩存字典
@property (strong, nonatomic)NSMutableDictionary *heightCacheDicCurrent; //當前行高緩存字典
//制作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 這樣一個類,為其添加了三個字典作為屬性,分別存儲在手機橫屏豎屏下的 Cell 緩存高度,Current 字典為當前手機屏幕狀態(tài)下的緩存字典,在它的懶加載方法中,我們將判斷使用的是橫屏緩存字典還是豎屏緩存字典。暴露四個方法,分別是“制作從緩存字典中取緩存高度的 key”、“判斷此 key 下是否有緩存高度”、“通過 key 取出緩存高度”、“通過 key 將對應高度緩存”這四個方法。
實現相當簡單,這里直接貼上代碼,不做過多解釋。
@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;
}
//根據橫豎屏狀態(tài)選擇字典
- (NSMutableDictionary *)heightCacheDicCurrent{
return UIDeviceOrientationIsPortrait([UIDevice currentDevice].orientation)?self.heightCacheDicV:self.heightCacheDicH;
}
@end
重點!創(chuàng)建 UITableView 的 Category ,計算 Cell 高度并緩存
我們首先為 UITableView 添加一個 HeightCache 作為屬性,方便用來存儲高度緩存,這里還是用 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);
}
從復用池中獲取一個用于計算的 Cell
//獲取一個用于計算高度的Cell
- (__kindof UITableViewCell *)LLQ_CalculateCellWithIdentifier:(NSString *)identifier{
if (!identifier.length) {
return nil;
}
//runtime獲取一個存儲cell的字典
NSMutableDictionary <NSString *, UITableViewCell *> *dicForTheUniqueCalCell = objc_getAssociatedObject(self, _cmd);
//如果取不到,就綁定一個
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; //設置為NO才能用代碼使用AutoLayout
cell.justForCalculate = YES; //設置只計算
dicForTheUniqueCalCell[identifier] = cell;
}
return cell;
}
此方法中為 UITableView 綁定了一個字典,目的是存儲某一種類的 Cell,而區(qū)分 Cell 種類的辦法就是通過 Cell 的重用標識符。通過重用標識符從字典中獲取 Cell,如果獲取不到,就從 TableView 的復用池中取出一個此種類的 Cell,并設置只計算屬性,存入綁定的字典,這樣一來,我們就保證了每種類的 Cell 有且只有一個用來計算。要注意的是,在實際項目使用中我們必須使用-registerClass:forCellReuseIdentifier: 或 -registerNib:forCellReuseIdentifier:其中之一的方法對 Cell 進行注冊。
計算 Cell 的高度
//計算cell高度
- (CGFloat)LLQ_CalculateCellHeightWithCell:(UITableViewCell *)cell{
CGFloat width = self.bounds.size.width;
//根據輔助視圖,調整寬度
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;
//非自適應模式,添加約束后計算約束后高度
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];
//根據約束計算高度
height = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
[cell.contentView removeConstraint:widthConstraint]; //移除約束
}
//如果約束添加錯誤,可能導致計算結果為0,則采用自適應模式計算約束
if (height == 0) {
height = [cell sizeThatFits:CGSizeMake(width, 0)].height;
}
//還是為0,默認高度
if (height == 0) {
height = 44;
}
if (self.separatorStyle != UITableViewCellSeparatorStyleNone) {
height += 1.0/[UIScreen mainScreen].scale;
}
return height;
}
首先計算 Cell 的 width,如果有輔助視圖,我們還要修正 width,判斷是否是 AutoSizeing 模式,來決定使用哪種方式算 Cell 的高度,如果使用約束算高,就是通過添加一個我們算好的固定 width 的約束,從而得出 Cell 的高度。能夠這樣做的前提是我們在 xib 中使用的 autolayout 約束正確。在最后判斷一下有無分割線,做最后一次高度修正。
將上面兩個方法整合,給 Cell 填充數據后計算出當前 Cell 的高度
//取出cell并對cell進行操作,然后計算高度
- (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];
}
首先獲取一個 Cell 然后將其放回復用池(因為我們在取 Cell 的方法中沒有將其放回),然后給 Cell 填充數據,這里使用了 block 將 Cell 傳遞到外界,填充完數據后使用算高方法計算高度。
計算高度,并將計算的高度緩存,本方法暴露給外界共外界調用
//供外部調用的方法
- (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]; //從字典中取出高度
}
//沒有緩存,計算緩存
CGFloat height = [self LLQ_CalculateCellWithIdentifier:identifier configuration:configuration];
//并進行緩存
[self.heightCache cacheHieght:height key:key];
return height;
}
return 0;
}
首先使用重用標識符和 IndexPath 制作高度緩存的 key,這樣制作出的 key 就能保證種類、組、行的唯一性,然后使用這個 key 去取緩存的高度,若沒有緩存高度就進行計算。
本 Demo 實現了 TableView 的行高自適應與行高緩存,這只是 FDTemplateLayoutCell 的一部分主要功能,在項目復雜情況下不夠適用,比如在移動一個單元格,刪除一個單元格等情況時本 Demo 沒有相應的處理實現,如果各位小伙伴項目需要,請直接使用 FDTemplateLayoutCell。
本 Demo 僅供學習使用。
最后,我還是會按照慣例把 Demo 共享給大家
Demo點這里!點這里!點這里!