經過實現一個橫向Tableview,瞭解UITableview工做原理

做者coderZ

UITableview代理方法介紹

UITableview有兩個相關代理UITableViewDelegate、UITableViewDataSource
dataSource是數據源代理,delegate則是相關操做代理git

dataSource

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section

經過返回值,告訴tableview的某個section應該顯示多少個單元格github

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath

經過返回值,告訴tableview,indexPath索引下的單元格的高度,tableview單元格的寬度與tableview相同數組

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath

經過返回cell,告訴tableview,indexPath索引下應該展示的單元格
以上即是tableview dataSource最基本的,也是必須實現的三個代理,經過這三個代理能夠展示一個最基本的tableview緩存

delegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath

點擊單元格回調方法框架

UITableViewCell以及重用

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
    if (cell == nil) {
        UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
    }
    return cell;
}

以上是tableview實現重用的基本寫法,經過建立一個帶有重用標識符的cell,tableview每次經過代理獲取cell時,都會先從重用池中獲取,節省內存消耗。 以上是tableview最基礎的幾個代理方法,下面經過代碼實現這幾個代理方法,一窺tableview內在工做的原理ide

實現一個橫向的tableview:MinScrollMenu

ps:源代碼已上傳至github:MinScrollMenu佈局

introduce.gifatom

1 定義代理spa

- (NSInteger)numberOfMenuCount:(MinScrollMenu *)menu;
- (CGFloat)scrollMenu:(MinScrollMenu*)menu widthForItemAtIndex:(NSInteger)index;
- (MinScrollMenuItem *)scrollMenu:(MinScrollMenu*)menu itemAtIndex:(NSInteger)index;
- (void)scrollMenu:(MinScrollMenu*)menu didSelectedItem: (MinScrollMenuItem *)item atIndex: (NSInteger)index;

模仿以前介紹的四個代理方法。代理

2 佈局

建立一個繼承UIView的子類,命名爲MinScrollMenu。
(1)添加一個scrollView屬性,初始化加到MinScrollMenu上,frame大小和父視圖同樣。正如系統的UITableView同樣,咱們也使用scrollView來實現功能

@property (nonatomic, strong) UIScrollView *scrollView;/*!< 橫向滾動的scrollView */

(2)添加一個繼承自UIView的屬性,命名爲contentView,初始化加到以前建立好的scrollView上。frame能夠先不設置,這個view主要用來裝載未來要顯示的單元格,frame大小須要之後計算。

@property (nonatomic, strong) UIView *contentView;/*!< 裝載item的view */

(3)如下幾個屬性主要用來緩存單元格數據源的數據

@property (nonatomic, strong) NSMutableArray *visibleItems;/*!< 屏幕範圍內的item數組 */
@property (nonatomic, strong) NSMutableSet *reuseableItems;/*!< 重用池 */
@property (nonatomic, strong) NSMutableDictionary *infoDict;/*!< 緩存item被選中信息 */
@property (nonatomic, strong) NSMutableDictionary *frameDict;/*!< 緩存item的frame */

3 處理數據源數據

(1) 根據代理獲取item個數

if (self.delegate != nil && [self.delegate respondsToSelector:@selector(numberOfMenuCount:)]) {
        _count = [self.delegate numberOfMenuCount:self];
    }

(2) 循環建立單元格item,由於是橫向滾動的,因此主要獲取寬度和改變x軸的值計算frame。計算出全部的item的frame並裝在字典中緩存,而後就能夠得出以前沒有設置的contentView的frame了,貼出代碼:

for (NSInteger i = 0; i < _count; ++i) {
        //獲取item的寬度
        width = [self itemWidthWithIndex:i];
        CGRect itemFrame = CGRectMake(x, y, width, height);

        // 超過屏幕可顯示範圍不加入到visibleItems數組
        CGFloat maxX = CGRectGetMaxX(itemFrame);
        CGFloat overItemWidth = width*3;
        if (i < _count-3) {
            overItemWidth = width + [self itemWidthWithIndex:i+1] + [self itemWidthWithIndex:i+2];
        }
        isOverScreenWidth = maxX > ScreenWidth + overItemWidth;
        if (!isOverScreenWidth) {
            // 獲取item,設置Frame, 添加到contentView上
            MinScrollMenuItem *item = [self itemWithIndex:i];
            if (item) {
                item.frame = itemFrame;
                [_contentView addSubview:item];

                // 添加點擊手勢
                UITapGestureRecognizer *tapGst = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapItem:)];
                [item addGestureRecognizer:tapGst];

                item.tag = ITEMTAG + i;

                // 加入到visibleItems數組
                [_visibleItems addObject:item];
            }
        }

        // 緩存數據
        [_frameDict setObject:@(i) forKey:NSStringFromCGRect(itemFrame)];
        [_infoDict setObject:@(NO) forKey:@(i)];

        // 計算scrollView的contentSize
        scrollContentWidth = maxX;

        x += width;
    }

    _scrollView.contentSize = CGSizeMake(scrollContentWidth, height);
    _contentView.frame = CGRectMake(0, 0, scrollContentWidth, height);

完成以上代碼,運行一下。就能夠看見item顯示了,可是滾動處理尚未完成,因此手指拖動scrollView右邊區域仍是空白一片,接下來就是核心的滾動處理和重用機制的實現
(3) 重用和滾動處理
重用和滾動處理是同時進行的,當tableView向右滾動時,若是最左邊的item已經離開屏幕範圍,那麼就能夠將它放進重用池中存儲,同時也要根據item的標識符從重用池裏取出item,設置frame,添加到visibleItems數組。如此就能夠循環使用幾個item來展示n個item的內容了。
實現UIScrollView代理方法

- (void)scrollViewDidScroll:(UIScrollView *)scrollView

經過這個方法能夠獲取當前scrollView移動的位移contentOffset
具體思路以下圖所示:

滾動.png

重用機制代碼:
內部查找重用的item:

NSSet *tempSet = [_reuseableItems filteredSetUsingPredicate:[NSPredicate predicateWithFormat:@"reuseIdentifer == %@", reuseItem.reuseIdentifer]];
    // 查詢複用池中有沒有相同複用符的item
    if (![tempSet isSubsetOfSet:_reuseableItems] || tempSet.count == 0) {
        // 沒有則添加item到複用池中
        [_reuseableItems addObject:reuseItem];
    }

公開API實現的代碼:

- (MinScrollMenuItem *)dequeueItemWithIdentifer:(NSString *)identifer {
    NSSet *tempSet = [_reuseableItems filteredSetUsingPredicate:[NSPredicate predicateWithFormat:@"reuseIdentifer == %@", identifer]];
    MinScrollMenuItem *item = tempSet.anyObject;
    return item;
}

也能夠不用謂詞查詢,直接使用循環查找,由於重用池每種標識符item通常只須要一個就足夠了。因此Set元素個數比較少。
(4)數據刷新reloadData方法實現
思路以下圖所示;

數據刷新.png

(5)點擊item回調響應方法實現:
Menu內的實現:
首先,將遍歷以前保存選中狀態的字典,若是value是YES,則修改成NO
第二,遍歷屏幕顯示item數組visibleItems,將item的isSelected屬性設爲NO
第三,將選中的item狀態改成選中,經過tag值獲取index索引,保存到緩存字典中。
第四,回調代理方法,通知控制器
貼上具體代碼:

- (void)tapItem: (UITapGestureRecognizer *)tapGst {
    [_infoDict enumerateKeysAndObjectsUsingBlock:^(NSNumber *key, NSNumber *obj, BOOL * _Nonnull stop) {
        *stop = obj.boolValue;
        if (*stop) {
            _infoDict[key] = @(NO);
        }
    }];
    for (MinScrollMenuItem *item in _visibleItems) {
        item.isSelected = NO;
        [_infoDict setObject:@(NO) forKey:@(item.tag-ITEMTAG)];
    }

    if ([tapGst.view isKindOfClass:[UIView class]]) {

        UIView *tempView = tapGst.view;
        MinScrollMenuItem *item = (MinScrollMenuItem *)tempView;

        if ([item isKindOfClass:[MinScrollMenuItem class]]) {
            item.isSelected = YES;
            [_infoDict setObject:@(YES) forKey:@(item.tag-ITEMTAG)];
            if (self.delegate && [self.delegate respondsToSelector:@selector(scrollMenu:didSelectedItem:atIndex:)]) {
                [self.delegate scrollMenu:self didSelectedItem:item atIndex:item.tag - ITEMTAG];
            }
        }
    }
}

Item內的實現:
首先,item添加一個選中狀態的CALayer類做爲屬性,建立好添加到item的layer上,不要忘記了設置爲隱藏,hidden=YES。再提供一個對外開放的BOOL值isSelected屬性。
第二,重寫isSelected屬性set方法,被選中時修改layer的hidden爲NO便可。

後記:tableview的四個基本代理方法已經都實現了。經過這個橫向滾動的類tableview控件,對tableview的工做原理有了更深一層的認識,固然tableview還有不少功能沒有實現,可是基本框架完成了,一些功能性的東西后面陸續能夠添加。你們能夠經過github地址:MinScrollMenu 下載源碼查看,不嫌棄的點個星吧:)

相關文章
相關標籤/搜索