使用Masonry創(chuàng)建一個(gè)下拉菜單

之前看到一個(gè)swift開源項(xiàng)目BTNavigationDropdownMenu, 就是一個(gè)類似新浪微博的下拉式導(dǎo)航菜單,看看下面的效果:

之前看這個(gè)項(xiàng)目的時(shí)候(現(xiàn)在作者已經(jīng)更新到適配橫豎屏切換了!用的UIViewAutoResizingMask),不能支持橫豎屏切換,而且沒有Objective-C版本,于是自己用Objective-C重新寫了一個(gè),并且加上Masonry做自動(dòng)布局適配屏幕切換,做一遍下來加深自己對View層次和自動(dòng)布局的理解。寫下來適合新手看看,高手就繞道吧,不啰嗦了,開始吧。。。

1、新建項(xiàng)目,盜用BTNavigationDropdownMenu的圖標(biāo)元素bundle到我的自己的項(xiàng)目下面。繼承UIView創(chuàng)建KTDropdownMenuView。配置CocoaPods,引入Masonry:

pod "Masonry"

2、添加一些基本的設(shè)置屬性和初始化方法,不夠的可以以后再添加

#import <UIKit/UIKit.h>

@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,顧名思義這個(gè)存放菜單名稱的數(shù)組,初始化前面的默認(rèn)值。個(gè)人喜歡用getter來實(shí)現(xiàn)懶加載,代碼風(fēng)格而已,看個(gè)人喜好,下面是代碼:

#import "KTDropdownMenuView.h"
#import <Masonry.h>

@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:@[@"首頁",@"朋友圈",@"我的關(guān)注",@"明星",@"家人朋友"]];
self.navigationItem.titleView = menuView;

self.navigationItem.titleView = menuView的作用是替換當(dāng)前的titleView為我們自定義的view。運(yùn)行一下,除了導(dǎo)航欄變綠之外,并沒有什么卵用。但是,運(yùn)用Xcode的視圖調(diào)試功能,你會(huì)發(fā)現(xiàn)還是有點(diǎn)卵用的:


轉(zhuǎn)動(dòng)一下,導(dǎo)航欄上有個(gè)View出現(xiàn)了有木有!只是menuView沒有顏色導(dǎo)致你看不見而已。

5、好,下面開始在我們的View上添加控件了,首先導(dǎo)航欄上面有一個(gè)可以點(diǎn)的button,同時(shí)右邊有一個(gè)箭頭是吧。在m文件中加上如下控件

@property (nonatomic, strong) UIButton *titleButton;
@property (nonatomic, strong) UIImageView *arrowImageView;

同時(shí)寫下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;
}

接下來當(dāng)然是addSubView添加到view中:-(instancetype)initWithFrame:(CGRect)frame titles:(NSArray *)titles方法中寫下:

 [self addSubview:self.titleButton];
 [self addSubview:self.arrowImageView];

運(yùn)行你會(huì)發(fā)現(xiàn)button和imageView的大小和位置顯然不是你想的那樣,因?yàn)槲覀儾]有設(shè)置控件的位置。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使用非常簡單,就簡單的三個(gè)方法,mas_makeConstraints, mas_remakeConstraints, mas_updateConstraints來進(jìn)行約束的管理, 比起用原生方法寫一堆的布局代碼簡單太多。強(qiáng)烈推薦喜歡用代碼寫View的童鞋使用Masonry來進(jìn)行約束布局。關(guān)于Masonry的更詳細(xì)用法可以去https://github.com/SnapKit/Masonry 上查看。
上面的代碼很容易理解,第一個(gè)約束語句是讓titleButton處于視圖的中間位置。第二個(gè)約束語句是讓arrowImageView保持與titleButton水平中心對齊,同時(shí)arrowImageView的左邊與titleButton的右邊水平距離為5。
Masonry使用鏈?zhǔn)秸Z法讓添加約束變得非常簡單,要是你自己用蘋果的原生API,你得寫一堆的代碼來實(shí)現(xiàn)布局。比如下面這樣又臭又長,還容易出錯(cuò)。另外一點(diǎn)就是Masonry的語法非常易讀,上面的幾行代碼從左往右閱讀,毫不費(fèi)力。

[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]]
];

運(yùn)行之后,果然,使我們預(yù)料的效果哈

Paste_Image.png

細(xì)心的會(huì)發(fā)現(xiàn)我用Masonry的時(shí)候并沒有設(shè)置arrowImageView與titleButton的size,但是照樣運(yùn)行地很好。這是因?yàn)樽詣?dòng)布局系統(tǒng)中,如果你沒有設(shè)置控件的size,那么就會(huì)默認(rèn)使用固有內(nèi)容大?。?strong>Intrinsic Content Size),固有內(nèi)容會(huì)驅(qū)動(dòng)設(shè)置控件的size。實(shí)際上Xcode里面大部分的控件都有Intrinsic Content Size。也就是說如果你內(nèi)容多的時(shí)候,size會(huì)自動(dòng)變大,反之內(nèi)容少的時(shí)候,size會(huì)自動(dòng)變小。自動(dòng)布局的Intrinsic Content Size這個(gè)特性在本地化不同語言(內(nèi)容長度不一致)的時(shí)候非常有用。比如用一個(gè)label顯示中文的時(shí)候,可能就兩個(gè)字很短,但是翻譯成英文變成一大串,這時(shí)候使用自動(dòng)布局,不要手動(dòng)去設(shè)置label的size,自動(dòng)布局會(huì)自動(dòng)設(shè)置好label所需的size。

6、下面添加tableView,加上如下屬性。tableView是用來裝載文字菜單列表的;backgroundView是后面的一層半透明的黑色背景,當(dāng)tableView出現(xiàn)的時(shí)候,backgroundView也出現(xiàn),菜單收起的時(shí)候一起消失;wrapperView則是tableView和backgroundView的父View。

@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, strong) UIView *backgroundView;
@property (nonatomic, strong) UIView *wrapperView;

那么問題來了,wrapperView附著到哪里?顯然不能加在KTDropdownMenuView上哈,答案是附著到當(dāng)前的keyWindow上面。因?yàn)槌跏蓟倪^程中并沒有傳入其他的View,而且也不應(yīng)該讓KTDropdownMenuView與其他的view產(chǎn)生關(guān)聯(lián),否則KTDropdownMenuView會(huì)隨著其他view的消失而消失。直接添加到keyWindow上面,即可以顯示在最上層。
另外一個(gè)問題是wrapperView的大小位置如何設(shè)置?如何保證旋轉(zhuǎn)屏幕也能適配大小?利用自動(dòng)布局可以適配旋轉(zhuǎn)屏幕,同時(shí)wrapperView要在導(dǎo)航欄下面顯示。那么很容易想到wrapperView的top要依靠在導(dǎo)航欄的bottom,同時(shí)左,右,下需要與當(dāng)前keyWindow分別對齊。
那么問題又來了,如何找到導(dǎo)航欄navigationBar?初始化方法并沒有傳進(jìn)來啊。。。當(dāng)然簡單的辦法在初始化方法里面?zhèn)饕粋€(gè)進(jìn)來,這里用BTNavigationDropdownMenu的思路,遞歸搜索最前面的UINavigationController,然后獲取navigationBar,代碼貼上來,自己理解。。。

@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的相關(guān)代碼和getter。
wrapperView的布局代碼,如前面分析的一樣:wrapperView的top要對其導(dǎo)航欄的bottom,同時(shí)左,右,下需要與當(dāng)前keyWindow分別對齊。那么在屏幕旋轉(zhuǎn)的時(shí)候,keyWindow和導(dǎo)航欄也會(huì)旋轉(zhuǎn)(系統(tǒng)幫你做的),wrapperView要保持約束關(guān)系不變,也會(huì)自動(dòng)跟著旋轉(zhuǎn),這就是為什么自動(dòng)布局能適應(yīng)屏幕旋轉(zhuǎn)的原因。

make.left.right.bottom.equalTo(keyWindow);
make.top.equalTo(navBar.mas_bottom);

運(yùn)行一下:



旋轉(zhuǎn)一下,自動(dòng)布局工作的很好,能自動(dòng)適應(yīng)屏幕旋轉(zhuǎn)。

7、下面加上按鈕響應(yīng)和動(dòng)畫,添加下面兩個(gè)屬性:

@property (nonatomic, assign) BOOL isMenuShow;
@property (nonatomic, assign) NSUInteger selectedIndex;

然后實(shí)現(xiàn)按鈕的點(diǎn)擊事件方法,實(shí)現(xiàn)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;
}

相應(yīng)的屬性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;
}

在實(shí)現(xiàn)動(dòng)畫方法showMenu和hideMenu之前,先考慮:這個(gè)tableView在出現(xiàn)的時(shí)候是從上往下出現(xiàn)的,也就是這個(gè)tableView出現(xiàn)前它的bottom應(yīng)該在wrapperView的top上面,并且要被擋住不能被看見(被擋住很簡單,設(shè)置wrapperView的clipsToBounds為YES,它的subView在超出邊界的時(shí)候自動(dòng)會(huì)被擋?。?。于是先修改init方法中設(shè)置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],這是因?yàn)樽詣?dòng)布局動(dòng)畫都是驅(qū)動(dòng)layoutIfNeeded來實(shí)現(xiàn)的,與以往的設(shè)置frame不一樣。給View添加或者更新約束后,并不能馬上看到效果,而是要等到view layout的時(shí)候觸發(fā),layoutIfNeeded就是手動(dòng)觸發(fā)這一過程。這里為了與后面的動(dòng)畫不沖突,首先調(diào)用一次,設(shè)置初始狀態(tài),下面是動(dòng)畫代碼:

- (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;
                     }];
}

代碼很簡單,主要是設(shè)置動(dòng)畫之后的tableView約束位置,旋轉(zhuǎn)arrowImageView同時(shí)改變backgroundView的透明度,注意這里是調(diào)用的mas_updateConstraints是更新約束,一搬做動(dòng)畫都是用這個(gè)。但是細(xì)心的話會(huì)發(fā)現(xiàn)有一個(gè)bug,動(dòng)畫過程中,還有把tableView往下面拽的時(shí)候,上面和導(dǎo)航欄之間會(huì)出現(xiàn)灰色背景啊。


不能忍,添加一個(gè)與tableCell一樣顏色的tableHeaderView到tableView上面,在showMenu方法的開頭加上下面代碼:

    UIView *headerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, kKTDropdownMenuViewHeaderHeight)];
    headerView.backgroundColor = self.cellColor;
    self.tableView.tableHeaderView = headerView;

其中kKTDropdownMenuViewHeaderHeight設(shè)置為300。值得注意的是,這里并不需要設(shè)置tableHeaderView的寬度,它會(huì)自適應(yīng)到tableView的寬度。還有加了tableHeaderView之后,相應(yīng)的mas_updateConstraints和mas_makeConstraints方法中需要將位置上移kKTDropdownMenuViewHeaderHeight的距離。同時(shí)把init方法中的[self.tableView layoutIfNeeded]移動(dòng)到添加tableHeaderView之后?,F(xiàn)在動(dòng)畫或者拖拽的時(shí)候不會(huì)看到丑陋的背景了。


完整的項(xiàng)目在這里,https://github.com/tujinqiu/KTDropdownMenuView
歡迎討論交流,批評指正!??!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,636評論 4 61
  • Masonry是一個(gè)輕量級(jí)的布局框架,擁有自己的描述語法,采用更優(yōu)雅的鏈?zhǔn)秸Z法封裝自動(dòng)布局,簡潔明了并具有高可讀性...
    3dcc6cf93bb5閱讀 1,940評論 0 1
  • (一)Masonry介紹 Masonry是一個(gè)輕量級(jí)的布局框架 擁有自己的描述語法 采用更優(yōu)雅的鏈?zhǔn)秸Z法封裝自動(dòng)布...
    木易林1閱讀 2,594評論 0 3
  • 特種兵訓(xùn)練,把業(yè)余時(shí)間全部都占用了,聽課、做作業(yè)、簽到、pk…… 小伙伴們每天平均睡覺時(shí)間都是在23:00,這就意...
    Nicole_dd09閱讀 326評論 0 0
  • 1. 安裝wireshark Ubuntu 14.04.3 缺省安裝后, 不包含Wireshark抓包軟件,因此首...
    wwyyzz閱讀 29,639評論 1 10

友情鏈接更多精彩內(nèi)容