美文网首页不明觉厉iOSiOS-VendoriOS资源库
使用Masonry创建一个下拉菜单

使用Masonry创建一个下拉菜单

作者: KevinTing | 来源:发表于2015-10-17 20:43 被阅读2319次

    之前看到一个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
    欢迎讨论交流,批评指正!!!

    相关文章

      网友评论

      • 舒马赫:Masory写的很棒,但是不喜欢纯代码写界面,太慢了,另外由于autolayout先天原因布局速度是比较慢的,会影响帧率。推荐使用xml的布局库FlexLib,采用前端布局标准flexbox(不使用autolayout),支持热刷新,自动计算高度等。可以到这里了解详细信息:

        https://github.com/zhenglibao/FlexLib
      • T_Yang:感谢 帮了大忙!
      • b523aa780226:nice,不过建议楼主加个点击空白处隐藏弹窗的功能 :smile:
      • 空转风:楼主,我按照你的步骤却无法横屏适配。能否指点一下

        - (instancetype)initWithFrame:(CGRect)frame
        {
        self = [super initWithFrame:frame];
        if (self) {
        [self addSubview:self.bgView];

        }
        return self;
        }

        -(void)layoutSubviews
        {

        }
        - (void)didMoveToWindow
        {
        [super didMoveToWindow];
        if(self.window)
        {
        [self.bgView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.offset(0);
        make.right.offset(0);
        make.bottom.offset(0);
        make.height.offset(64);
        }];
        }

        }
        -(UIView*)bgView
        {
        if (!_bgView) {
        _bgView = [[UIView alloc]init];
        _bgView.backgroundColor = [UIColor purpleColor];
        }
        return _bgView;
        }
        KevinTing:@年光逝也被僵尸号占了 你的约束写的有问题吧
      • 4a52d902c2bd:我想问一下,你这个用在tabbarcontroller 的情况好像就失效了?点击另一个标签页,再点回来,titlemeau就点击不出东西了。请问怎么解决呀?
        KevinTing:@从今以后 谢谢提醒,解析的很到位
        4a52d902c2bd:@从今以后 谢谢啦~
        不知什么人:@ToonoW 这是因为切换标签页时,导航控制器换了,KTDropdownMenuView 所在的导航栏也就从窗口移除了,这将导致下面这个约束被移除:
        [self.wrapperView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.right.bottom.equalTo(keyWindow);
        /* 这条约束被移除了,因为导航栏不在窗口上,
        而 wrapperView 还在窗口上,它俩没有共同的父视图了 */
        make.top.equalTo(navBar.mas_bottom);
        }];
        此时 wrapperView 的高度会变为 0,重新切换回先前标签页时,并没有重新添加这条约束,导致高度始终为 0。
        可以添加下面这个方法,重新切换回先前标签页时,KTDropdownMenuView 再次被添加到窗口上,此时重新添加这条丢失的约束即可:
        - (void)didMoveToWindow
        {
        if (self.window) {
        [self.wrapperView mas_makeConstraints:^(MASConstraintMaker *make) {
        /* self 即 KTDropdownMenuView,其 superview 即导航栏 */
        make.top.equalTo(self.superview.mas_bottom);
        }];
        }
        }
        先前添加这条约束的地方就可以不用添加了,不然会重复添加一条,不过也没啥影响。
      • Easy_VO:博主,你这个放到tabbar里,当切换tab之后再回去点击就失效了 :persevere:
        KevinTing:@Easyzhan 新版本已解决这个问题,提交到github上了
      • 4a52d902c2bd:博主,为什么我pod install 了 Masonry 之后出现编译错误?
        linker command failed with exit code 1 (use -v to see invocation)
        KevinTing:@ToonoW 额,解决了么?要看具体原因,愿意的话可以发工程到我的邮箱819931323@qq.com看看
        4a52d902c2bd:@KevinTing 我的确是打开新生成的xcworkspace文件的,很是疑惑
        KevinTing:@ToonoW pod install之后会自动生成一个xcworkspace文件,打开xcworkspace不要再打开原来的xcodeproj文件
      • NN_逝去:有个bug,我连续点击navigationBar的titleView...,会出现箭头向上缺无展开列表 :scream:
        KevinTing:@NN_逝去 是的,我也发现了,等下修改发到github上
      • wuqh1993:博主 还有一个很重要的问题 :cry: 就是tableView从上往下出现的时候,我的tableView总是覆盖着navgationBar滑下来的。。但是我看你的就是没有覆盖,,看你代码也没找到你这块儿是怎么解决的。。。
        wuqh1993:@KevinTing 博主,我代码给你发过去了 ^_^
        wuqh1993:@KevinTing 好的,谢了!!
        KevinTing:@wuqh_iOS 你把你的代码给我发过来,819931323@qq.com,我晚上回去了给你看看
      • wuqh1993:博主 ,为什么我给tableView修改约束,没有调用layoutIfNeed ,他就直接把位置给改变了。。。
        KevinTing:@wuqh_iOS layoutIfNeed是不是放置地方不对?比如table的目前位置是A,想动画到B,那么动画代码应该是设置table的constraint为B位置,然后【uiview animateWithDuration】block中layoutIfNeed
        wuqh1993:@KevinTing 可是 我明明UIView 动画里面加了[self.tableView layoutIfNedd] ,结果还是没有动画。。以前都遇到过这种问题。。。一直没有解决过。。。
        KevinTing:@wuqh_iOS 因为View在加载过程中会自动调用这个方法,一般的如果只使用约束来布局的话,不需要手动调用这个方法,但是如果需要动画的时候,layoutIfNeed可以手动触发更新布局
      • 大萌哥哥:写的很棒 :smile:
        KevinTing:@大萌哥哥 谢谢 :smile:

      本文标题:使用Masonry创建一个下拉菜单

      本文链接:https://www.haomeiwen.com/subject/sijacttx.html