UITableview有两个相关代理UITableViewDelegate、UITableViewDataSource
dataSource是数据源代理,delegate则是相关操做代理git
- (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缓存
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
点击单元格回调方法框架
- (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
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 下载源码查看,不嫌弃的点个星吧:)