使用Masonry创建一个下拉菜单

之前看到一个swift开源项目BTNavigationDropdownMenu, 就是一个类似新浪微博的下拉式导航菜单,看看下面的效果:

之前看这个项目的时候(现在作者已经更新到适配横竖屏切换了!用的UIViewAutoResizingMask),不能支持横竖屏切换,而且没有Objective-C版本,于是自己用Objective-C重新写了一个,并且加上Masonry做自动布局适配屏幕切换,做一遍下来加深自己对View层次和自动布局的理解。写下来适合新手看看,高手就绕道吧,不啰嗦了,开始吧。。。

1、新建项目,盗用BTNavigationDropdownMenu的图标元素bundle到我的自己的项目下面。继承UIView创建KTDropdownMenuView。配置CocoaPods,引入Masonry:

pod "Masonry"

2、添加一些基本的设置属性和初始化方法,不够的可以以后再添加

#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,顾名思义这个存放菜单名称的数组,初始化前面的默认值。个人喜欢用getter来实现懒加载,代码风格而已,看个人喜好,下面是代码:

#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:@[@"首页",@"朋友圈",@"我的关注",@"明星",@"家人朋友"]];
self.navigationItem.titleView = menuView;

self.navigationItem.titleView = menuView的作用是替换当前的titleView为我们自定义的view。运行一下,除了导航栏变绿之外,并没有什么卵用。但是,运用Xcode的视图调试功能,你会发现还是有点卵用的:


转动一下,导航栏上有个View出现了有木有!只是menuView没有颜色导致你看不见而已。

5、好,下面开始在我们的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添加到view中:-(instancetype)initWithFrame:(CGRect)frame titles:(NSArray *)titles方法中写下:

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

运行你会发现button和imageView的大小和位置显然不是你想的那样,因为我们并没有设置控件的位置。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,你得写一堆的代码来实现布局。比如下面这样又臭又长,还容易出错。另外一点就是Masonry的语法非常易读,上面的几行代码从左往右阅读,毫不费力。

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

运行之后,果然,使我们预料的效果哈

Paste_Image.png

细心的会发现我用Masonry的时候并没有设置arrowImageView与titleButton的size,但是照样运行地很好。这是因为自动布局系统中,如果你没有设置控件的size,那么就会默认使用固有内容大小(Intrinsic Content Size),固有内容会驱动设置控件的size。实际上Xcode里面大部分的控件都有Intrinsic Content Size。也就是说如果你内容多的时候,size会自动变大,反之内容少的时候,size会自动变小。自动布局的Intrinsic Content Size这个特性在本地化不同语言(内容长度不一致)的时候非常有用。比如用一个label显示中文的时候,可能就两个字很短,但是翻译成英文变成一大串,这时候使用自动布局,不要手动去设置label的size,自动布局会自动设置好label所需的size。

6、下面添加tableView,加上如下属性。tableView是用来装载文字菜单列表的;backgroundView是后面的一层半透明的黑色背景,当tableView出现的时候,backgroundView也出现,菜单收起的时候一起消失;wrapperView则是tableView和backgroundView的父View。

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

那么问题来了,wrapperView附着到哪里?显然不能加在KTDropdownMenuView上哈,答案是附着到当前的keyWindow上面。因为初始化的过程中并没有传入其他的View,而且也不应该让KTDropdownMenuView与其他的view产生关联,否则KTDropdownMenuView会随着其他view的消失而消失。直接添加到keyWindow上面,即可以显示在最上层。
另外一个问题是wrapperView的大小位置如何设置?如何保证旋转屏幕也能适配大小?利用自动布局可以适配旋转屏幕,同时wrapperView要在导航栏下面显示。那么很容易想到wrapperView的top要依靠在导航栏的bottom,同时左,右,下需要与当前keyWindow分别对齐。
那么问题又来了,如何找到导航栏navigationBar?初始化方法并没有传进来啊。。。当然简单的办法在初始化方法里面传一个进来,这里用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的相关代码和getter。
wrapperView的布局代码,如前面分析的一样:wrapperView的top要对其导航栏的bottom,同时左,右,下需要与当前keyWindow分别对齐。那么在屏幕旋转的时候,keyWindow和导航栏也会旋转(系统帮你做的),wrapperView要保持约束关系不变,也会自动跟着旋转,这就是为什么自动布局能适应屏幕旋转的原因。

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

运行一下:



旋转一下,自动布局工作的很好,能自动适应屏幕旋转。

7、下面加上按钮响应和动画,添加下面两个属性:

@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出现前它的bottom应该在wrapperView的top上面,并且要被挡住不能被看见(被挡住很简单,设置wrapperView的clipsToBounds为YES,它的subView在超出边界的时候自动会被挡住)。于是先修改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添加或者更新约束后,并不能马上看到效果,而是要等到view layout的时候触发,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
欢迎讨论交流,批评指正!!!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,029评论 4 62
  • Masonry是一个轻量级的布局框架,拥有自己的描述语法,采用更优雅的链式语法封装自动布局,简洁明了并具有高可读性...
    3dcc6cf93bb5阅读 1,753评论 0 1
  • (一)Masonry介绍 Masonry是一个轻量级的布局框架 拥有自己的描述语法 采用更优雅的链式语法封装自动布...
    木易林1阅读 2,316评论 0 3
  • 特种兵训练,把业余时间全部都占用了,听课、做作业、签到、pk…… 小伙伴们每天平均睡觉时间都是在23:00,这就意...
    Nicole_dd09阅读 236评论 0 0
  • 1. 安装wireshark Ubuntu 14.04.3 缺省安装后, 不包含Wireshark抓包软件,因此首...
    wwyyzz阅读 29,262评论 1 10