iOS - 仿美团下拉列表
下拉列表菜单很早之前就打算写个仿美团的下拉列表,但因为最近一直在忙其他的事情所以一直没有着手做,上周还算有时间,于是抽了一天的时间简单封装了下,当然到目前为止实现的功能还只是只能显示一组下拉列表,如果以后有时间,这个demo我会继续更新和维护,到时候会放在这个博客里边,笔者写这个demo主要是自己学习之用,当然能够帮到对这方面有需求的用户那更好。先看下效果图:
每点击一下就要改变标题颜色,而且文字右边那个朝下的三角形也要换成对应颜色的朝上的箭头,同时还要弹出对应的下拉列表。但这里我们就应该想,要实现这样的按钮效果,可能需要自己的封装,毕竟系统的UIButton是没有这样的效果的,同时我们可以解压下美团官方的APP报,如果能找找到对应的图片那么我们的思路就八九不离十了(经过笔者查看,官方包里边确实有对应的两个三角形图片),我们看到美团的下拉出来的TableView在切换不同的列表的时候,如果拉出来的TableView高度不同,列表会有个缓慢的改变高度的过程,那么我们就需要每次切换数据源时让TableView执行一次reloadData
的操作。
代码实现
封装菜单按钮
我们创建MenuButton
类,让其继承自UIView
。(笔者本来想让其继承自UIButton的,但是想到UIButton
里边已经有UILabel
和UIImageView
控件了,为了本着简洁高效的原则,所以就没有继承自UIButton
)
在.h
里边:
/** 当按钮标题改变时,要触发`setTitle:`这个方法 */
@property (nonatomic, copy) NSString *title;
/**
* 选中某个按钮时回调方法
*
* @param button 用来标记选中的按钮
* @param index 用来标记`选中了第几个按钮`
* @param selected 用来标记`这个按钮选中状态`
*/
@property (nonatomic, copy) void (^clickMenuButton) (MenuButton *button, NSString *title, BOOL selected);
/**
* 按钮初始化方法
*
* @param frame 按钮frame
* @param title 按钮标题
* @param defImage 按钮上的默认图片
* @param selImage 按钮选中时的图片
*/
- (instancetype)initWithFrame:(CGRect)frame
title:(NSString *)title
defImage:(UIImage *)defImage
selImage:(UIImage *)selImage
经过我们的分析我只,我们需要两个控件一个UILabel
用来显示按钮的标题,一个UIImageView
用来设置标题右边那个三角形图片,在.m
里边:
@interface MenuButton ()
{
NSString *_title; // 按钮标题
UIImage *_defImg; // 按钮没有选中时右边图片
UIImage *_selImg; // 按钮选中时右边图片
}
/** 用来设置按钮标题 */
@property (nonatomic, strong) UILabel *titLabel;
/** 用来设置图片 */
@property (nonatomic, strong) UIImageView *imgView;
@end
定义按钮初始化方法:
- (instancetype)initWithFrame:(CGRect)frame
title:(NSString *)title
defImage:(UIImage *)defImage
selImage:(UIImage *)selImage
{
self = [super initWithFrame:frame];
if (self) {
_title = title;
_defImg = defImage;
_selImg = selImage;
[self setupSubViews];
}
return self;
}
- (void)setupSubViews
{
self.selected = NO;
[self addSubview:self.titLabel];
[self addSubview:self.imgView];
// self.backgroundColor = [UIColor colorWithWhite:0.960 alpha:1.000];
// 给视图添加手势,当我们点击视图是触发`menuButtonClicked`方法
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(menuButtonClicked)];
[self addGestureRecognizer:tap];
}
#pragma mark - 手势点击事件
- (void)menuButtonClicked
{
_selected = ! _selected;
if (_selected) {
self.titLabel.textColor = kSelTitleCor;
self.imgView.image = _selImg;
}
else {
self.titLabel.textColor = kDefTitleCor;
self.imgView.image = _defImg;
}
if (self.clickMenuButton) {
self.clickMenuButton(self, self.titLabel.text,_selected);
}
}
当按钮的标题改变时,我们要实现视图的setTitle:
方法:
- (void)setTitle:(NSString *)title
{
self.titLabel.text = title;
UIFont *font = [UIFont systemFontOfSize:kFontSize];
_titLabel.font = font;
// 为了不至于让标题和图片由于位置变化而太难看,我们需要在按钮标题每次改变时重新设置它的frame
CGSize size = [title sizeWithFont:font maxSize:CGSizeMake(self.w, self.h)];
_titLabel.center = CGPointMake(self.w/2 - kTriangleWH/2, self.h/2);
_titLabel.bounds = CGRectMake(0, 0, size.width, size.height);
_imgView.frame = CGRectMake(CGRectGetMaxX(self.titLabel.frame), (self.h-kTriangleWH)/2, kTriangleWH, kTriangleWH);
[self setNeedsLayout];
[self setIsSeled:NO];
}
封装下拉菜单
我们创建MenuButton
类,让其继承自UIView
,在.h
里边:
/**
* 数据源
*/
@property (nonatomic, strong) NSArray *dataSource;
/**
* 选中某个按钮时回调方法(这个回调其实可以不写)
*
* @param button 用来标记选中的按钮
* @param index 用来标记`选中了第几个按钮`
* @param selected 用来标记`这个按钮选中状态`
*/
@property (nonatomic, copy) void (^clickMenuButton) (MenuButton *button, NSInteger index, BOOL selected);
/**
* 选中下拉列表某行的回调方法
*
* @param index 用来标记`选中了第几行`
* @param title 用来标记`这个这一行的标题`
*/
@property (nonatomic, copy) void (^clickListView) ( NSInteger index, NSString *title);
/**
* 初始化菜单视图
*
* @param frame 菜单视图frame(推荐x:0.f y:自定义 width:屏幕宽度 height:>=25)
* @param titles 要显示的按钮标题数组
* @param defImage 按钮没有点击之前右边那个小图片
* @param selImage 按钮点击之后右边那个小图片
*
* @return 菜单视图
*/
- (instancetype)initWithFrame:(CGRect)frame
Titles:(NSArray <NSString *>*)titles
defImage:(UIImage *)defImage
selImage:(UIImage *)selImage;
在.m
里边我们初始化界面:
- (instancetype)initWithFrame:(CGRect)frame Titles:(NSArray<NSString *> *)titles defImage:(UIImage *)defImage selImage:(UIImage *)selImage
{
self = [super initWithFrame:frame];
if (self) {
[self subViewsWithTitles:titles defImage:defImage selImage:selImage];
}
return self;
}
- (void)subViewsWithTitles:(NSArray *)titles defImage:(UIImage *)defImage selImage:(UIImage *)selImage
{
self.backgroundColor = [UIColor whiteColor];
// 上边的分割线
CGRect topLineFrame = CGRectMake(0, 1.f, kS_W, 0.5f);
UIView *topLine = [[UIView alloc] initWithFrame:topLineFrame];
topLine.backgroundColor = kLineCor;
[self addSubview:topLine];
// 下边的分割线
CGRect bottomLineFrame = CGRectMake(0, self.h-1.f, kS_W, 0.5f);
UIView *bottomLine = [[UIView alloc] initWithFrame:bottomLineFrame];
bottomLine.backgroundColor = kLineCor;
[self addSubview:bottomLine];
NSInteger count = [titles count];
for (int i=0; i<count; i++) {
// 创建按钮
CGFloat buttonW = (self.w - (count-1)*kLineW)/count;
CGFloat buttonH = 40.f;
CGFloat buttonX = (buttonW+kLineW) * i;
CGRect btnFrame = CGRectMake(buttonX, 0.f, buttonW, buttonH);
MenuButton *button = [[MenuButton alloc] initWithFrame:btnFrame title:titles[i] defImage:defImage selImage:selImage];
[self addSubview:button];
__weak typeof(self)weakSelf = self;
button.clickMenuButton = ^(MenuButton *button, NSString *title, BOOL selected){
if (weakSelf.clickMenuButton) {
weakSelf.clickMenuButton( button,i, selected);
}
if (!_button) {
_button = button;
}
if (button != _button) {
[_button resetStatus:_button];
}
else {
}
_currenTitle = title;
// ------------------------
_button = button;
if (selected) {
[self showListViewAnimation];
}
else {
[self hideListViewAnimation];
}
// ----------------------
};
// 按钮之间的竖直分割线
if (i < count-1) {
CGFloat lineX = buttonX + buttonW;
CGFloat lineY = (self.h-kLineH)/2;
CGRect lineFrame = CGRectMake(lineX, lineY, kLineW, kLineH);
UIView *line = [[UIView alloc] initWithFrame:lineFrame];
line.backgroundColor = kLineCor;
[self addSubview:line];
}
}
}
// 一定要重写这个方法,在这里`reloadData`,这样当我们点击不同的按钮时TableView高度将会发生变化
- (void)setDataSource:(NSArray *)dataSource
{
_dataSource = dataSource;
[self.lTableView reloadData];
}
// 更具数据源里边数据情况,返回TableView高度
- (CGFloat)maxListHeightWithModel:(NSArray *)dataSource
{
NSInteger count = dataSource.count;
CGFloat height = 0.f;
CGFloat oriHeight = kRowH*count;
oriHeight > kS_H/3*2 ? (height = kS_H/3*2) : (height = kRowH*count);
return height;
}
#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.dataSource.count;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return kRowH;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
ListCell *cell = [ListCell cellWithTableView:tableView];
cell.title = self.dataSource[indexPath.row];
cell.selected = [self.dataSource[indexPath.row] isEqualToString:_currenTitle];
return cell;
}
#pragma mark - UITableViewDelegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// 当点击下拉菜单某一行时移除下菜单和遮罩层
[UIView animateWithDuration:0.2f animations:^{
self.shadow.alpha = 0.f;
self.lTableView.h = 0.f;
} completion:^(BOOL finished) {
[self.shadow removeFromSuperview];
[self.lTableView removeFromSuperview];
}];
_button.isSeled = NO;
_button.title = self.dataSource[indexPath.row];
// 当点击下拉菜单某一行时重置按钮状态
[_button resetStatus:_button];
// 实现回调方法
if (self.clickListView) {
self.clickListView(indexPath.row, self.dataSource[indexPath.row]);
}
}
到这里几个主要的方法差不多了,由于现在只是实现了单列下拉列表的情况,所以代码还是比较简单的。
我们在ViewController
里边测试下代码效果:
// 在TableView的这个方法里边我们返回下拉菜单视图
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
// 推荐将`MenuListView`设置为tableView的第一组的组头视图
UIImage *defImg = [UIImage imageNamed:@"gc_navi_arrow_down"];
UIImage *selImg = [UIImage imageNamed:@"gc_navi_arrow_up"];
CGRect frame = CGRectMake(0.f, 0.f, kS_W, 40.f);
NSArray *titles = @[@"自助餐", @"附近", @"智能排序", @"筛选"];
MenuListView *menu = [[MenuListView alloc] initWithFrame:frame Titles:titles defImage:defImg selImage:selImg];
__weak typeof (menu)weakMenu = menu;
menu.clickMenuButton = ^(MenuButton *button, NSInteger index, BOOL selected){
// NSLog(@"点击了第 %ld 个按钮,选中还是取消?:%d", index, selected);
if (index == 0) {
weakMenu.dataSource = @[@"自助餐",@"火锅",@"海鲜",@"烧烤啤酒",@"甜点饮食",@"生日蛋糕",@"小吃快餐",@"日韩料理",@"西餐",@"聚餐宴请",@"川菜",@"江浙菜",@"香锅烤鱼",@"粤菜",@"中式烧烤/烤串",@"西北菜",@"咖啡酒吧",@"京菜鲁菜",@"湘菜",@"生鲜蔬果",@"东北菜",@"云贵菜",@"东南亚菜",@"素食",@"创意菜",@"躺/粥/炖菜",@"新疆菜",@"其他美食"];
}
else if (index == 1) {
weakMenu.dataSource = @[@"附近",@"新津县",@"都江堰",@"温江区",@"郫县",@"龙泉驿区",@"锦江区",@"金牛区",@"成华区",@"青羊区",@"武侯区"];
}
else if (index == 2) {
weakMenu.dataSource = @[@"智能排序", @"离我最近", @"评价最高", @"最新发布", @"人气最高", @"价格最低", @"价格最高"];
}
else if (index == 3) {
weakMenu.dataSource = @[@"只看免预约",@"节假日可用",@"用餐时间段",@"用餐人数",@"餐厅地点"];
}
};
// 选中下拉列表某行时的回调(这个回调方法请务必实现!)
menu.clickListView = ^(NSInteger index, NSString *title){
NSLog(@"选中了-> %d 标题-> %@", index, title);
};
return menu;
}
运行结果:
写在后面
目前这个demo实现的功能还比较简单,如果以后有时间我会继续更新。