在开始这篇文章之前,想必大家都应该使用过Autolayout方式的界面布局,相信大家都有过类似于如下这样的API调用:
[NSLayoutConstraint(item: self.viewA, attribute: .CenterY, relatedBy: .Equal, toItem: self.viewB, attribute: .CenterY, multiplier: 1.0, constant: 0.0)]
抑或是Visual Format Language
NSLayoutConstraint.constraintsWithVisualFormat("|-(leftPadding)-[imageView(imageViewWidth)]-(rigntPadding)-[labelA]-(4)-[labelB]-(>=44)-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: methics, views: views)
这种冗长而又晦涩的代码,真是恶心人啊。因此在Github上涌现了一大堆简化布局的开源库,如SnapKit, Mansory以及今天我们要说的PureLayout。
在这之中,PureLayout是最轻量级的,它仅仅是对Autolayout现成的语法进行了一层封装,相较于Mansory引入的一些新概念,Purelayout更直接易懂。
源码解析
Purelayout的源码基本没什么难懂的地方,我们首先来看一下其项目结构:
- PurelayoutDefines.h
- ALView + PureLayout.h/.m
- NSArray + PureLayout.h/.m
- NSLayoutConstraint + Purelayout.h/.m
PurelayoutDefines
首先从PurelayoutDefines上入手,这个文件主要是进行一些类似Domain Specific Language定义的转化,如:
typedef NS_ENUM(NSInteger, ALEdge) {
/** The left edge of the view. */
ALEdgeLeft = NSLayoutAttributeLeft,
/** The right edge of the view. */
ALEdgeRight = NSLayoutAttributeRight,
/** The top edge of the view. */
ALEdgeTop = NSLayoutAttributeTop,
/** The bottom edge of the view. */
ALEdgeBottom = NSLayoutAttributeBottom,
/** The leading edge of the view (left edge for left-to-right languages like English, right edge for right-to-left languages like Arabic). */
ALEdgeLeading = NSLayoutAttributeLeading,
/** The trailing edge of the view (right edge for left-to-right languages like English, left edge for right-to-left languages like Arabic). */
ALEdgeTrailing = NSLayoutAttributeTrailing
};
上述这段代码,就是将传统的UIKit中的NSLayoutAttribute的枚举类型全部转换成对应的PureLayout中的定义,如ALEdgeRight对应到NSLayoutAttributeRight。
LayoutMargins
在这里补充一点题外知识,在iOS8中,苹果为Autolayout引入了LayoutMargins这一概念。这个概念乍一听可能都不了解,但是大家回忆下,比如在Storyboard中,我们拖拽一个UIView到ViewController的view并设置边距的时候,上边距和下边距对应的限制都是layout guide,如下图所示:
简单来说,在iOS7上就已经存在了LayoutMargin了,当时的作用是用来限制view的真实内容不会被UINavigationBar(上部)以及UIToolbar(下部)所遮盖。而从iOS8中开始,苹果将这一技术引入到了任意一个UIView中。
ALView + Purelayout
ALView实际上是UIView或者NSView的别名,通过添加ALView的分类,可以通过Define在编译期进行替换,避免为NSView和UIView各创建一份重复的代码。这个类中的API过多,因此我们以轴对齐为典型的例子来分解下源码:
- 轴对齐
在PureLayout中,包括Vertical
,Horizontal
,Baseline
等几种轴对齐方式,其中Baseline指的是View中潜在包含文字的Baseline。
好,我们来看看相关的API
/** Aligns an axis of the view to the same axis of another view. */
- (NSLayoutConstraint *)autoAlignAxis:(ALAxis)axis toSameAxisOfView:(ALView *)otherView;
从该API的名称,我们可以直观的感觉出其作用是用于将两个View按照同一个轴对齐。这个API是一个Convenience Init,其层层传递
- (NSLayoutConstraint *)autoAlignAxis:(ALAxis)axis toSameAxisOfView:(ALView *)otherView
{
return [self autoAlignAxis:axis toSameAxisOfView:otherView withOffset:0.0];
}
- (NSLayoutConstraint *)autoAlignAxis:(ALAxis)axis toSameAxisOfView:(ALView *)otherView withOffset:(CGFloat)offset
{
return [self autoConstrainAttribute:(ALAttribute)axis toAttribute:(ALAttribute)axis ofView:otherView withOffset:offset];
}
最后调用了
- (NSLayoutConstraint *)autoConstrainAttribute:(ALAttribute)attribute toAttribute:(ALAttribute)toAttribute ofView:(ALView *)otherView withOffset:(CGFloat)offset`
好,那就让我们来看看这个上述这个函数的实现,如下所示:
//1.
self.translatesAutoresizingMaskIntoConstraints = NO;
//2.
NSLayoutAttribute layoutAttribute = [NSLayoutConstraint al_layoutAttributeForAttribute:attribute];
NSLayoutAttribute toLayoutAttribute = [NSLayoutConstraint al_layoutAttributeForAttribute:toAttribute];
//3.
NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:self attribute:layoutAttribute relatedBy:relation toItem:otherView attribute:toLayoutAttribute multiplier:1.0 constant:offset];
//4.
[constraint autoInstall];
return constraint;
- 1.首先将translatesAutoresizingMaskIntoConstraints设置为false,对于要使用autolayout的UIView,必须设置为false,也就是不将传统frame布局中的Autoresizing Mask转换成约束。
- 2.根据传入的PureLayout属性转换成对应的NSLayoutAttribute
- 3.调用冗长恶心的Autolayout API构建约束
- 4.添加约束
在这里,我们需要注意一下这个[constraint autoInstall]
,让我们来探一探实现:
- (void)autoInstall
{
// 1. iOS8+
#if __PureLayout_MinBaseSDK_iOS_8_0 || __PureLayout_MinBaseSDK_OSX_10_10
if ([self respondsToSelector:@selector(setActive:)]) {
[NSLayoutConstraint al_applyGlobalStateToConstraint:self];
// 1.1
if ([NSLayoutConstraint al_preventAutomaticConstraintInstallation]) {
[[NSLayoutConstraint al_currentArrayOfCreatedConstraints] addObject:self];
} else {
// 1.2
self.active = YES;
}
return;
}
#endif /* __PureLayout_MinBaseSDK_iOS_8_0 || __PureLayout_MinBaseSDK_OSX_10_10 */
// 2. iOS 7
NSAssert(self.firstItem || self.secondItem, @"Can't install a constraint with nil firstItem and secondItem.");
if (self.firstItem) {
if (self.secondItem) {
NSAssert([self.firstItem isKindOfClass:[ALView class]] && [self.secondItem isKindOfClass:[ALView class]], @"Can only automatically install a constraint if both items are views.");
ALView *commonSuperview = [self.firstItem al_commonSuperviewWithView:self.secondItem];
[commonSuperview al_addConstraint:self];
} else {
NSAssert([self.firstItem isKindOfClass:[ALView class]], @"Can only automatically install a constraint if the item is a view.");
[self.firstItem al_addConstraint:self];
}
} else {
NSAssert([self.secondItem isKindOfClass:[ALView class]], @"Can only automatically install a constraint if the item is a view.");
[self.secondItem al_addConstraint:self];
}
}
整个实现的部分被一分为二,上半部分专门针对iOS8+的,下半部分针对iOS7(事实上在整个PureLayout的设计中,大部分地方的处理方式都一分为二了)
我们暂时也不管al_applyGlobalStateToConstraint:self
以及 al_preventAutomaticConstraintInstallation
的作用,我们从1.2看起。
- 在iOS8上,启用或者禁用一个AutoLayout的Constraint变得更加容易了,仅仅需要设置active即可
- 在iOS7上,需要手动的addConstraint或者removeConstraint
- 在处理iOS7的逻辑当中,需要判断当前这个Constraint是否是针对两个Item的,如果是,找到他们的公共父View,在父View在添加约束,比如添加View A和View B之间的间距;而如果是单一一个View,比如是设置高度或者宽度的,直接在当前View添加即可。
- 通过调用
al_addConstraint
进行约束实际的添加。
而al_addConstraint的实现则如下所示:
[NSLayoutConstraint al_applyGlobalStateToConstraint:constraint];
if ([NSLayoutConstraint al_preventAutomaticConstraintInstallation]) {
[[NSLayoutConstraint al_currentArrayOfCreatedConstraints] addObject:constraint];
} else {
[self addConstraint:constraint];
}
这里又出现了al_applyGlobalStateToConstraint:constraint
以及al_preventAutomaticConstraintInstallation
了,这次我们可不能再躲着它了,赶紧瞧一瞧。
首先是al_applyGlobalStateToConstraint:constraint
,这个参数对应的是一个全局静态变量,用于判断:
if ([NSLayoutConstraint al_isExecutingPriorityConstraintsBlock]) {
constraint.priority = [NSLayoutConstraint al_currentGlobalConstraintPriority];
}
而这个al_isExecutingPriorityConstraintsBlock
则是用于如下这个函数:
+ (void)autoSetPriority:(ALLayoutPriority)priority forConstraints:(ALConstraintsBlock)block
{
NSAssert(block, @"The constraints block cannot be nil.");
if (block) {
[[self al_globalConstraintPriorities] addObject:@(priority)];
block();
[[self al_globalConstraintPriorities] removeLastObject];
}
}
这里可能大家有点晦涩,主要在于PureLayout对于给Constraint设置Priority定义了一个Block-based的方法,也就是autoSetPriority
。在回调的Block中,可以对多个Constraint设置同一个大小的Priority。(其实我也不是很理解这个集体加Priority设计的目的)
不过需要有一点可以肯定的是,设置Constraint的Priority的时机一定要在addConstraint
或者active = true
之前
而对于al_preventAutomaticConstraintInstallation
这个变量,作者在API中描述了如下一段话:
Creates all of the constraints in the block, then installs (activates) them all at once.
All constraints created from calls to the PureLayout API in the block are returned in a single array.
This may be more efficient than installing (activating) each constraint one-by-one.
简而言之,一次性添加所有约束(实际上调用了UIKit的APIactivateConstraints
),比一个个添加要有效率。然而,Purelayout的这个特性对于iOS7来说,用不上,只能通过addConstraint
一个个装,哈哈,么么哒
NSArray + Purelayout
说完了ALView的layout,我们接下来说说另外的NSArray + Purelayout。顾名思义,该分类的主要目的就是给一个NSArray中的所有UIView添加约束。
比如这个API:
- (__NSArray_of(NSLayoutConstraint *) *)autoDistributeViewsAlongAxis:(ALAxis)axis
alignedTo:(ALAttribute)alignment
withFixedSpacing:(CGFloat)spacing
insetSpacing:(BOOL)shouldSpaceInsets
matchedSizes:(BOOL)shouldMatchSizes
其实现如下:
NSAssert([self al_containsMinimumNumberOfViews:1], @"This array must contain at least 1 view to distribute.");
//1. 第一部分
ALDimension matchedDimension;
ALEdge firstEdge, lastEdge;
switch (axis) {
case ALAxisHorizontal:
case ALAxisBaseline: // same value as ALAxisLastBaseline
#if __PureLayout_MinBaseSDK_iOS_8_0
case ALAxisFirstBaseline:
#endif /* __PureLayout_MinBaseSDK_iOS_8_0 */
matchedDimension = ALDimensionWidth;
firstEdge = ALEdgeLeading;
lastEdge = ALEdgeTrailing;
break;
case ALAxisVertical:
matchedDimension = ALDimensionHeight;
firstEdge = ALEdgeTop;
lastEdge = ALEdgeBottom;
break;
default:
NSAssert(nil, @"Not a valid ALAxis.");
return nil;
}
CGFloat leadingSpacing = shouldSpaceInsets ? spacing : 0.0;
CGFloat trailingSpacing = shouldSpaceInsets ? spacing : 0.0;
//2. 第二部分
__NSMutableArray_of(NSLayoutConstraint *) *constraints = [NSMutableArray new];
ALView *previousView = nil;
for (id object in self) {
if ([object isKindOfClass:[ALView class]]) {
ALView *view = (ALView *)object;
view.translatesAutoresizingMaskIntoConstraints = NO;
if (previousView) {
// Second, Third, ... View
[constraints addObject:[view autoPinEdge:firstEdge toEdge:lastEdge ofView:previousView withOffset:spacing]];
if (shouldMatchSizes) {
[constraints addObject:[view autoMatchDimension:matchedDimension toDimension:matchedDimension ofView:previousView]];
}
[constraints addObject:[view al_alignAttribute:alignment toView:previousView forAxis:axis]];
}
else {
// First view
[constraints addObject:[view autoPinEdgeToSuperviewEdge:firstEdge withInset:leadingSpacing]];
}
previousView = view;
}
}
if (previousView) {
// Last View
[constraints addObject:[previousView autoPinEdgeToSuperviewEdge:lastEdge withInset:trailingSpacing]];
}
return constraints;
- 这个API的目的是将一组UIView按照Spacing间距进行均分,同时每个UIView的宽度或者高度保持一致。
- 第一部分是根据传入的轴,进行判断,是在竖直方向均分还是水平方向均分,同时影响的还有是宽度一致还是高度一致。
- 第二部分是根据传入的轴(比如水平方向),将前一个View的右边距和后一个View的左边距添加间距,循环添加,直至最后一个View的右边距和父View的右边距添加完成约束。
其他方面,这个分类的作用基本和ALView + PureLayout一致
,也就不再重复解释了。
至此,PureLayout的源码解析基本上差不多了,其余类似于边对齐的API,如:
- (NSLayoutConstraint *)autoPinEdge:(ALEdge)edge toEdge:(ALEdge)toEdge ofView:(ALView *)otherView;
又或者是约束尺寸的,如:
- (__NSArray_of(NSLayoutConstraint *) *)autoSetDimensionsToSize:(CGSize)size;
都大同小异,在此就不一一赘述了。
最后,强调一点:
- PureLayout必须在主线程使用,其本身实现非常依赖于静态的全局变量。