序言
最近项目中要用到类似百度外卖app筛选器,于是自己动手去实现。本文将详尽的利用DataSource和Delegate设计模式去实现(OC)。但由于笔者知识水平有限,没有考虑内存优化的问题,一些代码规范和设计思维存在错误,恳请前辈们批评指正。
效果图
设计思路
在布局方面采用的是Masonry,也有用到YYKit的一些方法,一些宏编译的色值可替换,这里就不给出具体的色值。先来看看FilterViewDelegate
和FilterViewDataSource
@protocol FilterViewDelegate
@optional
- (void)filterView:(FilterView *)filterView willDisplayMenuFromItemView:(UIView *)itemView forIndex:(NSInteger)index;
- (void)filterView:(FilterView *)filterView didDisplayMenuFromItemView:(UIView *)itemView forIndex:(NSInteger)index;
@required
- (void)filterView:(FilterView *)filterView didSelectIndex:(NSInteger)index inItemIndx:(NSInteger)itemIndex;
@end
其中方法:
- (void)filterView:(FilterView *)filterView willDisplayMenuFromItemView:(UIView *)itemView forIndex:(NSInteger)index;
- (void)filterView:(FilterView *)filterView didDisplayMenuFromItemView:(UIView *)itemView forIndex:(NSInteger)index;
分别为将要显示筛选菜单和菜单显示后的代理,可不必具体实现。
- (void)filterView:(FilterView *)filterView didSelectIndex:(NSInteger)index inItemIndx:(NSInteger)itemIndex;
这个方法表示从哪一个Item中选择了具体的哪一行,这个方法必须具体实现。
@protocol FilterViewDataSource
@required
- (NSInteger)numberOfItemInFilterView:(FilterView *)filterView;
- (NSString *)filterView:(FilterView *)filterView titleForItem:(NSInteger)item;
- (NSInteger)filterView:(FilterView *)filterView numberOfRowsForItem:(NSInteger)item;
- (NSString *)filterView:(FilterView *)filterView titleForRows:(NSInteger)row item:(NSInteger)item;
@end
其中FilterViewDataSource中的每个办法必须实现,下面是几个方法的介绍:
- (NSInteger)numberOfItemInFilterView:(FilterView *)filterView;
该方法表示有多少个Item,即有多少个筛选项,如效果图就有4个帅选项。
- (NSString *)filterView:(FilterView *)filterView titleForItem:(NSInteger)item;
每个筛选项的标题
- (NSInteger)filterView:(FilterView *)filterView numberOfRowsForItem:(NSInteger)item;
每个筛选项下的筛选菜单的行数
- (NSString *)filterView:(FilterView *)filterView titleForRows:(NSInteger)row item:(NSInteger)item;
每个筛选项下的筛选菜单的标题
具体实现
在FilterView.h
定义如下:
@interface FilterView : UIView
@property(nonatomic,weak) id delegate;
@property(nonatomic,weak) id dataSource;
- (void)hideMenu;
@end
然后在FilterView.m
中定义一些数据如下:
@interface FilterView ()
//标题数组
@property (nonatomic,strong) NSMutableArray *itemTitleArray;
//标题下帅选项数组,二维数组
@property (nonatomic,strong) NSMutableArray *dataArray;
//帅选列表
@property (nonatomic,weak) UITableView *tableView;
@end
@implementation FilterView {
//菜单数目
NSInteger itemCount;
UIWindow *window;
//当前选中的ITEM下标
NSInteger currentSelectItemIndex;
//当前选中的菜单项
UIView *currentSeleectItemView;
//是否正在显示帅选菜单
BOOL isShowMenu;
//遮罩层
UIView *maskView;
}
重写UIView
的初始化方法,主要是初始化定义的一些数据:
- (instancetype)init {
self = [super init];
if (self) {
window = [[UIApplication sharedApplication] keyWindow];
self.itemTitleArray = [[NSMutableArray alloc] initWithCapacity:0];
self.dataArray = [[NSMutableArray alloc] initWithCapacity:0];
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
window = [[UIApplication sharedApplication] keyWindow];
self.itemTitleArray = [[NSMutableArray alloc] initWithCapacity:0];
self.dataArray = [[NSMutableArray alloc] initWithCapacity:0];
}
return self;
}
然后通过setter
的方式将具体实现的数据源保存起来,具体代码如下:
- (void)setDataSource:(id)dataSource {
_dataSource = dataSource;
if ([_dataSource respondsToSelector:@selector(numberOfItemInFilterView:)]) {
//有多少个item
itemCount = [_dataSource numberOfItemInFilterView:self];
}
if ([_dataSource respondsToSelector:@selector(filterView:titleForItem:)]) {
for (int i = 0; i < itemCount; i ++) {
//item的标题
NSString *title = [_dataSource filterView:self titleForItem:i];
NSLog(@"title:%@",title);
[_itemTitleArray addObject:title];
}
}
if ([_dataSource respondsToSelector:@selector(filterView:numberOfRowsForItem:)]) {
for (int i = 0; i < itemCount; i ++) {
//每个item下的帅选行数
NSInteger rows = [_dataSource filterView:self numberOfRowsForItem:i];
NSMutableArray *titleArray = [[NSMutableArray alloc] initWithCapacity:rows];
//先用""默认填空
for (int row = 0; row < rows; row ++) {
[titleArray addObject:@""];
}
[_dataArray addObject:titleArray];
}
}
if ([_dataSource respondsToSelector:@selector(filterView:titleForRows:item:)]) {
for (int i = 0; i < itemCount; i ++) {
NSMutableArray *titleArray = [_dataArray[i] mutableCopy];
for (int k = 0; k < titleArray.count; k ++) {
//替换之前填空的数据
_dataArray[i][k] = [_dataSource filterView:self titleForRows:k item:i];
}
}
}
//保存数据再初始化视图
[self setupView];
}
初始化视图前,用- (UIView *)createItemView:(NSString *)title
方法,通过传入菜单标题,即可创建itemView,即一个菜单项。方法具体实现为:
- (UIView *)createItemView:(NSString *)title {
UIView *itemView = [[UIView alloc] init];
itemView.backgroundColor = [UIColor whiteColor];
UILabel *itemTitleLabel = [[UILabel alloc] init];
[itemView addSubview:itemTitleLabel];
itemTitleLabel.font = [UIFont systemFontOfSize:11];
itemTitleLabel.textColor = MainThemeColor;
itemTitleLabel.text = title;
[itemTitleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.height.equalTo(itemView);
make.centerX.equalTo(itemView).offset(-8);
make.left.greaterThanOrEqualTo(itemView);
}];
UIButton *arrowButton = [UIButton buttonWithType:UIButtonTypeCustom];
[itemView addSubview:arrowButton];
//为了防止点到箭头按钮
arrowButton.userInteractionEnabled = NO;
UIImage *normalImage = [UIImage imageNamed:@"arrow_normal"];
UIImage *selectedImage = [UIImage imageNamed:@"arrow_selected"];
[arrowButton setBackgroundImage:normalImage forState:UIControlStateNormal];
[arrowButton setBackgroundImage:selectedImage forState:UIControlStateSelected];
//给箭头按钮设置一个tag
arrowButton.tag = ArrowButtonTag;
[arrowButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(itemView);
make.left.equalTo(itemTitleLabel.mas_right).offset(0);
make.right.lessThanOrEqualTo(itemView);
make.size.mas_equalTo(normalImage.size);
}];
//设置菜单栏的tag
itemView.tag = [_itemTitleArray indexOfObject:title];
//设置圆角属性
itemView.layer.masksToBounds = YES;
itemView.layer.cornerRadius = 8.0f;
return itemView;
}
好了,准备工作就绪,现在开始初始化视图创建菜单栏:
//布局参照物
UIView *leftView = self;
for (int i = 0; i < itemCount ; i ++) {
UIView *itemView = [self createItemView:_itemTitleArray[i]];
[self addSubview:itemView];
[itemView mas_makeConstraints:^(MASConstraintMaker *make) {
if (i) {
make.width.equalTo(leftView);
make.left.equalTo(leftView.mas_right).offset(8);
}else {
make.left.equalTo(leftView).offset(2);
}
make.top.height.bottom.equalTo(self);
make.height.equalTo(@30);
if (i == itemCount - 1) {
make.right.equalTo(self).offset(-2);
}
}];
leftView = itemView;
}
因为在显示筛选菜单的时候需要一个遮罩层,所以加上遮罩层初始化的代码:
//初始化遮罩层
maskView = [[UIView alloc] initWithFrame:window.bounds];
//先隐藏,显示筛选菜单的时候再显示
maskView.hidden = YES;
[window addSubview:maskView];
初始化显示筛选数据用的tableView,具体代码如下:
//初始化筛选菜单的tableView
UITableView *tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
_tableView = tableView;
_tableView.dataSource = self;
_tableView.delegate = self;
_tableView.tableFooterView = [UIView new];
[_tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:cellID];
[window addSubview:_tableView];
//先设置左右约束的布局,显示的时候再跟新顶部约束调整布局
[_tableView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.equalTo(window);
}];
实现UITableView
的UITableViewDataSource
#pragma mark - UITableViewDelegate,UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
NSArray *rows = _dataArray[currentSelectItemIndex];
return rows.count;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 30;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellID forIndexPath:indexPath];
cell.textLabel.text = _dataArray[currentSelectItemIndex][indexPath.row];
cell.textLabel.font = [UIFont systemFontOfSize:12];
return cell;
}
至此,视图的初始化工作完成。下面来实现显示和消失的动画:
****显示动画****
- (void)showMenuForm:(UIView *)itemView {
//记录当前显示的菜单栏下标
currentSelectItemIndex = itemView.tag;
//刷新数据
[_tableView reloadData];
//更新tableview的顶部约束
[_tableView mas_updateConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(window).offset(self.frame.origin.y + 30 + 64 + 1);
make.height.equalTo(@(_tableView.contentSize.height));
}];
//显示遮罩层,同时加上手势点击事件
maskView.hidden = NO;
UITapGestureRecognizer *tapMaskView = [[UITapGestureRecognizer alloc] initWithActionBlock:^(id _Nonnull sender)
{
//消失动画
[self hideMenu];
}];
maskView.userInteractionEnabled = YES;
maskView.backgroundColor = MainDimBackgroundColor;
[maskView addGestureRecognizer:tapMaskView];
[maskView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(_tableView.mas_bottom);
make.left.right.bottom.equalTo(window);
}];
//具体的动画实现
CGRect frame = _tableView.frame;
_tableView.frame = CGRectMake(frame.origin.x, frame.origin.y, frame.size.width, 0);
[UIView animateWithDuration:0.2 animations:^{
_tableView.frame = CGRectMake(frame.origin.x, frame.origin.y, frame.size.width, _tableView.contentSize.height);
maskView.alpha = 1.0;
} completion:^(BOOL finished) {
}
}];
}
这里需要注意的是,在自身父视图中完成布局后,才得到self.frame.origin.y
,然后才在window
得到_tableView
的顶部偏移值,就暂时这样解决。如果有更好的方法,欢迎一起探讨。
添加手势时,选择了YYKit中对手势扩展的方法,通过block
的形式,方便直观理解。
****消失动画****
- (void)hideMenu {
//改变菜单箭头状态
UIButton *arrowButton = [currentSeleectItemView viewWithTag:ArrowButtonTag];
arrowButton.selected = NO;
CGRect frame = _tableView.frame;
[UIView animateWithDuration:0.2 animations:^{
_tableView.frame = CGRectMake(frame.origin.x, frame.origin.y, frame.size.width, 0);
maskView.alpha = 0.f;
} completion:^(BOOL finished) {
}];
}
因为这里消失动画有很多个入口,所以消失前获取菜单栏的按钮,设置选定值为NO
动画设计好之后,最重要的一步,就是给每一个菜单栏添加点击手势,从而实现箭头的变化和菜单选项的显示和消失动画,下面是实现代码:
itemView.userInteractionEnabled = YES;
UITapGestureRecognizer *tapItemView = [[UITapGestureRecognizer alloc] initWithActionBlock:^(id _Nonnull sender) {
UIView *tempItemView = ((UITapGestureRecognizer *)sender).view;
UIButton *arrowButton = [tempItemView viewWithTag:ArrowButtonTag];
arrowButton.selected = !arrowButton.selected;
if (arrowButton.selected) {
if (isShowMenu) {
//改变上一菜单的箭头状态
UIButton *lastArrowButton = [currentSeleectItemView viewWithTag:ArrowButtonTag];
lastArrowButton.selected = NO;
[self hideMenu];
}
if (isShowMenu) {
NSLog(@"菜单正在显示,先隐藏后再显示");
}else {
NSLog(@"菜单没在显示,直接显示");
}
//这里需要延迟执行,不然会出现隐藏后不显示的问题
[self performSelector:@selector(showMenuForm:) withObject:tempItemView afterDelay:0.1];
isShowMenu = YES;
}else {
NSLog(@"菜单正在显示,直接隐藏");
[self hideMenu];
isShowMenu = NO;
}
currentSeleectItemView = tempItemView;
}];
[itemView addGestureRecognizer:tapItemView];
上面的代码可在创建itemView
之后添加。需要特别注意的是,当前菜单选项正在显示的时候,点击另外一个菜单栏继续显示的时候,我是先调用消失的动画再显示,若不添加延迟显示的方法,会显示不出来。
最后实现FilterViewDelegate
,在动画显示前添加下列代码:
if ([self.delegate respondsToSelector:@selector(filterView:willDisplayMenuFromItemView:forIndex:)]) {
[self.delegate filterView:self willDisplayMenuFromItemView:itemView forIndex:currentSelectItemIndex];
}
显示动画执行完毕后:
if (finished) {
if ([self.delegate respondsToSelector:@selector(filterView:didDisplayMenuFromItemView:forIndex:)]) {
[self.delegate filterView:self willDisplayMenuFromItemView:itemView forIndex:currentSelectItemIndex];
}
}
在UITableViewDelegate
的- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
实现我们的代理方法:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if ([self.delegate respondsToSelector:@selector(filterView:didSelectIndex:inItemIndx:)]) {
[self.delegate filterView:self didSelectIndex:indexPath.row inItemIndx:currentSelectItemIndex];
}
}
具体用法示列
在某个ViewController
中继承FilterViewDelegate
和FilterViewDataSource
itemArray = @[@"酒款品种",@"出产年份",@"陈酿年份",@"品饮分数"];
dataArray = @[@[@"酒款品种1",@"酒款品种2",@"酒款品种3",@"酒款品种4"],@[@"出产年份1",@"出产年份2",@"出产年份3"],@[@"酿年份1",@"酿年份2"],@[@"品饮分数1",@"品饮分数2",@"品饮分数3",@"品饮分数4",@"品饮分数5"]];
FilterView *filterView = [[FilterView alloc] initWithFrame:CGRectZero];
filterView.dataSource = self;
filterView.delegate = self;
[self.view addSubview:filterView];
[filterView mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.width.equalTo(self.view);
make.top.equalTo(self.view).offset(2);
}];
#pragma mark - FilterViewDelegate,FilterViewDataSource
- (NSInteger)numberOfItemInFilterView:(FilterView *)filterView {
return itemArray.count;
}
- (NSString *)filterView:(FilterView *)tableView titleForItem:(NSInteger)item {
return itemArray[item];
}
- (NSInteger)filterView:(FilterView *)filterView numberOfRowsForItem:(NSInteger)item {
return ((NSArray *)dataArray[item]).count;
}
- (NSString *)filterView:(FilterView *)filterView titleForRows:(NSInteger)row item:(NSInteger)item {
return dataArray[item][row];
}
- (void)filterView:(FilterView *)filterView willDisplayMenuFromItemView:(UIView *)itemView forIndex:(NSInteger)index
{
NSLog(@"willDisplayMenu");
}
- (void)filterView:(FilterView *)filterView didDisplayMenuFromItemView:(UIView *)itemView forIndex:(NSInteger)index
{
NSLog(@"didDisplayMenu");
}
- (void)filterView:(FilterView *)filterView didSelectIndex:(NSInteger)index inItemIndx:(NSInteger)itemIndex {
NSLog(@"didSelectIndex");
[filterView hideMenu];
NSLog(@"reslut--%@",[NSString stringWithFormat:@"筛选结果:%@,%@",itemArray[itemIndex],dataArray[itemIndex][index]]);
}
结语
由于初次写文章,表达能力有限,不到之处敬请谅解。笔者是准大四的学生,欢迎志同道合人士一同探讨,深入学习。文章出现纰漏之处在所难免,恳请前辈们批评指正,不胜感激。