有时我们会需要用到类似QQ好友列表那样可展开的列表,无奈iOS并没有像Android那样给我们提供。只能自己实现了。其实自己封装也好,自由度高,更灵活。
感谢@TinyQ的TQMultiStateTableView为我提供了一个很好地思路。本文正是借鉴他的作品并做了一些自己的改动,例如实现了非互斥功能,即可以同时点开多个子列表。另外,这里实现的是二级列表,即只能展开一层,他的源代码中是展开两层,通过学习他的代码,我想展开几层都能实现了。不过现在由于我的应用中只需要展开一层,所以节省了不少代码
效果如下:
废话不多说,直接上代码
一、思路
第一级列表是列表的header,即有很多个section,每个section都有一个header,初始状态下隐藏各个section里的row,同时为header添加点击手势,当点击时判断点击section是否已经展开,如果展开了,就关闭之,否则将其打开。而为了实现非互斥的效果,我们用一个数组来记录已经展开的section。
二、头文件:
#import <UIKit/UIKit.h>
@protocol MultistageTableViewDataSource , MultistageTableViewDelegate;
@interface MultistageTableView : UIView <UITableViewDataSource, UITableViewDelegate>
/**
* 主表格
*/
@property (strong, nonatomic) UITableView *tableView;
/**
* 当前展开的所有cell的indexPath的数组
*/
@property (strong, nonatomic) NSMutableArray *currentOpenedIndexPaths;
/**
* 数据源
*/
@property (weak, nonatomic) id<MultistageTableViewDataSource> dataSource;
/**
* 协议
*/
@property (weak, nonatomic)id<MultistageTableViewDelegate> delegate;
/**
* 根据标识符取出重用cell
*
* @param identifier 重用标识符
*
* @return 可重用的cell,或者nil(如果没有可重用的)
*/
- (id)dequeueReusableCellWithIdentifier:(NSString *)identifier;
- (id)dequeueReusableCellWithIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath;
- (id)dequeueReusableHeaderFooterViewWithIdentifier:(NSString *)identifier;
/**
* 取消对cell的选中状态
*
* @param indexPath 选中的cell的indexPath
* @param animated 是否使用动画
*/
- (void)deselectRowAtIndexPath:(NSIndexPath *)indexPath animated:(BOOL)animated;
/**
* 重新加载数据
*/
- (void)reloadData;
@end
@protocol MultistageTableViewDataSource <NSObject>
@required
- (NSInteger)m_tableView:(MultistageTableView *)mtableView numberOfRowsInSection:(NSInteger)section;
- (UITableViewCell *)m_tableView:(MultistageTableView *)mtableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
@optional
- (NSInteger)numberOfSectionsInMTableView:(MultistageTableView *)mtableView;
@end
@protocol MultistageTableViewDelegate <NSObject>
@optional
- (CGFloat)m_tableView:(MultistageTableView *)mtableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
- (CGFloat)m_tableView:(MultistageTableView *)mtableView heightForHeaderInSection:(NSInteger)section;
- (UIView *)m_tableView:(MultistageTableView *)mtableView viewForHeaderInSection:(NSInteger)section;
- (void)m_TableView:(MultistageTableView *)mTableView willOpenHeaderAtSection:(NSInteger)section;
- (void)m_TableView:(MultistageTableView *)mTableView willCloseHeaderAtSection:(NSInteger)section;
- (void)m_TableView:(MultistageTableView *)mTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
- (UITableViewCellEditingStyle)m_tableView:(MultistageTableView *)mtableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath;
- (void)m_tableView:(MultistageTableView *)mtableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath;
@end
首先解释一点,看到这么“复制”来的UITableView的代码(实际上这还不全,随着以后的使用,这个还必定会继续扩充UITableView的方法),大家一定认为这是个low设计——直接继承UITableView不就行了?其实这样是有原因的。我试着继承UITableView写过,但是很可惜失败了,因为数据源需要“双方提供”,即该控件需要提供,同时需要客户端提供。因此此处我们用组合而不用继承。理解了这点后头文件就没有什么可说的了。
三、关键代码
1、获得展开的行,这里先根据数据源获得section的数量,然后触发代理,将当前要展开的section加入到记录数组中,将该行的下标记录下来并返回。
/**
* 展开一个header所新增加的行
*
* @param section 待展开的一组数据所在的section
*
* @return 该section内所有indexPath信息
*/
- (NSMutableArray *)indexPathsForOpenHeaderInSection:(NSInteger)section {
NSMutableArray *indexPaths = [NSMutableArray array];
//询问数据源行数
NSInteger rowCount = [self get_numberOfRowsInSection:section];
//调用代理
if ([self.delegate respondsToSelector:@selector(m_TableView:willOpenHeaderAtSection:)]) {
[self.delegate m_TableView:self willOpenHeaderAtSection:section];
}
//打开了第section个子列表
[self.currentOpenedIndexPaths addObject:[NSIndexPath indexPathForRow:-1 inSection:section]];
//在当期列表中添加rowCount行数据
for (int i = 0; i < rowCount; i++) {
[indexPaths addObject:[NSIndexPath indexPathForRow:i inSection:section]];
}
return indexPaths;
}
2、获得要关闭的行,与上类似
- (NSMutableArray *)indexPathsForCloseHeaderInSection:(NSInteger)section {
NSMutableArray *indexPaths = [NSMutableArray array];
//询问数据源行数
NSInteger rowCount = [self get_numberOfRowsInSection:section];
//调用代理
if ([self.delegate respondsToSelector:@selector(m_TableView:willCloseHeaderAtSection:)]) {
[self.delegate m_TableView:self willCloseHeaderAtSection:section];
}
//关闭第section个子列表
[self.currentOpenedIndexPaths removeObject:[NSIndexPath indexPathForRow:-1 inSection:section]];
for (int i = 0; i < rowCount; i++) {
[indexPaths addObject:[NSIndexPath indexPathForRow:i inSection:section]];
}
return indexPaths;
}
3、展开或关闭某section,判断是展开还是关闭,然后调用UITableView的相关方法更新视图
- (void)openOrCloseHeaderWithSection:(NSInteger)section {
NSMutableArray *openedIndexPaths = [NSMutableArray array];
NSMutableArray *deleteIndexPaths = [NSMutableArray array];
//如果当前没有任何子列表被打开
if (self.currentOpenedIndexPaths.count == 0) {
openedIndexPaths = [self indexPathsForOpenHeaderInSection:section];
} else {
BOOL found = NO;
for (NSIndexPath *ip in self.currentOpenedIndexPaths) {
//如果是关闭当前已经打开的子列表
if (ip.section == section) {
found = YES;
deleteIndexPaths = [self indexPathsForCloseHeaderInSection:section];
break;
}
}
//打开新的子列表
if (!found) {
openedIndexPaths = [self indexPathsForOpenHeaderInSection:section];
}
}
[self.tableView beginUpdates];
if (openedIndexPaths.count > 0) {
[self.tableView insertRowsAtIndexPaths:openedIndexPaths withRowAnimation:UITableViewRowAnimationAutomatic];
}
if (deleteIndexPaths.count > 0) {
[self.tableView deleteRowsAtIndexPaths:deleteIndexPaths withRowAnimation:UITableViewRowAnimationAutomatic];
}
[self.tableView endUpdates];
}
而tap的触发方法就是调用上述打开或关闭列表的方法。这其中,连接所有方法的section值可以通过在tableview的协议中为header的tag赋值得到。
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
UIView *header = [self get_viewForHeaderInSection:section];
if (header) {
CGFloat height = [self tableView:tableView heightForHeaderInSection:section];
header.frame = CGRectMake(0, 0, self.tableView.frame.size.width, height);
header.tag = section;
[self addTapGestureRecognizerAction:@selector(tableViewHeaderTouchUpInside:) toView:header];
}
return header;
}
其余的都比较简单,就是一些初始化或者对tableview的方法的处理调用。这里就不一一列举了。可以参考上传的代码。