你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 新手教程:使用Masonry創建一個下拉式菜單

新手教程:使用Masonry創建一個下拉式菜單

編輯:IOS開發基礎

0.jpg

之前看到一個swift開源項目

https://github.com/PhamBaTho/BTNavigationDropdownMenu

就是一個類似新浪微博的下拉式導航菜單,看看下面的效果。

1.gif

之前看這個項目的時候(大概一個月之前??今天上去看的時候作者已經更新到適配橫豎屏切換了!用的UIViewAutoResizingMask),不能支持橫豎屏切換,這明顯是沒有做布局適配啊,而且沒有Objective-C版本,於是自己用Objective-C重新寫了一個,並且加上Masonry做自動布局適配屏幕切換。當然這裡完全可以用UIViewAutoResizingMask做橫豎屏切換,但是Masonry用起來也很簡單。做一遍下來加深自己對View和自動布局的理解。。。寫下來適合新手看看,高手就繞道吧。不啰嗦了,開始吧。

首先盜用BTNavigationDropdownMenu的圖標元素bundle到我的新建的項目https://github.com/tujinqiu/KTDropdownMenuView下面。。。

1、新建項目,集成UIView創建KTDropdownMenuView。配置CocoaPods。

BTNavigationDropdownMenu

blob.png

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的視圖調試功能,你會發現還是有點卵用的。

blob.png

轉動一下,導航欄上有個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]]
];

運行之後,果然,使我們預料的效果哈

blob.png

細心的會發現我用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。

blob.png

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

blob.png

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往下面拽的時候,上面和導航欄之間會出現灰色背景啊。

blob.png

不能忍。添加一個與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之後。現在動畫還有拖拽不會看到背景了。

blob.png

完整的項目在這裡,https://github.com/tujinqiu/KTDropdownMenuView

歡迎討論交流,批評指正!!!


  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved