很早之前就打算寫個仿美團的下拉列表,但因為最近一直在忙其他的事情所以一直沒有著手做,上周還算有時間,于是抽了一天的時間簡單封裝了下,當(dāng)然到目前為止實現(xiàn)的功能還只是只能顯示一組下拉列表,如果以后有時間,這個demo我會繼續(xù)更新和維護,到時候會放在這個博客里邊,筆者寫這個demo主要是自己學(xué)習(xí)之用,當(dāng)然能夠幫到對這方面有需求的用戶那更好。先看下效果圖:

思路分析
對于項目中特別是我們在開發(fā)中之前沒有遇到的需求或者問題,首先我們應(yīng)該通過對需求的分析而總結(jié)出實現(xiàn)的思路,其實不管對于iOS開發(fā),只要有了實現(xiàn)思路,代碼就不是什么問題了。其實對于美團的下拉列表,通過觀察可以發(fā)現(xiàn)當(dāng)對商品列表的TableView做向上滑動時它的下拉列表的菜單是有懸停效果的,而當(dāng)對TableView不斷向下拉時它的下拉列表的菜單又會隨著TableView向下滑動,而我們知道TableView的組頭視圖是有這個效果的,所以如果沒有什么特別需求,我們完全可以將要封裝的下拉列表菜單當(dāng)做TableView的第一組的組頭視圖。再看TableView的下拉菜單按鈕:
每點擊一下就要改變標(biāo)題顏色,而且文字右邊那個朝下的三角形也要換成對應(yīng)顏色的朝上的箭頭,同時還要彈出對應(yīng)的下拉列表。但這里我們就應(yīng)該想,要實現(xiàn)這樣的按鈕效果,可能需要自己的封裝,畢竟系統(tǒng)的UIButton是沒有這樣的效果的,同時我們可以解壓下美團官方的APP報,如果能找找到對應(yīng)的圖片那么我們的思路就八九不離十了(經(jīng)過筆者查看,官方包里邊確實有對應(yīng)的兩個三角形圖片),我們看到美團的下拉出來的TableView在切換不同的列表的時候,如果拉出來的TableView高度不同,列表會有個緩慢的改變高度的過程,那么我們就需要每次切換數(shù)據(jù)源時讓TableView執(zhí)行一次reloadData
的操作。
代碼實現(xiàn)
封裝菜單按鈕
我們創(chuàng)建MenuButton
類,讓其繼承自UIView
。(筆者本來想讓其繼承自UIButton的,但是想到UIButton
里邊已經(jīng)有UILabel
和UIImageView
控件了,為了本著簡潔高效的原則,所以就沒有繼承自UIButton
)
在.h
里邊:
/** 當(dāng)按鈕標(biāo)題改變時,要觸發(fā)`setTitle:`這個方法 */
@property (nonatomic, copy) NSString *title;
/**
* 選中某個按鈕時回調(diào)方法
*
* @param button 用來標(biāo)記選中的按鈕
* @param index 用來標(biāo)記`選中了第幾個按鈕`
* @param selected 用來標(biāo)記`這個按鈕選中狀態(tài)`
*/
@property (nonatomic, copy) void (^clickMenuButton) (MenuButton *button, NSString *title, BOOL selected);
/**
* 按鈕初始化方法
*
* @param frame 按鈕frame
* @param title 按鈕標(biāo)題
* @param defImage 按鈕上的默認(rèn)圖片
* @param selImage 按鈕選中時的圖片
*/
- (instancetype)initWithFrame:(CGRect)frame
title:(NSString *)title
defImage:(UIImage *)defImage
selImage:(UIImage *)selImage
經(jīng)過我們的分析我只,我們需要兩個控件一個UILabel
用來顯示按鈕的標(biāo)題,一個UIImageView
用來設(shè)置標(biāo)題右邊那個三角形圖片,在.m
里邊:
@interface MenuButton ()
{
NSString *_title; // 按鈕標(biāo)題
UIImage *_defImg; // 按鈕沒有選中時右邊圖片
UIImage *_selImg; // 按鈕選中時右邊圖片
}
/** 用來設(shè)置按鈕標(biāo)題 */
@property (nonatomic, strong) UILabel *titLabel;
/** 用來設(shè)置圖片 */
@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];
// 給視圖添加手勢,當(dāng)我們點擊視圖是觸發(fā)`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);
}
}
當(dāng)按鈕的標(biāo)題改變時,我們要實現(xiàn)視圖的setTitle:
方法:
- (void)setTitle:(NSString *)title
{
self.titLabel.text = title;
UIFont *font = [UIFont systemFontOfSize:kFontSize];
_titLabel.font = font;
// 為了不至于讓標(biāo)題和圖片由于位置變化而太難看,我們需要在按鈕標(biāo)題每次改變時重新設(shè)置它的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];
}
封裝下拉菜單
我們創(chuàng)建MenuButton
類,讓其繼承自UIView
,在.h
里邊:
/**
* 數(shù)據(jù)源
*/
@property (nonatomic, strong) NSArray *dataSource;
/**
* 選中某個按鈕時回調(diào)方法(這個回調(diào)其實可以不寫)
*
* @param button 用來標(biāo)記選中的按鈕
* @param index 用來標(biāo)記`選中了第幾個按鈕`
* @param selected 用來標(biāo)記`這個按鈕選中狀態(tài)`
*/
@property (nonatomic, copy) void (^clickMenuButton) (MenuButton *button, NSInteger index, BOOL selected);
/**
* 選中下拉列表某行的回調(diào)方法
*
* @param index 用來標(biāo)記`選中了第幾行`
* @param title 用來標(biāo)記`這個這一行的標(biāo)題`
*/
@property (nonatomic, copy) void (^clickListView) ( NSInteger index, NSString *title);
/**
* 初始化菜單視圖
*
* @param frame 菜單視圖frame(推薦x:0.f y:自定義 width:屏幕寬度 height:>=25)
* @param titles 要顯示的按鈕標(biāo)題數(shù)組
* @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++) {
// 創(chuàng)建按鈕
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`,這樣當(dāng)我們點擊不同的按鈕時TableView高度將會發(fā)生變化
- (void)setDataSource:(NSArray *)dataSource
{
_dataSource = dataSource;
[self.lTableView reloadData];
}
// 更具數(shù)據(jù)源里邊數(shù)據(jù)情況,返回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
{
// 當(dāng)點擊下拉菜單某一行時移除下菜單和遮罩層
[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];
// 當(dāng)點擊下拉菜單某一行時重置按鈕狀態(tài)
[_button resetStatus:_button];
// 實現(xiàn)回調(diào)方法
if (self.clickListView) {
self.clickListView(indexPath.row, self.dataSource[indexPath.row]);
}
}
到這里幾個主要的方法差不多了,由于現(xiàn)在只是實現(xiàn)了單列下拉列表的情況,所以代碼還是比較簡單的。
我們在ViewController
里邊測試下代碼效果:
// 在TableView的這個方法里邊我們返回下拉菜單視圖
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
// 推薦將`MenuListView`設(shè)置為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 = @[@"自助餐",@"火鍋",@"海鮮",@"燒烤啤酒",@"甜點飲食",@"生日蛋糕",@"小吃快餐",@"日韓料理",@"西餐",@"聚餐宴請",@"川菜",@"江浙菜",@"香鍋烤魚",@"粵菜",@"中式燒烤/烤串",@"西北菜",@"咖啡酒吧",@"京菜魯菜",@"湘菜",@"生鮮蔬果",@"東北菜",@"云貴菜",@"東南亞菜",@"素食",@"創(chuàng)意菜",@"躺/粥/燉菜",@"新疆菜",@"其他美食"];
}
else if (index == 1) {
weakMenu.dataSource = @[@"附近",@"新津縣",@"都江堰",@"溫江區(qū)",@"郫縣",@"龍泉驛區(qū)",@"錦江區(qū)",@"金牛區(qū)",@"成華區(qū)",@"青羊區(qū)",@"武侯區(qū)"];
}
else if (index == 2) {
weakMenu.dataSource = @[@"智能排序", @"離我最近", @"評價最高", @"最新發(fā)布", @"人氣最高", @"價格最低", @"價格最高"];
}
else if (index == 3) {
weakMenu.dataSource = @[@"只看免預(yù)約",@"節(jié)假日可用",@"用餐時間段",@"用餐人數(shù)",@"餐廳地點"];
}
};
// 選中下拉列表某行時的回調(diào)(這個回調(diào)方法請務(wù)必實現(xiàn)!)
menu.clickListView = ^(NSInteger index, NSString *title){
NSLog(@"選中了-> %d 標(biāo)題-> %@", index, title);
};
return menu;
}
運行結(jié)果:
寫在后面
目前這個demo實現(xiàn)的功能還比較簡單,如果以后有時間我會繼續(xù)更新。