
之前看到一個swift開源項目
https://github.com/PhamBaTho/BTNavigationDropdownMenu
就是一個類似新浪微博的下拉式導航菜單,看看下面的效果。

之前看這個項目的時候(大概一個月之前??今天上去看的時候作者已經更新到適配橫豎屏切換了!用的UIViewAutoResizingMask),不能支持橫豎屏切換,這明顯是沒有做布局適配啊,而且沒有Objective-C版本,於是自己用Objective-C重新寫了一個,並且加上Masonry做自動布局適配屏幕切換。當然這裡完全可以用UIViewAutoResizingMask做橫豎屏切換,但是Masonry用起來也很簡單。做一遍下來加深自己對View和自動布局的理解。。。寫下來適合新手看看,高手就繞道吧。不啰嗦了,開始吧。
首先盜用BTNavigationDropdownMenu的圖標元素bundle到我的新建的項目https://github.com/tujinqiu/KTDropdownMenuView下面。。。
1、新建項目,集成UIView創建KTDropdownMenuView。配置CocoaPods。
BTNavigationDropdownMenu

2、添加一些基本的設置屬性和初始化方法,不夠的可以以後再添加
#import @interface KTDropdownMenuView : UIView // cell color default greenColor @property (nonatomic, strong) UIColor *cellColor; // cell seprator color default whiteColor @property (nonatomic, strong) UIColor *cellSeparatorColor; // cell height default 44 @property (nonatomic, assign) CGFloat cellHeight; // animation duration default 0.4 @property (nonatomic, assign) CGFloat animationDuration; // text color default whiteColor @property (nonatomic, strong) UIColor *textColor; // text font default system 17 @property (nonatomic, strong) UIFont *textFont; // background opacity default 0.3 @property (nonatomic, assign) CGFloat backgroundAlpha; - (instancetype)initWithFrame:(CGRect)frame titles:(NSArray*)titles; @end
3、在m文件中定義私有屬性titles,顧名思義這個存放菜單名稱的數組,初始化前面的默認值。個人喜歡用getter來實現懶加載,代碼風格而已,看個人喜好。下面是代碼。。。
#import "KTDropdownMenuView.h"
#import
@interface KTDropdownMenuView()
@property (nonatomic, copy) NSArray *titles;
@end
@implementation KTDropdownMenuView
#pragma mark -- life cycle --
- (instancetype)initWithFrame:(CGRect)frame titles:(NSArray *)titles
{
if (self = [super initWithFrame:frame])
{
_animationDuration=0.4;
_backgroundAlpha=0.3;
_cellHeight=44;
_selectedIndex = 0;
_titles= titles;
}
return self;
}
#pragma mark -- getter and setter --
- (UIColor *)cellColor
{
if (!_cellColor)
{
_cellColor = [UIColor greenColor];
}
return _cellColor;
}
- (UIColor *)cellSeparatorColor
{
if (!_cellSeparatorColor)
{
_cellSeparatorColor = [UIColor whiteColor];
}
return _cellSeparatorColor;
}
- (UIColor *)textColor
{
if (!_textColor)
{
_textColor = [UIColor whiteColor];
}
return _textColor;
}
- (UIFont *)textFont
{
if(!_textFont)
{
_textFont = [UIFont systemFontOfSize:17];
}
return _textFont;
}4、在ViewController中加上如下代碼
[self.navigationController.navigationBar setBarTintColor:[UIColor greenColor]]; KTDropdownMenuView*menuView = [[KTDropdownMenuView alloc] initWithFrame:CGRectMake(0,0,100,44) titles:@[@"首頁",@"朋友圈",@"我的關注",@"明星",@"家人朋友"]]; self.navigationItem.titleView = menuView;
self.navigationItem.titleView = menuView的作用是替換當前的titleView為我們自定義的view。運行一下,除了導航欄變綠之外,並沒有什麼卵用。。。但是,運用Xcode的視圖調試功能,你會發現還是有點卵用的。

轉動一下,導航欄上有個View出現了有木有。
好,下面開始在我們的View上添加控件了,首先導航欄上面有一個可以點的button,同時右邊有一個箭頭是吧。在m文件中加上如下控件
@property (nonatomic, strong) UIButton *titleButton; @property (nonatomic, strong) UIImageView *arrowImageView;
同時寫下getter
- (UIButton *)titleButton
{
if (!_titleButton)
{
_titleButton = [[UIButton alloc] init];
[_titleButton setTitle:[self.titles objectAtIndex:0] forState:UIControlStateNormal];
[_titleButton addTarget:self action:@selector(handleTapOnTitleButton:) forControlEvents:UIControlEventTouchUpInside];
[_titleButton.titleLabel setFont:self.textFont];
[_titleButton setTitleColor:self.textColor forState:UIControlStateNormal];
}
return _titleButton;
}
- (UIImageView *)arrowImageView
{
if (!_arrowImageView)
{
NSString * bundlePath = [[ NSBundle mainBundle] pathForResource:@"KTDropdownMenuView" ofType:@ "bundle"];
NSString *imgPath= [bundlePath stringByAppendingPathComponent:@"arrow_down_icon.png"];
UIImage *image=[UIImage imageWithContentsOfFile:imgPath];
_arrowImageView = [[UIImageView alloc] initWithImage:image];
}
return _arrowImageView;
}接下來當然是addSubView是吧
(instancetype)initWithFrame:(CGRect)frame titles:(NSArray *)titles中寫下
[self addSubview:self.titleButton]; [self addSubview:self.arrowImageView];
運行你會發現button和imageView的大小和位置顯然不是你想的那樣,因為我們並沒有設置控件的frame。好,下面用Masonry了。上代碼。
[self.titleButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.equalTo(self);
}];
[self.arrowImageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.titleButton.mas_right).offset(5);
make.centerY.equalTo(self.titleButton.mas_centerY);
}];Masonry使用非常簡單,就簡單的三個方法,mas_makeConstraints, mas_remakeConstraints, mas_updateConstraints, 比起蘋果自己寫一堆的布局代碼簡單太多。推薦用代碼寫View的童鞋使用Masonry。關於Masonry的詳細說明可以去https://github.com/SnapKit/Masonry 上查看。
上面的代碼很容易理解,第一個約束語句是讓titleButton處於視圖的中間位置。第二個約束語句是讓arrowImageView保持與titleButton水平中心對齊,同時arrowImageView的左邊與titleButton的右邊水平距離為5。
Masonry使用鏈式語法讓添加約束變得非常簡單,要是你自己用蘋果的API活著可視化語言,你得寫一堆的代碼來實現布局。比如下面這樣又臭又長,還容易出錯。
[superview addConstraints:@[ //view1 constraints [NSLayoutConstraint constraintWithItem:view1 attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:superview attribute:NSLayoutAttributeTop multiplier:1.0 constant:padding.top], [NSLayoutConstraint constraintWithItem:view1 attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:superview attribute:NSLayoutAttributeLeft multiplier:1.0 constant:padding.left], [NSLayoutConstraint constraintWithItem:view1 attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:superview attribute:NSLayoutAttributeBottom multiplier:1.0 constant:-padding.bottom], [NSLayoutConstraint constraintWithItem:view1 attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:superview attribute:NSLayoutAttributeRight multiplier:1 constant:-padding.right]] ];
運行之後,果然,使我們預料的效果哈

細心的會發現我用Masonry的時候並沒有設置arrowImageView與titleButton的size,但是照樣運行很好哈。這是因為自動布局系統中,如果你沒有設置控件的size,那麼就會默認使用固有內容大小(Intrinsic Content Size),固有內容會驅動設置控件的size。實際上Xcode裡面大部分的控件都有Intrinsic Content Size。也就是說如果你內容多的時候,size會自動變大。自動布局的這個好處在本地化不同語言(內容長度不一致)的時候非常有用。如果中文的label就兩個字,但是英文一大串的時候,建議你使用自動布局,不要手動去設置label的size。
5、下面添加tableView,加上如下屬性。tableView干嘛?顯然是裝載文字菜單列表啊。
@property (nonatomic, strong) UITableView *tableView; @property (nonatomic, strong) UIView *backgroundView; @property (nonatomic, strong) UIView *wrapperView;
backgroundView是後面的一層半透明的黑色背景,當tableView出現的時候,backgroundView也出現,菜單收起的時候一起消失。wrapperView則是tableView和backgroundView的父View。
那麼問題來了,wrapperView附著到哪裡?顯然不能加在KTDropdownMenuView上哈,答案是附著到當前的keyWindow上面。因為初始化的過程中並沒有傳入其他的View,而且也不應該讓KTDropdownMenuView與其他的view產生關聯。直接添加到keyWindow上面,即可以顯示在最上層。
另外一個問題是wrapperView的大小位置如何設置?如何保證旋轉屏幕也能適配大小?利用自動布局可以適配旋轉屏幕,同時wrapperView要在導航欄下面顯示。那麼很容易想到wrapperView的top要依靠在導航欄的bottom,同時左,右,下需要與當前keyWindow分別對齊。
那麼問題又來了,如何找到navigationBar?初始化方法並沒有傳進來啊。。。當然簡單的辦法是傳一個進來一個哈,這裡用BTNavigationDropdownMenu的思路,遞歸搜索最前面的UINavigationController就行,代碼貼上來,自己理解。。。
@implementation UIViewController (topestViewController)
- (UIViewController *)topestViewController
{
if (self.presentedViewController)
{
return [self.presentedViewController topestViewController];
}
if ([self isKindOfClass:[UITabBarController class]])
{
UITabBarController *tab = (UITabBarController *)self;
return [[tab selectedViewController] topestViewController];
}
if ([self isKindOfClass:[UINavigationController class]])
{
UINavigationController *nav = (UINavigationController *)self;
return [[nav visibleViewController] topestViewController];
}
return self;
}
@end下面在初始化方法中加上如下代碼
UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
UINavigationBar *navBar = [keyWindow.rootViewController topestViewController].navigationController.navigationBar;
[keyWindow addSubview:self.wrapperView];
[self.wrapperView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.bottom.equalTo(keyWindow);
make.top.equalTo(navBar.mas_bottom);
}];
[self.wrapperView addSubview:self.backgroundView];
[self.backgroundView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.wrapperView);
}];
[self.wrapperView addSubview:self.tableView];
[self.tableView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.wrapperView);
}];以上略掉tableViewDataSource的相關代碼和getter。

旋轉一下,很好,沒有問題,自動布局工作的很好

6、下面加上按鈕響應和動畫
添加下面兩個屬性
@property (nonatomic, assign) BOOL isMenuShow; @property (nonatomic, assign) NSUInteger selectedIndex;
然後實現按鈕的點擊事件方法,實現tableView的delegate方法
#pragma mark -- UITableViewDataDelegate --
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
self.selectedIndex = indexPath.row;
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
#pragma mark -- handle actions --
- (void)handleTapOnTitleButton:(UIButton *)button
{
self.isMenuShow = !self.isMenuShow;
}相應的屬性setter
- (void)setIsMenuShow:(BOOL)isMenuShow
{
if (_isMenuShow != isMenuShow)
{
_isMenuShow = isMenuShow;
if (isMenuShow)
{
[self showMenu];
}
else
{
[self hideMenu];
}
}
}
- (void)setSelectedIndex:(NSUInteger)selectedIndex
{
if (_selectedIndex != selectedIndex)
{
_selectedIndex = selectedIndex;
[_titleButton setTitle:[_titles objectAtIndex:selectedIndex] forState:UIControlStateNormal];
[self.tableView reloadData];
}
self.isMenuShow = NO;
}在實現動畫方法showMenu和hideMenu之前,先考慮:這個tableView在出現的時候是從上往下出現的,也就是這個tableView的出現的這幾行的下端應該在wrapperView的頂端,於是先修改init方法中設置tableView起始位置的代碼。
CGFloat tableCellsHeight = _cellHeight * _titles.count;
[self.tableView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.equalTo(self.wrapperView);
make.top.equalTo(self.wrapperView.mas_top).offset(-tableCellsHeight);
make.bottom.equalTo(self.wrapperView.mas_bottom).offset(tableCellsHeight);
}];
[self.tableView layoutIfNeeded];
self.wrapperView.hidden = YES;注意到最後加了一句 [self.tableView layoutIfNeeded],這是因為自動布局動畫都是驅動layoutIfNeeded來實現的,與以往的設置frame不一樣。給View添加或者更新約束後,並不能馬上看到效果,而是要等到viewlayout的時候觸發。layoutIfNeeded就是手動觸發這一過程。這裡為了與後面的動畫不沖突,首先調用一次,設置初始狀態。下面是動畫代碼。
- (void)showMenu
{
[self.tableView mas_updateConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.wrapperView);
}];
self.wrapperView.hidden = NO;
self.backgroundView.alpha = 0.0;
[UIView animateWithDuration:self.animationDuration
animations:^{
self.arrowImageView.transform = CGAffineTransformRotate(self.arrowImageView.transform, M_PI);
}];
[UIView animateWithDuration:self.animationDuration * 1.5
delay:0
usingSpringWithDamping:0.7
initialSpringVelocity:0.5
options:UIViewAnimationOptionCurveLinear
animations:^{
[self.tableView layoutIfNeeded];
self.backgroundView.alpha = self.backgroundAlpha;
} completion:nil];
}
- (void)hideMenu
{
CGFloat tableCellsHeight = _cellHeight * _titles.count;
[self.tableView mas_updateConstraints:^(MASConstraintMaker *make) {
make.left.right.equalTo(self.wrapperView);
make.top.equalTo(self.wrapperView.mas_top).offset(-tableCellsHeight);
make.bottom.equalTo(self.wrapperView.mas_bottom).offset(tableCellsHeight);
}];
[UIView animateWithDuration:self.animationDuration
animations:^{
self.arrowImageView.transform = CGAffineTransformRotate(self.arrowImageView.transform, M_PI);
}];
[UIView animateWithDuration:self.animationDuration * 1.5
delay:0
usingSpringWithDamping:0.7
initialSpringVelocity:0.5
options:UIViewAnimationOptionCurveLinear
animations:^{
[self.tableView layoutIfNeeded];
self.backgroundView.alpha = 0.0;
} completion:^(BOOL finished) {
self.wrapperView.hidden = YES;
}];
}代碼很簡單,主要是設置動畫之後的tableView約束位置,旋轉arrowImageView同時改變backgroundView的透明度,注意這裡是調用的mas_updateConstraints是更新約束,一搬做動畫都是用這個。但是細心的話會發現有一個bug,動畫過程中,還有把tableView往下面拽的時候,上面和導航欄之間會出現灰色背景啊。

不能忍。添加一個與tableCell一樣顏色的tableHeaderView到tableView上面吧。在showMenu方法的開頭加上下面代碼。
UIView *headerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, kKTDropdownMenuViewHeaderHeight)]; headerView.backgroundColor = self.cellColor; self.tableView.tableHeaderView = headerView;
其中kKTDropdownMenuViewHeaderHeight設置為300。值得注意的是,這裡並不需要設置tableHeaderView的寬度,它會自適應到tableView的寬度。還有加了tableHeaderView之後,相應的mas_updateConstraints和mas_makeConstraints方法中需要將位置上移kKTDropdownMenuViewHeaderHeight的距離。同時把init方法中的[self.tableView layoutIfNeeded]移動到添加tableHeaderView之後。現在動畫還有拖拽不會看到背景了。

完整的項目在這裡,https://github.com/tujinqiu/KTDropdownMenuView
歡迎討論交流,批評指正!!!