前言
这几天刚刚给学生讲到了表格视图的使用,有学生问我,如果要实现类似于腾讯好友列表展开收缩的效果该怎么实现呢?以前我写过有关这样效果的demo,但是一直没有发表成文,今天刚好有时间,于是写出来,分享给各位,当然要实现这种效果方法还是有很多,这里就讲解下我的实现方式,不到之处,还往各位提点建议,我会及时改进。
效果
为了简化数据,所以每个分组里面的好友数据都是一样的。
实现思路
1、用一个字典来判断,将section作为key,如果字典对应key有值,则展开组,反之则关闭组。
2、自定义头部视图,并且给头部视图添加点击事件(UITapGestureRecognizer
)监测用户点击分组。
3、在主页为表视图创建头部视图的时候,为其设置tag值,其值为当前所在的组(section),并且在自定义头部视图类中,通过委托模式,将其tag值传递到主页,为其获取到对应头部视图的section
,设为字典key。
4、涉及技术:委托模式、自定义单元格、自定义头部视图、plist文件读取、点击手势以及协议传值。
数据处理
好友列表展示的数据,我已预先存到plist文件中,包括图片素材,也一并导入了工程,如下所示:
素材下载地址:http://download.csdn.net/detail/hierarch_lee/9412991
代码实现
由于代码相对较简单,并且关键部分我也详细注明了代码解释,因此,就不做具体的讲解。
涉及文件
1、LHY_HomeViewController:主页,继承于UIViewController,用于展示好友列表。
2、LHY_FriendsListCell:自定义单元格,继承于UITableViewCell,用于布局好友列表信息;
3、LHY_HeaderView:自定义头部视图,继承于UITableViewHeaderFooterView,用于布局分组显示数据;
代码块1:为主页添导航栏,这里我是手动创建UIWindow,首先在Info.plist将Main字段删除,然后在AppDelegate.m文件中,导入LHY_HomeViewController,添加如下代码:
- AppDelegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
LHY_HomeViewController *lhyVc = [[LHY_HomeViewController alloc] init];
UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:lhyVc];
_window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
_window.backgroundColor = [UIColor blackColor];
_window.rootViewController = nav;
[_window makeKeyAndVisible];
return YES;
}
代码块2:自定义单元格,LHY_FriendsListCell,主要布局好友信息包括头像、昵称以及签名,具体实现如下:
- LHY_FriendsListCell.h
#import <UIKit/UIKit.h>
@interface LHY_FriendsListCell : UITableViewCell
@property (strong, nonatomic) UIImageView *headPortraitImageView; /**< 头像 */
@property (strong, nonatomic) UILabel *nicknameLabel; /**< 昵称 */
@property (strong, nonatomic) UILabel *signatureLabel; /**< 签名 */
@end
- LHY_FriendsListCell.m
#import "LHY_FriendsListCell.h"
@implementation LHY_FriendsListCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
// 加载视图
[self.contentView addSubview:self.headPortraitImageView];
[self.contentView addSubview:self.nicknameLabel];
[self.contentView addSubview:self.signatureLabel];
}
return self;
}
#pragma mark *** Getters ***
- (UIImageView *)headPortraitImageView {
if (!_headPortraitImageView) {
_headPortraitImageView = [[UIImageView alloc] init];
_headPortraitImageView.bounds = CGRectMake(0, 0, 60, 60);
_headPortraitImageView.center = CGPointMake(40, 40);
// 设置文本现实模式
_headPortraitImageView.contentMode = UIViewContentModeScaleAspectFit;
// 设置圆角
_headPortraitImageView.layer.cornerRadius = 30;
// 设置图层超出父视图部分不显示
_headPortraitImageView.layer.masksToBounds = YES;
}
return _headPortraitImageView;
}
- (UILabel *)nicknameLabel {
if (!_nicknameLabel) {
_nicknameLabel = [[UILabel alloc] initWithFrame:CGRectMake(80, 5, 310, 30)];
_nicknameLabel.font = [UIFont boldSystemFontOfSize:18];
_nicknameLabel.textColor = [UIColor redColor];
}
return _nicknameLabel;
}
- (UILabel *)signatureLabel {
if (!_signatureLabel) {
_signatureLabel = [[UILabel alloc] initWithFrame:CGRectMake(80, 50, 310, 20)];
_signatureLabel.font = [UIFont systemFontOfSize:15];
}
return _signatureLabel;
}
@end
代码块3:自定义头部视图,LHY_HeaderView,本类主要布局组标题以及指示(展开/关闭)控件,其次,通过添加点击手势结合委托模式,传递tag值。
- LHY_HeaderView.h
#import <UIKit/UIKit.h>
@class LHY_HeaderView;
// 协议声明
@protocol LHY_HeaderViewDelegate <NSObject>
- (void)lhyHeaderView:(LHY_HeaderView *)lhyHeaderView didSelectedWithTag:(NSInteger)tag;
@end
@interface LHY_HeaderView : UITableViewHeaderFooterView
@property (nonatomic, strong) UILabel *sectionTitleLabel; /**< 组标题 */
@property (nonatomic, strong) UIImageView *indictorImageView; /**< 指示图片 */
@property (nonatomic, weak) id <LHY_HeaderViewDelegate> delegate; /**< 代理 */
@end
- LHY_HeaderView.m
#import "LHY_HeaderView.h"
@implementation LHY_HeaderView
- (instancetype)initWithReuseIdentifier:(nullable NSString *)reuseIdentifier {
if (self = [super initWithReuseIdentifier:reuseIdentifier]) {
self.contentView.backgroundColor = [UIColor clearColor];
self.layer.borderWidth = 0.34;
self.layer.borderColor = [UIColor lightGrayColor].CGColor;
// 加载控件
[self.contentView addSubview:self.indictorImageView];
[self.contentView addSubview:self.sectionTitleLabel];
// 添加点击手势
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(respondsToTapGesture:)];
[self addGestureRecognizer:tapGesture];
}
return self;
}
#pragma mark *** Gestures ***
- (void)respondsToTapGesture:(UITapGestureRecognizer *)gesture {
// 判断代理是否存并且遵守协议,如果满足条件,则让代理执行协议方法,并且将对应组以及tag值传递给代理;
if (_delegate && [_delegate conformsToProtocol:@protocol(LHY_HeaderViewDelegate)]) {
[_delegate lhyHeaderView:self didSelectedWithTag:self.tag];
}
}
#pragma mark *** Getters ***
- (UIImageView *)indictorImageView {
if (!_indictorImageView) {
_indictorImageView = [[UIImageView alloc] init];
_indictorImageView.bounds = CGRectMake(0, 0, 20, 20);
_indictorImageView.center = CGPointMake(15, 22);
}
return _indictorImageView;
}
- (UILabel *)sectionTitleLabel {
if (!_sectionTitleLabel) {
_sectionTitleLabel = [[UILabel alloc] init];
_sectionTitleLabel.bounds = CGRectMake(0, 0, 200, 30);
_sectionTitleLabel.center = CGPointMake(CGRectGetMaxX(self.indictorImageView.frame) + CGRectGetMidX(_sectionTitleLabel.bounds) + 10, 22);
_sectionTitleLabel.font = [UIFont boldSystemFontOfSize:18];
}
return _sectionTitleLabel;
}
@end
代码块4:加载主页,LHY_HomeViewController,本类主要配置表格视图显示,以及处理组展开/关闭状态。
- LHY_HomeViewController.h
#import <UIKit/UIKit.h>
@interface LHY_HomeViewController : UIViewController
@end
- LHY_HomeViewController.m
#import "LHY_HomeViewController.h"
#import "LHY_HeaderView.h"
#import "LHY_FriendsListCell.h"
static NSString *const kLHY_ViewControllerTitle = @"好友列表";
static NSString *const kReusableCellIdentifier = @"a1b1c1";
static NSString *const kHeaderFooterViewReuseIdentifier = @"a2b2c2";
@interface LHY_HomeViewController () <UITableViewDataSource, UITableViewDelegate, LHY_HeaderViewDelegate>
{
NSArray *_sectionTitles; /**< 分组标题集合 */
NSArray *_friendsListInfo; /**< 好友信息集合 */
NSMutableDictionary *_openDict; /**< 用于设置当前分组展开或关闭 */
}
@property (nonatomic, strong) UITableView *tableView; /**< 表格视图 */
- (void)initializeDataSource; /**< 初始化数据源 */
- (void)initializeUserInterface; /**< 初始化用户界面 */
@end
@implementation LHY_HomeViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self initializeDataSource];
[self initializeUserInterface];
}
#pragma mark *** Initialize methods ***
- (void)initializeDataSource {
// 初始化分组数据
_sectionTitles = @[@"同事", @"家人", @"朋友", @"同学", @"陌生人", @"黑名单"];
// 根据plist文件获取好友列表数据
NSString *plistPath = [[NSBundle mainBundle] pathForResource:@"FriendsList" ofType:@"plist"];
_friendsListInfo = [NSArray arrayWithContentsOfFile:plistPath];
}
- (void)initializeUserInterface {
self.title = kLHY_ViewControllerTitle;
// 关闭系统自动偏移
self.automaticallyAdjustsScrollViewInsets = NO;
self.view.backgroundColor = [UIColor whiteColor];
// 加载表格视图
[self.view addSubview:self.tableView];
}
#pragma mark *** <UITableViewDataSource, UITableViewDelegate> ***
// 设置组数
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
// 返回分组数据集合的个数
return _sectionTitles.count;
}
// 设置行数
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
// 返回好友列表信息集合的个数
return _friendsListInfo.count;
}
// 设置单元格
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
LHY_FriendsListCell *cell = [tableView dequeueReusableCellWithIdentifier:kReusableCellIdentifier forIndexPath:indexPath];
// 超出父视图不显示,如果不将其值设为YES(默认为NO),那么现实在表格视图上的数据会出现重叠的bug。
cell.layer.masksToBounds = YES;
// 获取值
NSString *nickName = _friendsListInfo[indexPath.row][@"nickName"];
NSString *headPortrait = _friendsListInfo[indexPath.row][@"headPortrait"];
NSString *signature = _friendsListInfo[indexPath.row][@"context"];
// 赋值控件
cell.nicknameLabel.text = nickName;
cell.signatureLabel.text = signature;
cell.headPortraitImageView.image = [UIImage imageNamed:headPortrait];
return cell;
}
// 设置行高
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
// 根据当前组获取字典key
NSString *key = [NSString stringWithFormat:@"%ld", indexPath.section];
// 判断如果字典对应key是否有值,如果有值,说明当前组展开,返回 80 高度;否则为关闭,返回 0 高度;
if ([_openDict objectForKey:key]) {
return 80;
}else {
return 0;
}
}
// 设置组高
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
return 44;
}
// 自定义头部视图
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
// 创建头部视图
// 头部视图重用
LHY_HeaderView *lhyHeaderView = [tableView dequeueReusableHeaderFooterViewWithIdentifier:kHeaderFooterViewReuseIdentifier];
// 设置代理
lhyHeaderView.delegate = self;
// 配置视图显示
lhyHeaderView.sectionTitleLabel.text = _sectionTitles[section];
lhyHeaderView.indictorImageView.image = [UIImage imageNamed:@"flag.png"];
// 设置tag值
lhyHeaderView.tag = section;
// 根据字典对应key(section)是否有值来判断当前组应该展开还是收缩
// 如果有值,则说明应该展开组,无值则应该关闭组
NSString *key = [NSString stringWithFormat:@"%ld", section];
// 通过动画效果实现展开与关闭指示状态的显示
[UIView animateWithDuration:0.4 animations:^{
// 如果展开,则将指示图片旋转45°,箭头向下,意为展开;
// 如果关闭,则移除transform属性,回复原样,箭头向右,意为关闭;
lhyHeaderView.indictorImageView.transform = [_openDict objectForKey:key] ? CGAffineTransformMakeRotation(M_PI_2) : CGAffineTransformIdentity;
}];
return lhyHeaderView;
}
#pragma mark *** LHY_HeaderViewDelegate ***
- (void)lhyHeaderView:(LHY_HeaderView *)lhyHeaderView didSelectedWithTag:(NSInteger)tag {
// 判断字典是否存在,如果不存在,则新建;
// 如果对一个未初始化的可变字典作数据操作,将会导致奔溃;
if (!_openDict) {
_openDict = [NSMutableDictionary dictionary];
}
// 获取tag值,并将其作为字典key
NSString *key = [NSString stringWithFormat:@"%ld", tag];
// 模拟展开与关闭
// 判断当前组是否有值,有值则移除对应key-value对,无值则设置key-value对。
if (![_openDict objectForKey:key]) {
[_openDict setObject:key forKey:key];
}else{
[_openDict removeObjectForKey:key];
}
// 刷新表格视图指定组
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:tag] withRowAnimation:UITableViewRowAnimationFade];
}
#pragma mark *** Getters ***
- (UITableView *)tableView {
if (!_tableView) {
_tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 64, CGRectGetWidth(self.view.bounds), CGRectGetHeight(self.view.bounds) - 64) style:UITableViewStylePlain];
// 设置代理
_tableView.delegate = self;
// 设置数据源
_tableView.dataSource = self;
// 设置尾部视图(此设置可解决当表格视图在屏幕能完全展示数据,并且有多余空间情况下出现分割线的问题)
_tableView.tableFooterView = [[UIView alloc] init];
// 注册单元格
[_tableView registerClass:[LHY_FriendsListCell class] forCellReuseIdentifier:kReusableCellIdentifier];
// 注册头部视图
[_tableView registerClass:[LHY_HeaderView class] forHeaderFooterViewReuseIdentifier:kHeaderFooterViewReuseIdentifier];
}
return _tableView;
}
@end
到了这一步,运行工程,你将会看到上述展示的效果。如果还有朋友不太熟悉思路,欢迎加Q:894416347,填好备注信息,或留言,一起探讨。