前段時(shí)間在網(wǎng)上看到一篇關(guān)于AutoLayout 約束動畫方法的文章:《Animating Autolayout Constraints》,譯文詼諧幽默,寫得很不錯(cuò)。但是初次看完此文章,有種奇怪的感覺:雖然主旨是在描述怎樣在runtime處理約束,但是總感覺這種方法非常的“離奇”,有化簡為繁多此一舉之嫌??墒羌热籄pple提供了這種方法,應(yīng)該有比較理想的使用場景。所以看完之后仍然不斷回味,然后突然就想到到了一種合適的場景能夠完美的解釋這種看似“無聊”的方法。這也為我們將來處理類似的需求時(shí)多了一種思路。這里我把上述文章和我自己的思路總結(jié)出來,給大家一個(gè)參考。歡迎看過的朋友討論。
首先,我利用原文中的例子,在這里給大家介紹下達(dá)到同樣的效果,有哪兩種不同的思路。先看預(yù)期效果:

當(dāng)打開開關(guān)時(shí),藍(lán)色的View會從右邊推入到黃色的View右邊,然后兩個(gè)View會均分屏幕并列排列。兩者之間的Spacing是10 pt。
注意這里有個(gè)小細(xì)節(jié),藍(lán)色的View在推入時(shí),是由遠(yuǎn)及近的,并不是一直都是挨在黃色的View旁邊10pt的地方。
好,這個(gè)效果實(shí)現(xiàn)起來,我們一般會:
思路一:直接操作兩個(gè)View的Frame:
(以下示例以IB操作為例,如果不用IB也是一樣的思路)
1)IB中View的初始化
首先在IB中畫好黃色的View:

2)Code創(chuàng)建另一個(gè)View
在Code中動態(tài)創(chuàng)建藍(lán)色的View:
- (void)viewDidLoad {
[super viewDidLoad];
self.blueView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 360, 360)];
self.blueView.backgroundColor = [UIColor blueColor];
[self.view addSubview:self.blueView];
self.blueView.hidden = YES;
}
-(void)viewDidLayoutSubviews
{
float blueViewx = self.yellowView.frame.origin.x + self.yellowView.frame.size.width + 100;
float blueViewy = self.yellowView.frame.origin.y;
self.blueView.frame = CGRectMake(blueViewx, blueViewy, 360, 360);
self.blueView.hidden = NO;
}
當(dāng)然,我這里為了演示方便,用了360這樣的magic number,而且是在viewDidLoad的時(shí)候就把blueView創(chuàng)建好了,在實(shí)際開發(fā)中,你完全可以根據(jù)實(shí)際需要對blueView進(jìn)行l(wèi)azy load。
注意:這里blueView的位置在viewDidLayoutSubViews中進(jìn)行處理而不是在viewDidLoad中的原因是,blueView的初始狀態(tài)時(shí)必須要和yellowView處在同一個(gè)水平線上,也就是說必須等到y(tǒng)ellowView的layout完全確定以后。而在viewDidLoad時(shí),yellowView的layout并沒有完全確定。當(dāng)然你也可以在viewDidAppear中進(jìn)行處理,但是顯然不如在viewDidlayoutSubViews中更合理。
3)動畫處理兩個(gè)View
在Switcher的開關(guān)Action中處理兩個(gè)View的變化:
- (IBAction)fancyMode:(id)sender {
if (self.s.on){
// yellow view縮小
[UIView animateWithDuration:1.0 animations:^{
self.yellowView.frame = CGRectMake(self.yellowView.frame.origin.x,
self.yellowView.frame.origin.y,
(self.yellowView.frame.size.width - 10.0) / 2,
self.yellowView.frame.size.height);
}];
// 計(jì)算blue view frame的x使其靠近
[UIView animateWithDuration:1.0 animations:^{
self.blueView.frame = CGRectMake(self.yellowView.frame.origin.x + self.yellowView.frame.size.width + 10,
self.yellowView.frame.origin.y,
self.yellowView.frame.size.width,
self.yellowView.frame.size.height);
}];
}
else {
// yellow view擴(kuò)展
[UIView animateWithDuration:1.0 animations:^{
self.yellowView.frame = CGRectMake(self.yellowView.frame.origin.x,
self.yellowView.frame.origin.y,
self.yellowView.frame.size.width * 2 + 10.0,
self.yellowView.frame.size.height);
}];
// 計(jì)算blue view frame的x使得距離移開
[UIView animateWithDuration:1.0 animations:^{
self.blueView.frame = CGRectMake(self.yellowView.frame.origin.x + self.yellowView.frame.size.width + 100.0,
self.yellowView.frame.origin.y,
self.blueView.frame.size.width * 2,
self.blueView.frame.size.height);
}];
}
}
好,以上是我們常用的方法,非常直觀簡單。
那么下面來看下譯文中提到的另一個(gè)思路:
思路二:通過兩個(gè)View的Autolayout Constraints來間接操作Frame
這個(gè)思路的核心是對View的變化不是直接修改Frame,而是通過autolayout的設(shè)定,讓UIKit自行處理Frame的layout設(shè)定從而達(dá)到間接操作Frame的效果。同樣是上述需求,我們來看下處理步驟(具體詳情可以參考譯文):
1)創(chuàng)建IB和約束
在IB上拖出視圖,拉上約束。這個(gè)時(shí)候倆視圖都是可見的。

黃圖有五個(gè)約束:左邊相對父視圖間隔,右邊相對藍(lán)圖間隔,上邊相對switch間隔,下邊相對父視圖間隔,以及和藍(lán)圖寬度相等約束。

藍(lán)圖和黃圖的約束差不多,除了藍(lán)圖是右邊相對父視圖間隔。

非必需約束優(yōu)先級
在只有黃圖可見的時(shí)候(真是不錯(cuò)),我們需要加另一個(gè)約束,也就是它右側(cè)相對父視圖的間隔約束。如果在上面我加上這個(gè)約束,那么他就和那個(gè)"右側(cè)相對藍(lán)圖約束"沖突了,因?yàn)樗麄z同時(shí)有優(yōu)先級1000。為了避免沖突以及移動藍(lán)圖,我們可以改變一下黃藍(lán)圖間隔的那個(gè)約束的優(yōu)先級。
必需約束優(yōu)先級是這個(gè)UILayoutPriorityRequired(1000),你不能在運(yùn)行時(shí)改變一個(gè)必需約束的優(yōu)先級。優(yōu)先級比UILayoutPriorityRequired小的,就是一個(gè)可選或者非必需的約束,類似這種,只要你別把優(yōu)先級設(shè)置為UILayoutPriorityRequired,你就可以改。
所以首先,我們把藍(lán)圖右側(cè)相對父視圖約束的優(yōu)先級搞低一點(diǎn),搞到750.

2)關(guān)聯(lián)約束
然后我們在給黃圖加一個(gè)它右側(cè)相對父視圖的約束(就像上面提到的),優(yōu)先級也搞到750.

為了在運(yùn)行時(shí)改變藍(lán)圖右側(cè)約束我們得先把這個(gè)約束拖到代碼中。
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *yellowViewConstraint;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *blueViewConstraint;
為了確保我們把藍(lán)圖推出屏幕,我們也得調(diào)整黃圖和藍(lán)圖中間的間隔約束,所以我們把這個(gè)約束也整到代碼中。
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *viewSpacingContraint;
3) 更新約束
現(xiàn)在可以很容易的寫一個(gè)方法來根據(jù)模式開關(guān)設(shè)置藍(lán)圖約束想要的優(yōu)先級。這里我對原文的代碼做了一些修改:
- (void)updateConstraintsForMode {
if (self.modeSwitch.isOn) {
self.viewSpacingContraint.constant = 10.0;
// ---
// 原文: self.blueViewConstraint.priority = UILayoutPriorityDefaultHigh+1;
// +++
self.yellowViewConstraint.priority = UILayoutPriorityDefaultLow;
self.blueViewConstraint.priority = UILayoutPriorityDefaultHigh;
} else {
self.viewSpacingContraint.constant = 100.0;
// ---
// 原文:self.blueViewConstraint.priority = UILayoutPriorityDefaultHigh-1;
// +++
self.yellowViewConstraint.priority = UILayoutPriorityDefaultHigh;
self.blueViewConstraint.priority = UILayoutPriorityDefaultLow;
}
}
我們在storyboard中把黃圖右側(cè)相對父視圖的約束也設(shè)定了優(yōu)先級UILayoutPriorityDefaultHigh(750)。為了使藍(lán)圖可見,我們需要把藍(lán)圖的右側(cè)約束優(yōu)先級設(shè)定的比750高一些,而隱藏起藍(lán)圖時(shí)我們得把它設(shè)定的低一些。
我們在視圖第一次加載時(shí)也應(yīng)該配置下約束。
- (void)viewDidLoad {
// ...
[self updateConstraintsForMode];
}
4) 約束動畫:
蘋果的 Auto Layout Guide描述了autoLayout搞動畫的基本方法,這樣:
- (IBAction)enableMode:(UISwitch *)sender {
// ...
[self.view layoutIfNeeded];
[UIView animateWithDuration:1.0 animations:^{ [self updateConstraintsForMode];
[self.view layoutIfNeeded];
}];
}
思考
好,到這里,相信很多人會和我有一樣的問題,明明思路1更簡潔直接,為什么偏偏非要使用思路2這么晦澀的方法。更何況有很多人對在IB中使用AutoLayout非常的痛恨厭惡,更是無法接受去操作constraints來間接操作Frame。那好,我們來考慮下面這個(gè)場景:

當(dāng)滑動Slider Bar的時(shí)候,紅View的大小會隨之變化,然后下面的所有View都會跟著一起移動。
那么我們來看這種場景在上述兩種思路下需要怎樣實(shí)現(xiàn)。
由于兩種思路View的初始化狀態(tài)都是一樣的,不同的關(guān)鍵是在Slider變化時(shí)候的操作不同:
思路一:調(diào)整Frame
1)首先在IB中設(shè)定View,做好AutoLayout適配;
2)頭文件中定位:
@property (weak, nonatomic) IBOutlet UIView *redView;
@property (weak, nonatomic) IBOutlet UIView *yellowView;
@property (weak, nonatomic) IBOutlet UIView *blueView;
@property (weak, nonatomic) IBOutlet UIView *greenView;
@property (weak, nonatomic) IBOutlet UISlider *slider;
3)處理Slider Bar的變化:
- (IBAction)spaceChanged:(id)sender {
static float old = 0.0;
float value = self.slider.value;
float delta = value - old;
old = value;
[self updateMainViewWithDelta:delta];
[self updateOtherView:self.yellowView WithDelta:delta];
[self updateOtherView:self.blueView WithDelta:delta];
[self updateOtherView:self.greenView WithDelta:delta];
}
- (void) updateMainViewWithDelta:(float)delta
{
self.redView.frame = CGRectMake(self.redView.frame.origin.x,
self.redView.frame.origin.y,
self.redView.frame.size.width,
self.redView.frame.size.height + delta);
}
- (void) updateOtherView:(UIView *)view WithDelta:(float)delta
{
view.frame = CGRectMake(view.frame.origin.x,
view.frame.origin.y + delta,
view.frame.size.width,
view.frame.size.height);
}
思路二:調(diào)整Constraints
1)首先設(shè)定Constraints:
除了紅色View之外其他的View都有4個(gè)約束:leading space, trailing space, top space, height;紅色View沒有height約束,但是由一個(gè)Bottom space to Bottom Layout;所有constraints priority均為默認(rèn)(1000)。
2)拽出紅色View的Bottom space約束到redViewbottomConstraint;

這個(gè)時(shí)候,頭文件里只需要有這2個(gè)property就可以:
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *redViewbottomConstraint;
@property (weak, nonatomic) IBOutlet UISlider *slider;
3)我們來看spaceChanged:方法里需要怎么寫……
- (IBAction)spaceChanged:(id)sender {
static float old = 0.0;
float value = self.slider.value;
float delta = value - old;
old = value;
self.redViewbottomConstraint.constant -= delta;
}
塔塔??!~~只要1行有木有?!所有東西全部搞定!
可以看到這種場景下方法二的優(yōu)勢沒有?
也許你會說:“不對!方法二還是要花一部分精力去設(shè)定constraints!方法一就不需要!” 可是我會說,如果你用IB去創(chuàng)建View,方法一還是需要花精力在IB中設(shè)定AutoLayout,就算你用code去創(chuàng)建View,方法一還是需要花精力去算每一個(gè)View的初始狀態(tài)(當(dāng)然Github的神器Masonary能夠大大簡化設(shè)定Autolayout的過程)。而且憑心而論,但就這種多個(gè)View的場景,在IB中繪制View顯然要比Code來得簡單的多。
如果你還不服氣,那么我們思考下這個(gè)場景:

這里,我使用中間紅色方塊的width和height constraints,全部代碼只有這些:
// ViewController.h
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *width;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *height;
@property (weak, nonatomic) IBOutlet UISlider *slider;
// ViewController.m
- (IBAction)changed:(id)sender {
self.width.constant = self.slider.value;
self.height.constant = self.slider.value;
}
當(dāng)然,在此之前,需要在IB中設(shè)定好這些View的約束,但是非常簡單,大家自己可以試試,在此不表。
然和你可以思考下如果用常規(guī)的思路一,去調(diào)整每一個(gè)方塊的Frame,需要多少代碼。
總結(jié)
我把原作者提到的一種操作UIView Layout的技術(shù)進(jìn)行引申,總結(jié)了2種操作UIView的方法。
當(dāng)然,我并不是說思路2就一定比思路1好,所有的技術(shù)都是工具,這里只是利用這個(gè)例子給大家拓寬思路,在某些場合下,退一步換個(gè)思路,從約束的角度去考慮問題可能能帶來意想不到的效果。
希望能幫到您。