【原】通过实现一个横向Tableview,了解UITableview工作原理

转载请注明原作者coderZ

UITableview代理方法介绍

UITableview有两个相关代理UITableViewDelegate、UITableViewDataSource
dataSource是数据源代理,delegate则是相关操作代理

dataSource

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

通过返回值,告诉tableview的某个section应该显示多少个单元格
- (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内在工作的原理

实现一个横向的tableview:MinScrollMenu

ps:源代码已上传至github:MinScrollMenu

introduce.gif
1 定义代理
- (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
下载源码查看,不嫌弃的点个星吧:)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容