前段時間在網(wǎng)上看到一篇關于AutoLayout 約束動畫方法的文章:《Animating Autolayout Constraints》殿怜,譯文詼諧幽默廉涕,寫得很不錯泻云。但是初次看完此文章,有種奇怪的感覺:雖然主旨是在描述怎樣在runtime處理約束狐蜕,但是總感覺這種方法非常的“離奇”宠纯,有化簡為繁多此一舉之嫌〔闶停可是既然Apple提供了這種方法婆瓜,應該有比較理想的使用場景。所以看完之后仍然不斷回味,然后突然就想到到了一種合適的場景能夠完美的解釋這種看似“無聊”的方法廉白。這也為我們將來處理類似的需求時多了一種思路个初。這里我把上述文章和我自己的思路總結(jié)出來,給大家一個參考猴蹂。歡迎看過的朋友討論院溺。
首先,我利用原文中的例子磅轻,在這里給大家介紹下達到同樣的效果珍逸,有哪兩種不同的思路。先看預期效果:
當打開開關時瓢省,藍色的View會從右邊推入到黃色的View右邊弄息,然后兩個View會均分屏幕并列排列。兩者之間的Spacing是10 pt勤婚。
注意這里有個小細節(jié)摹量,藍色的View在推入時,是由遠及近的馒胆,并不是一直都是挨在黃色的View旁邊10pt的地方缨称。
好,這個效果實現(xiàn)起來祝迂,我們一般會:
思路一:直接操作兩個View的Frame:
(以下示例以IB操作為例睦尽,如果不用IB也是一樣的思路)
1)IB中View的初始化
首先在IB中畫好黃色的View:
2)Code創(chuàng)建另一個View
在Code中動態(tài)創(chuàng)建藍色的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;
}
當然,我這里為了演示方便型雳,用了360這樣的magic number当凡,而且是在viewDidLoad的時候就把blueView創(chuàng)建好了,在實際開發(fā)中纠俭,你完全可以根據(jù)實際需要對blueView進行l(wèi)azy load沿量。
注意:這里blueView的位置在viewDidLayoutSubViews中進行處理而不是在viewDidLoad中的原因是,blueView的初始狀態(tài)時必須要和yellowView處在同一個水平線上冤荆,也就是說必須等到y(tǒng)ellowView的layout完全確定以后朴则。而在viewDidLoad時,yellowView的layout并沒有完全確定钓简。當然你也可以在viewDidAppear中進行處理乌妒,但是顯然不如在viewDidlayoutSubViews中更合理。
3)動畫處理兩個View
在Switcher的開關Action中處理兩個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);
}];
// 計算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擴展
[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);
}];
// 計算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);
}];
}
}
好外邓,以上是我們常用的方法撤蚊,非常直觀簡單。
那么下面來看下譯文中提到的另一個思路:
思路二:通過兩個View的Autolayout Constraints來間接操作Frame
這個思路的核心是對View的變化不是直接修改Frame损话,而是通過autolayout的設定拴魄,讓UIKit自行處理Frame的layout設定從而達到間接操作Frame的效果。同樣是上述需求,我們來看下處理步驟(具體詳情可以參考譯文):
1)創(chuàng)建IB和約束
在IB上拖出視圖匹中,拉上約束。這個時候倆視圖都是可見的豪诲。
黃圖有五個約束:左邊相對父視圖間隔顶捷,右邊相對藍圖間隔,上邊相對switch間隔屎篱,下邊相對父視圖間隔服赎,以及和藍圖寬度相等約束。
藍圖和黃圖的約束差不多交播,除了藍圖是右邊相對父視圖間隔重虑。
非必需約束優(yōu)先級
在只有黃圖可見的時候(真是不錯),我們需要加另一個約束秦士,也就是它右側(cè)相對父視圖的間隔約束缺厉。如果在上面我加上這個約束,那么他就和那個"右側(cè)相對藍圖約束"沖突了隧土,因為他倆同時有優(yōu)先級1000提针。為了避免沖突以及移動藍圖,我們可以改變一下黃藍圖間隔的那個約束的優(yōu)先級曹傀。
必需約束優(yōu)先級是這個UILayoutPriorityRequired(1000)辐脖,你不能在運行時改變一個必需約束的優(yōu)先級。優(yōu)先級比UILayoutPriorityRequired小的皆愉,就是一個可選或者非必需的約束嗜价,類似這種,只要你別把優(yōu)先級設置為UILayoutPriorityRequired幕庐,你就可以改久锥。
所以首先,我們把藍圖右側(cè)相對父視圖約束的優(yōu)先級搞低一點翔脱,搞到750.
2)關聯(lián)約束
然后我們在給黃圖加一個它右側(cè)相對父視圖的約束(就像上面提到的)奴拦,優(yōu)先級也搞到750.
為了在運行時改變藍圖右側(cè)約束我們得先把這個約束拖到代碼中。
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *yellowViewConstraint;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *blueViewConstraint;
為了確保我們把藍圖推出屏幕届吁,我們也得調(diào)整黃圖和藍圖中間的間隔約束错妖,所以我們把這個約束也整到代碼中粪躬。
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *viewSpacingContraint;
3) 更新約束
現(xiàn)在可以很容易的寫一個方法來根據(jù)模式開關設置藍圖約束想要的優(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è)相對父視圖的約束也設定了優(yōu)先級UILayoutPriorityDefaultHigh(750)熏纯。為了使藍圖可見贫悄,我們需要把藍圖的右側(cè)約束優(yōu)先級設定的比750高一些香椎,而隱藏起藍圖時我們得把它設定的低一些陨仅。
我們在視圖第一次加載時也應該配置下約束味榛。
- (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。那好哩簿,我們來考慮下面這個場景:
當滑動Slider Bar的時候宵蕉,紅View的大小會隨之變化,然后下面的所有View都會跟著一起移動节榜。
那么我們來看這種場景在上述兩種思路下需要怎樣實現(xiàn)羡玛。
由于兩種思路View的初始化狀態(tài)都是一樣的,不同的關鍵是在Slider變化時候的操作不同:
思路一:調(diào)整Frame
1)首先在IB中設定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)首先設定Constraints:
除了紅色View之外其他的View都有4個約束:leading space, trailing space, top space, height;紅色View沒有height約束,但是由一個Bottom space to Bottom Layout浓若;所有constraints priority均為默認(1000)渺杉。
2)拽出紅色View的Bottom space約束到redViewbottomConstraint;
這個時候挪钓,頭文件里只需要有這2個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;
}
塔塔J窃健!~~只要1行有木有碌上?倚评!所有東西全部搞定!
可以看到這種場景下方法二的優(yōu)勢沒有馏予?
也許你會說:“不對天梧!方法二還是要花一部分精力去設定constraints!方法一就不需要霞丧!” 可是我會說呢岗,如果你用IB去創(chuàng)建View,方法一還是需要花精力在IB中設定AutoLayout蛹尝,就算你用code去創(chuàng)建View后豫,方法一還是需要花精力去算每一個View的初始狀態(tài)(當然Github的神器Masonary能夠大大簡化設定Autolayout的過程)。而且憑心而論突那,但就這種多個View的場景挫酿,在IB中繪制View顯然要比Code來得簡單的多。
如果你還不服氣愕难,那么我們思考下這個場景:
這里早龟,我使用中間紅色方塊的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;
}
當然,在此之前葱弟,需要在IB中設定好這些View的約束壹店,但是非常簡單,大家自己可以試試芝加,在此不表茫打。
然和你可以思考下如果用常規(guī)的思路一,去調(diào)整每一個方塊的Frame妖混,需要多少代碼。
總結(jié)
我把原作者提到的一種操作UIView Layout的技術(shù)進行引申轮洋,總結(jié)了2種操作UIView的方法制市。
當然,我并不是說思路2就一定比思路1好弊予,所有的技術(shù)都是工具祥楣,這里只是利用這個例子給大家拓寬思路,在某些場合下汉柒,退一步換個思路误褪,從約束的角度去考慮問題可能能帶來意想不到的效果。
希望能幫到您碾褂。