用 Auto Layout 等距排列元件

本來以為 Auto Layout 可以用類似 @"|-[view1(viewWidth)]-space-[view2(viewWidth)]-space-[view3(viewWidth)]-|" 這樣的語法自動把三個等寬的 view 等距排列的,沒想到沒辦法。要達到這個效果的話,一個做法是把每個 space 真的塞一個空白的 view 進去,調整這個 view 的寬度,就像 @"|-[view1(viewWidth)]-[space1]-[view2(viewWidth)]-[space2(==space1)]-[view3(viewWidth)]-|" ,但這麼做會生出一堆空白 view,怎麼想都很蠢…另外一條路就是針對每個 view 單獨設定 constraint。

constraint 的建構式長這樣:

+ (instancetype)constraintWithItem:(id)view1
                         attribute:(NSLayoutAttribute)attr1
                         relatedBy:(NSLayoutRelation)relation
                            toItem:(id)view2
                         attribute:(NSLayoutAttribute)attr2
                        multiplier:(CGFloat)multiplier
                          constant:(CGFloat)c

如果我們這樣設定:

NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:view1
                                                              attribute:NSLayoutAttributeCenterX
                                                              relatedBy:NSLayoutRelationEqual
                                                                 toItem:superView
                                                              attribute:NSLayoutAttributeCenterX
                                                             multiplier:multiplier
                                                               constant:constant];

就會有如下的效果:

$$view1.centerX = multiplier \times superView.centerX + constant$$

因此我們希望最終得到的公式形式為:

$$view_k.centerX = a_k \times superView.centerX + b_k$$

那麼只要知道 \(a_k\) \(b_k\) 的公式,就可以用來設定每個 view 的 constraint。

透過觀察,可以發現幾件事,首先,\(view_0.centerX\) 的位置就是它寬度的一半:

$$\begin{aligned} view_0.centerX &= \frac{1}{2} \cdot view.width \ &= 0 \cdot superView.centerX + \frac{1}{2} \cdot view.width \end{aligned}$$

另外,\(superView\) 的寬度等於 \(superView.centerX\) 的兩倍,也等於 n 倍的 \(view.width\) 加上 (n-1) 倍的間隔 (\(space\)),因此我們可以把 \(space\) 表示如下:

$$\begin{aligned} superView.width &= superView.centerX \times 2 = n \times view.width + (n-1) \times space \ \Rightarrow space &= \frac{2\cdot{superView.centerX}-n\cdot{view.width}}{n-1} \end{aligned}$$

最後,\(view_k.centerX\) 等於 \(view_0.centerX\) 加上 \(k\) 倍的 \(view.width\) 加 \(k\) 倍的 \(space\):

$$\begin{aligned} view_k.centerX &= view_0.centerX + k \times (view.width + space) \ &= \frac {1}{2} \cdot view.width + k \cdot view.width + k \cdot (\frac{2\cdot{superView.centerX}-n\cdot{view.width}}{n-1}) \ &= \frac {2 \cdot k}{n-1} \times superView.centerX + (\frac {1}{2} + k - \frac{n }{n-1} \cdot k) \times view.width \ &= \frac {2 \cdot k}{n-1} \times superView.centerX + \frac {1} {2} \times (1-\frac {2 \cdot k}{n-1}) \times view.width \end{aligned}$$

這時假設

$$a_k = \frac {2}{n-1} \cdot k$$

就可以得到

$$\begin{aligned} \Rightarrow view_k.centerX &= \frac {2 \cdot k}{n-1} \times superView.centerX + \frac {1} {2} \times (1-\frac {2 \cdot k}{n-1}) \times view.width \ &= a_k \times superView.centerX + \frac {1} {2} (1-a_k) \times view.width \end{aligned}$$

也就是

$$\begin{aligned} multiplier_k &= \frac {2}{n-1} \cdot k \ constant_k &= (1-multiplier_k) \times \frac {1} {2} view.width \end{aligned}$$

寫成 code 可以用這樣的做法:

NSDictionary *views = NSDictionaryOfVariableBindings(view1, view2, view3, view4, view5, view6);
NSArray *subViews = @[view1, view2, view3, view4, view5, view6];
CGFloat viewWidth = 30.0;
NSDictionary *metrics = @{@"viewWidth": @30};
CGFloat multiplier = 2.0 / (subViews.count - 1.0);
CGFloat constantFactor = viewWidth / 2.0;

[superView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[view1(viewWidth)]->=0-[view2(viewWidth)]->=0-[view3(viewWidth)]->=0-[view4(viewWidth)]->=0-[view5(viewWidth)]->=0-[view6(viewWidth)]|"
                                                                 options:NSLayoutFormatAlignAllTop|NSLayoutFormatAlignAllBottom
                                                                 metrics:metrics
                                                                   views:views]];
[superView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[view1(viewWidth)]|"
                                                                  options:0
                                                                  metrics:metrics
                                                                    views:views]];

for (int k=0; k<subViews.count; k++) {
    [superView addConstraint:[NSLayoutConstraint constraintWithItem:subViews[k]
                                                          attribute:NSLayoutAttributeCenterX
                                                          relatedBy:NSLayoutRelationEqual
                                                             toItem:superView
                                                          attribute:NSLayoutAttributeCenterX
                                                         multiplier:k * multiplier
                                                           constant:(1.0 - multiplier * k) * constantFactor]];
}

但是這種做法會讓內部的 subView 貼緊 superView,呈現 @"|[view1]-[view2]-...-[viewk]|" 的形式。如果希望第一個 subView 和最後一個 subView 與 superView 之間有邊界,呈現 @"|-[view1]-[view2]-...-[viewk]-|" 的形式,用一樣的方式推導,可以算出

$$\begin{aligned} multiplier_k &= \frac {2}{n-1} \cdot k \ constant_k &= (1-multiplier_k) \times (\frac {1} {2} view.width + margin) \end{aligned}$$

code 則改為:

NSDictionary *views = NSDictionaryOfVariableBindings(view1, view2, view3, view4, view5, view6);
NSArray *subViews = @[view1, view2, view3, view4, view5, view6];
CGFloat viewWidth = 30.0;
CGFloat margin = 8.0;
NSDictionary *metrics = @{@"viewWidth": @30, @"margin":@8};
CGFloat multiplier = 2.0 / (subViews.count - 1.0);
CGFloat constantFactor = viewWidth / 2.0 + margin;

[superView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-margin-[view1(viewWidth)]->=0-[view2(viewWidth)]->=0-[view3(viewWidth)]->=0-[view4(viewWidth)]->=0-[view5(viewWidth)]->=0-[view6(viewWidth)]-margin-|"
                                                                 options:NSLayoutFormatAlignAllTop|NSLayoutFormatAlignAllBottom
                                                                 metrics:metrics
                                                                   views:views]];
[superView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[view1(viewWidth)]|"
                                                                  options:0
                                                                  metrics:metrics
                                                                    views:views]];

for (int k=0; k<subViews.count; k++) {
    [superView addConstraint:[NSLayoutConstraint constraintWithItem:subViews[k]
                                                          attribute:NSLayoutAttributeCenterX
                                                          relatedBy:NSLayoutRelationEqual
                                                             toItem:superView
                                                          attribute:NSLayoutAttributeCenterX
                                                         multiplier:k * multiplier
                                                           constant:(1.0 - multiplier * k) * constantFactor]];
}

參考來源:PureLayout/NSArray+PureLayout.m

Contents

comments powered by Disqus