铲子三方

iOS自动布局框架Masonry的使用及其原理

2018-12-10  本文已影响0人  bertrand319

作者: 温桂龙
部门: 新业务支持研发团队

0、iOS布局的发展史

在iOS发展早期,由于iPhone屏幕大小是固定的,开发者不需要考虑因屏幕大小差异而造成的适配的问题,在开发应用进行布局时,采用的是直接通过代码计算控件在其父控件的位置和大小的方式对UI控件进行布局。

再到后面,在iPad推出后,apple公司推出autoresizing用于指定当UI控件的父控件发生变化时如何调整布局,达到屏幕适配的效果。

到了iOS6推出的时候,iPhone以及iPad的屏幕尺寸逐渐变多,为了能更好地对不同的屏幕进行适配,apple公司推出了基于约束的、描述性的autoLayout布局系统对不同屏幕的iOS设备进行屏幕适配。

在iOS8中,apple公司推出了sizeClass布局系统用以支持更多屏幕大小不一样的iOS设备的屏幕适配。

1、iOS布局方式的比较

代码计算frame

无论怎样的布局方式,最基本的原则都是指定控件的位置与大小。如果屏幕的大小是固定不变的,则只有横屏和竖屏两种情况,通过代码计算出控件的位置即可。frame是指控件在其父控件中的位置和大小,在iOS中,可以使用具体的frame初始化view,view被创建后也可以对frame进行修改。

frame示例:

//创建一个view,其相对父控件左边距为10、上边距为10;宽高均为10
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(10, 10, 10, 10)];

//修改view的frame,左、上边距均改为15;宽高分别改为20和30
view.frame = CGRectMake(15, 15, 20, 30);

使用直接计算frame的方式,控件的位置与大小都是直接写死的,基本上是没有适配可言的。

autoresizing

使用autoresizing布局时,可以指定view的autoresizingMask属性,即当UI控件的父控件发生变化时如何调整布局的属性,共有六个枚举值。

autoresizingMask属性及autoresizing布局示例:

UIViewAutoresizingNone//默认值,不自动调整布局
UIViewAutoresizingFlexibleLeftMargin //保持右边距不变,调整左边距
UIViewAutoresizingFlexibleWidth//保持左、右边距不变,调整控件的宽度
UIViewAutoresizingFlexibleRightMargin//保持左边距不变,调整右边距,
UIViewAutoresizingFlexibleTopMargin//保持下边距不变,调整上边距
UIViewAutoresizingFlexibleHeight//保持上、下边距不变,调整控件高度
UIViewAutoresizingFlexibleBottomMargin//保持上边距不变,调整下边距,

UIView *view = [[UIView alloc] init];
//父控件变化时,保持宽度与高度不变,自动调整与父控件的左右边距与上下边距
view.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;

使用autoresizing进行布局,控件调整布局依赖于父控件的布局变化,所以只适用于描述父子控件之间的调整关系,而不适用于描述同一层级之间的控或者没有层级关系的控件之间的关系。使用autoresizing,对屏幕适配而言,依然有比较大的局限性。

autoLayout

autoLayout的主要概念是参照与约束。它关注的不是控件位置与大小的具体数值,而是关注控件属性参照另一个控件的属性的约束关系,该约束关系一般是线性关系。

autoLayout示例:

UIView *view = [[UIView alloc] init];
NSLayoutConstraint *lc =[NSLayoutConstraint constraintWithItem:anotherView//被约束的控件
                                                     attribute:NSLayoutAttributeLeft
                                                     relatedBy:NSLayoutRelationEqual
                                                        toItem:view//参照控件
                                                     attribute:NSLayoutAttributeLeft
                                                    multiplier:2//线性关系倍数
                                                      constant:0];//乘以倍数后需要加的数值

[view addConstraint:lc];//添加约束到参照控件

添加约束的规则:添加到其最近的父控件。

autoLayout约束属性:

NSLayoutAttributeLeft,
NSLayoutAttributeRight,
NSLayoutAttributeTop,
NSLayoutAttributeBottom,
NSLayoutAttributeLeading,
NSLayoutAttributeTrailing,
NSLayoutAttributeWidth,
NSLayoutAttributeHeight,
NSLayoutAttributeCenterX,
NSLayoutAttributeCenterY,
NSLayoutAttributeLastBaseline,

NSLayoutAttributeBaseline NS_SWIFT_UNAVAILABLE("Use 'lastBaseline' instead") = NSLayoutAttributeLastBaseline,

NSLayoutAttributeFirstBaseline NS_ENUM_AVAILABLE_IOS(8_0),


NSLayoutAttributeLeftMargin NS_ENUM_AVAILABLE_IOS(8_0),
NSLayoutAttributeRightMargin NS_ENUM_AVAILABLE_IOS(8_0),
NSLayoutAttributeTopMargin NS_ENUM_AVAILABLE_IOS(8_0),
NSLayoutAttributeBottomMargin NS_ENUM_AVAILABLE_IOS(8_0),
NSLayoutAttributeLeadingMargin NS_ENUM_AVAILABLE_IOS(8_0),
NSLayoutAttributeTrailingMargin NS_ENUM_AVAILABLE_IOS(8_0),
NSLayoutAttributeCenterXWithinMargins NS_ENUM_AVAILABLE_IOS(8_0),
NSLayoutAttributeCenterYWithinMargins NS_ENUM_AVAILABLE_IOS(8_0),

NSLayoutAttributeNotAnAttribute = 0

使用autoLayout进行布局,不再关注控件的位置与大小的具体数值,而是可以通过描述控件的约束条件对控件进行调整,可以设置约束的属性相当的全面,而且对于不同层级的控件,只需要将约束添加到其最近的父控件即可,这就使得使用autoLayout进行布局,可以很方便地进行屏幕适配工作。但是可以看出,使用纯代码布局的话,autoLayout添加一个约束的代码量将会比较多。这也是使得基于autoLayout的第三方布局框架Masonry流行的原因之一。

sizeClass

在iOS8中推出的sizeClass,将屏幕的宽高抽象为三种 ,即所有的设备分为3*3共9种

通过指定屏幕的宽高类型,就不再需要根据屏幕的具体尺寸去进行适配,甚至也不再有横竖屏的概念了。但是sizeClass只是对屏幕的宽高进行了分类,舍弃了具体尺寸的概念,具体的适配工作仍然需要autoLayout来实现。

小结

计算frame是最原始也是最直接的布局方式,它直接指定来控件的大小和位置,而这也是布局的根本所在,无论autoresizing或autoLayout最终也只是为了确定在不同大小的屏幕下控件的大小和位置。直接计算frame的方式或autoresizing,在iOS设备越来越多的现状下显然难以优雅地解决屏幕适配问题。而无论是手写代码布局,或者是使用Xib、Stroyboard等可视化布局,autoLayout都是目前比较好选择。但是autoLayout亦存在代码重复且代码量比较多的问题。而基于autoLayout的Masonry框架是对原生autoLayout的一种优化,可以使autoLayout用起来变得相对简洁优雅。本文后面的内容主要探讨的也是Masonry框架的使用及其原理。

2、Masonry的基本使用

项目中使用Masonry:

在项目的podfile文件中添加Masonry,然后相应地pod install即可。

target 'Demo' do
    platform :ios, '8.0'
    project 'Demo.xcodeproj'
        
    pod 'Masonry'
        
end
Masonry布局的基本例子
UIView *redView = [[UIView alloc] init];
redView.backgroundColor = [UIColor redColor];
self.redView = redView;
[self.view addSubview:redView];
[redView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.top.bottom.mas_equalTo(0);//上下边距均为0
    make.centerX.mas_equalTo(self.view.mas_centerX);//X轴中心对齐self.view的X轴中心。
    make.width.mas_equalTo(self.view.frame.size.width/3.0);//宽度为self.view的宽度1/3。
}];

UIView *greenView = [[UIView alloc] init];
greenView.backgroundColor = [UIColor greenColor];
self.greenView = greenView;
[self.view addSubview:greenView];
[greenView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.left.top.bottom.mas_equalTo(0);
    make.right.mas_equalTo(redView.mas_left);
}];

UIView *blueView = [[UIView alloc] init];
blueView.backgroundColor = [UIColor blueColor];
self.blueView = blueView;
[self.view addSubview:blueView];
[blueView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.right.top.bottom.mas_equalTo(0);
    make.left.mas_equalTo(redView.mas_right);
}];

上面代码定义一个redView,上下边距为0,宽度为self.view.frame即页面宽度的1/3,水平居中。redView左边和右边的其余空间分别为一个greenView和一个blueView,这时就可以以redView为基准约束greenView的右边与blueView的左边。

make.right.mas_equalTo(redView.mas_left)//约束(greenView的)right对齐redView的left。
make.left.mas_equalTo(redView.mas_right)//约束(blueView的)left对齐redView的right。

同时,Masonry支持链式调用,make.top.bottom.mas_equalTo(0)等同于

make.top.mas_equalTo(0)

make.bottom.mas_equalTo(0)

1.jpg
Masonry框架主要约束
mas_left
mas_top
mas_right
mas_bottom
mas_leading
mas_trailing
mas_width
mas_height
mas_centerX
mas_centerY
mas_baseline
- (MASViewAttribute *(^)(NSLayoutAttribute))mas_attribute

#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100)

mas_firstBaseline
mas_lastBaseline

#endif

#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000)

mas_leftMargin
mas_rightMargin
mas_topMargin
mas_bottomMargin
mas_leadingMargin
mas_trailingMargin
mas_centerXWithinMargins
mas_centerYWithinMargins

#endif

#if (__IPHONE_OS_VERSION_MAX_ALLOWED >= 110000) || (__TV_OS_VERSION_MAX_ALLOWED >= 110000)

mas_safeAreaLayoutGuide
mas_safeAreaLayoutGuideTop
mas_safeAreaLayoutGuideBottom
mas_safeAreaLayoutGuideLef
mas_safeAreaLayoutGuideRight

#endif

Masonry框架的主要约束是对NSLayoutAttribute的封装,与autoLayout的约束基本是一致的,但是由于Masonry的封装,在使用时,代码量变得更少,而且更加的简练直观。

3、Masonry部分源码分析

添加约束时,调用的为mas_makeConstraints,参数^(MASConstraintMaker *make) 为一个block,

[redView mas_makeConstraints:^(MASConstraintMaker *make) { }];

在View+MASAdditions.m中,可以看到mas_makeConstraints的实现,主要是MASConstraintMaker的声明与install工作

- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    block(constraintMaker);
    return [constraintMaker install];
}

这里block(constraintMaker)的block,就是前面传入的block,要搞懂mas_makeConstraints干了什么,我们先看block中做了些什么,前面为redView添加约束时是这么写的:

[redView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.bottom.mas_equalTo(0);
        make.centerX.mas_equalTo(self.view.mas_centerX);
        make.width.mas_equalTo(self.view.frame.size.width/3.0);
    }];

暂且先不管链式调用的原理,这里调用了top、bottom、mas_equalTo、centerX、width等方法,以非链式调用的思路分析Masonry的源码。

- (MASConstraint *)top {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTop];
}
- (MASConstraint *)centerX {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeCenterX];
}
- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];
    MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
    if ([constraint isKindOfClass:MASViewConstraint.class]) {
        //replace with composite constraint
        NSArray *children = @[constraint, newConstraint];
        MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
        compositeConstraint.delegate = self;
        [self constraint:constraint shouldBeReplacedWithConstraint:compositeConstraint];
        return compositeConstraint;
    }
    if (!constraint) {
        newConstraint.delegate = self;
        [self.constraints addObject:newConstraint];
    }
    return newConstraint;
}

top、bottom、centerX、width等方法都是类似的,调用addConstraintWithLayoutAttribute,传入对应的NSLayoutAttribute属性。在addConstraintWithLayoutAttribute中,通过initWithFirstViewAttribute创建一个带有firstViewAttribute的MASViewConstraint对象newConstraint,由于传入的constraint为nil ,所以执行的为将约束添加到self.constraints,并返回newConstraint。

再看mas_equalTo方法:

#define mas_equalTo(...)                 equalTo(MASBoxValue((__VA_ARGS__)))
- (MASConstraint * (^)(id))equalTo {
    return ^id(id attribute) {
        return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
    };
}
- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
    return ^id(id attribute, NSLayoutRelation relation) {
        if ([attribute isKindOfClass:NSArray.class]) {
            NSAssert(!self.hasLayoutRelation, @"Redefinition of constraint relation");
            NSMutableArray *children = NSMutableArray.new;
            for (id attr in attribute) {
                MASViewConstraint *viewConstraint = [self copy];
                viewConstraint.layoutRelation = relation;
                viewConstraint.secondViewAttribute = attr;
                [children addObject:viewConstraint];
            }
            MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
            compositeConstraint.delegate = self.delegate;
            [self.delegate constraint:self shouldBeReplacedWithConstraint:compositeConstraint];
            return compositeConstraint;
        } else {
            NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @"Redefinition of constraint relation");
            self.layoutRelation = relation;
            self.secondViewAttribute = attribute;
            return self;
        }
    };
}

这里mas_equalTo做的做核心的事情是保证之前未设置过relation的前提下把NSLayoutRelationEqual和参数attribute传入了对应的layoutRelation 和secondViewAttribute中。secondViewAttribute中包含item和attribute,这些参数将会在后面调用autoLayout时被用到。

再回过头来看MASConstraintMaker的install方法:

- (NSArray *)install {
    if (self.removeExisting) {
        NSArray *installedConstraints = [MASViewConstraint installedConstraintsForView:self.view];
        for (MASConstraint *constraint in installedConstraints) {
            [constraint uninstall];
        }
    }
    NSArray *constraints = self.constraints.copy;
    for (MASConstraint *constraint in constraints) {
        constraint.updateExisting = self.updateExisting;
        [constraint install];
    }
    [self.constraints removeAllObjects];
    return constraints;
}

这里的install方法的实现,除了基本的逻辑处理,最终通过[constraint install]调用MASConstraint的install方法。MASConstraint为一个接口,在非链式调用时,我们这里用到的它的实现类为MASViewConstraint。

MASViewConstraint的install方法的代码:

- (void)install {
    if (self.hasBeenInstalled) {
        return;
    }
    
    if ([self supportsActiveProperty] && self.layoutConstraint) {
        self.layoutConstraint.active = YES;
        [self.firstViewAttribute.view.mas_installedConstraints addObject:self];
        return;
    }
    
    //获取firstLayoutItem、firstLayoutAttribute、secondLayoutItem、secondLayoutAttribute
    MAS_VIEW *firstLayoutItem = self.firstViewAttribute.item;
    NSLayoutAttribute firstLayoutAttribute = self.firstViewAttribute.layoutAttribute;
    MAS_VIEW *secondLayoutItem = self.secondViewAttribute.item;
    NSLayoutAttribute secondLayoutAttribute = self.secondViewAttribute.layoutAttribute;
    
    //若不存在secondViewAttribute,则父控件为secondLayoutItem,secontAttribute
    //firstLayoutAttribute保持一致
    if (!self.firstViewAttribute.isSizeAttribute && !self.secondViewAttribute) {
        secondLayoutItem = self.firstViewAttribute.view.superview;
        secondLayoutAttribute = firstLayoutAttribute;
    }
    
    //设置AutoLayout约束
    MASLayoutConstraint *layoutConstraint
        = [MASLayoutConstraint constraintWithItem:firstLayoutItem
                                        attribute:firstLayoutAttribute
                                        relatedBy:self.layoutRelation
                                           toItem:secondLayoutItem
                                        attribute:secondLayoutAttribute
                                       multiplier:self.layoutMultiplier
                                         constant:self.layoutConstant];
    
    layoutConstraint.priority = self.layoutPriority;
    layoutConstraint.mas_key = self.mas_key;
    
    //若存在secondViewAttribute.view,添加约束到最近的父控件
    if (self.secondViewAttribute.view) {
        MAS_VIEW *closestCommonSuperview = [self.firstViewAttribute.view mas_closestCommonSuperview:self.secondViewAttribute.view];
        NSAssert(closestCommonSuperview,
                 @"couldn't find a common superview for %@ and %@",
                 self.firstViewAttribute.view, self.secondViewAttribute.view);
        self.installedView = closestCommonSuperview;
    } else if (self.firstViewAttribute.isSizeAttribute) {//如果size attribute,添加到则自身
        self.installedView = self.firstViewAttribute.view;
    } else {//其他情况添加到父控件
        self.installedView = self.firstViewAttribute.view.superview;
    }


    MASLayoutConstraint *existingConstraint = nil;
    if (self.updateExisting) {//需要更新约束
        existingConstraint = [self layoutConstraintSimilarTo:layoutConstraint];
    }
    if (existingConstraint) {//已存在,替换约束
        // just update the constant
        existingConstraint.constant = layoutConstraint.constant;
        self.layoutConstraint = existingConstraint;
    } else {//其他情况直接添加约束
        [self.installedView addConstraint:layoutConstraint];
        self.layoutConstraint = layoutConstraint;
        [firstLayoutItem.mas_installedConstraints addObject:self];
    }
}

这里最终就到了真正的autoLayout了,调用autoLayout设置约束的主要参数的来源是前面的top、mas_equalTo等方法赋的值。

 MASLayoutConstraint *layoutConstraint
        = [MASLayoutConstraint constraintWithItem:firstLayoutItem
                                        attribute:firstLayoutAttribute
                                        relatedBy:self.layoutRelation
                                           toItem:secondLayoutItem
                                        attribute:secondLayoutAttribute
                                       multiplier:self.layoutMultiplier
                                         constant:self.layoutConstant];

以上就是再非链式调用时,Masonry源码调用方法分析。

4、链式调用原理分析

如果需要将上下边距同时设置为0,我们可以使用

make.top.bottom.mas_equalTo(0)

如果需要将左边对齐anotherView的右边,但同时保持一定的offset,可以使用

make.left.equalTo(anotherView.mas_right).offset(8)

这种用法就是所谓的链式调用。

- (MASConstraint *)top {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTop];
}
@property (nonatomic, strong, readonly) MASConstraint *left;
@property (nonatomic, strong, readonly) MASConstraint *top;
@property (nonatomic, strong, readonly) MASConstraint *right;
@property (nonatomic, strong, readonly) MASConstraint *bottom;
@property (nonatomic, strong, readonly) MASConstraint *leading;
@property (nonatomic, strong, readonly) MASConstraint *trailing;
......

留意一个细节,top等方法,返回值类型是MASConstraint,而MASConstraintMaker中,定义了一系列的属性,也就是说,make.top可以视为top属性的getter方法,而该方法返回的是一个带参数的block,于是可以在block中接受这个参数,再次调用left等其它方法,这个是链式调用的基础。

再看回addConstraintWithLayoutAttribute方法

- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];
    MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
    if ([constraint isKindOfClass:MASViewConstraint.class]) {
        //replace with composite constraint
        NSArray *children = @[constraint, newConstraint];
        MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
        compositeConstraint.delegate = self;
        [self constraint:constraint shouldBeReplacedWithConstraint:compositeConstraint];
        return compositeConstraint;
    }
    if (!constraint) {
        newConstraint.delegate = self;
        [self.constraints addObject:newConstraint];
    }
    return newConstraint;
}

当链式调用时如make.top.left时

if ([constraint isKindOfClass:MASViewConstraint.class]) {
        NSArray *children = @[constraint, newConstraint];
        MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
        compositeConstraint.delegate = self;
         //以新的compositeConstraint替换原来的constraint
        [self constraint:constraint shouldBeReplacedWithConstraint:compositeConstraint];
        return compositeConstraint;
    }
- (id)initWithChildren:(NSArray *)children {
    self = [super init];
    if (!self) return nil;

    _childConstraints = [children mutableCopy];
    for (MASConstraint *constraint in _childConstraints) {
        constraint.delegate = self;
    }

    return self;
}

如果是继续链式调用,如make.top.left.right,则会有

后面继续链式调用,都是继续执行第三步了。

这样就不断保存了链式调用的约束,知道前面所述的block结束。

而MASConstraint接口的另一个实现类MASCompositeConstraint,即是处理链式调用形成的复合约束的类。

- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
    return ^id(id attr, NSLayoutRelation relation) {
        for (MASConstraint *constraint in self.childConstraints.copy) {
            constraint.equalToWithRelation(attr, relation);
        }
        return self;
    };
}

在其equalToWithRelation方法中,对self.childConstraints的约束进行了遍历,并调用了MASConstraint的equalToWithRelation方法进行设置,达到了对通过链式调用添加的各个约束进行设置的效果。

5、总结

本文对代码计算frame、autoresizing、autoLayout、sizeClass这几种布局方式做了简单的概要介绍,并分析了基于autoLayout的Masonry框架源码以及其链式调用的实现。可以看出,Masonry在链式调用、MASConstraint接口等无论在代码规范、设计模式等方面都做得很好,是一个相当优秀的框架。如果是OC纯代码布局的话,使用Masonry框架将会是一个很好的选择。

鉴于水平有限,上文难免会有纰漏之处,希望大家能指出文中纰漏或不足之处。

上一篇下一篇

猜你喜欢

热点阅读