前段时间在网上看到一篇关于AutoLayout 约束动画方法的文章:《Animating Autolayout Constraints》,译文诙谐幽默,写得很不错。但是初次看完此文章,有种奇怪的感觉:虽然主旨是在描述怎样在runtime处理约束,但是总感觉这种方法非常的“离奇”,有化简为繁多此一举之嫌。可是既然Apple提供了这种方法,应该有比较理想的使用场景。所以看完之后仍然不断回味,然后突然就想到到了一种合适的场景能够完美的解释这种看似“无聊”的方法。这也为我们将来处理类似的需求时多了一种思路。这里我把上述文章和我自己的思路总结出来,给大家一个参考。欢迎看过的朋友讨论。
首先,我利用原文中的例子,在这里给大家介绍下达到同样的效果,有哪两种不同的思路。先看预期效果:
当打开开关时,蓝色的View会从右边推入到黄色的View右边,然后两个View会均分屏幕并列排列。两者之间的Spacing是10 pt。
注意这里有个小细节,蓝色的View在推入时,是由远及近的,并不是一直都是挨在黄色的View旁边10pt的地方。
好,这个效果实现起来,我们一般会:
思路一:直接操作两个View的Frame:
(以下示例以IB操作为例,如果不用IB也是一样的思路)
1)IB中View的初始化
首先在IB中画好黄色的View:
2)Code创建另一个View
在Code中动态创建蓝色的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创建好了,在实际开发中,你完全可以根据实际需要对blueView进行lazy load。
注意:这里blueView的位置在viewDidLayoutSubViews中进行处理而不是在viewDidLoad中的原因是,blueView的初始状态时必须要和yellowView处在同一个水平线上,也就是说必须等到yellowView的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)创建IB和约束
在IB上拖出视图,拉上约束。这个时候俩视图都是可见的。
黄图有五个约束:左边相对父视图间隔,右边相对蓝图间隔,上边相对switch间隔,下边相对父视图间隔,以及和蓝图宽度相等约束。
蓝图和黄图的约束差不多,除了蓝图是右边相对父视图间隔。
非必需约束优先级
在只有黄图可见的时候(真是不错),我们需要加另一个约束,也就是它右侧相对父视图的间隔约束。如果在上面我加上这个约束,那么他就和那个"右侧相对蓝图约束"冲突了,因为他俩同时有优先级1000。为了避免冲突以及移动蓝图,我们可以改变一下黄蓝图间隔的那个约束的优先级。
必需约束优先级是这个UILayoutPriorityRequired(1000),你不能在运行时改变一个必需约束的优先级。优先级比UILayoutPriorityRequired小的,就是一个可选或者非必需的约束,类似这种,只要你别把优先级设置为UILayoutPriorityRequired,你就可以改。
所以首先,我们把蓝图右侧相对父视图约束的优先级搞低一点,搞到750.
2)关联约束
然后我们在给黄图加一个它右侧相对父视图的约束(就像上面提到的),优先级也搞到750.
为了在运行时改变蓝图右侧约束我们得先把这个约束拖到代码中。
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *yellowViewConstraint;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *blueViewConstraint;
为了确保我们把蓝图推出屏幕,我们也得调整黄图和蓝图中间的间隔约束,所以我们把这个约束也整到代码中。
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *viewSpacingContraint;
3) 更新约束
现在可以很容易的写一个方法来根据模式开关设置蓝图约束想要的优先级。这里我对原文的代码做了一些修改:
- (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中把黄图右侧相对父视图的约束也设定了优先级UILayoutPriorityDefaultHigh(750)。为了使蓝图可见,我们需要把蓝图的右侧约束优先级设定的比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都会跟着一起移动。
那么我们来看这种场景在上述两种思路下需要怎样实现。
由于两种思路View的初始化状态都是一样的,不同的关键是在Slider变化时候的操作不同:
思路一:调整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);
}
思路二:调整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;
}
塔塔!!~~只要1行有木有?!所有东西全部搞定!
可以看到这种场景下方法二的优势没有?
也许你会说:“不对!方法二还是要花一部分精力去设定constraints!方法一就不需要!” 可是我会说,如果你用IB去创建View,方法一还是需要花精力在IB中设定AutoLayout,就算你用code去创建View,方法一还是需要花精力去算每一个View的初始状态(当然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的约束,但是非常简单,大家自己可以试试,在此不表。
然和你可以思考下如果用常规的思路一,去调整每一个方块的Frame,需要多少代码。
总结
我把原作者提到的一种操作UIView Layout的技术进行引申,总结了2种操作UIView的方法。
当然,我并不是说思路2就一定比思路1好,所有的技术都是工具,这里只是利用这个例子给大家拓宽思路,在某些场合下,退一步换个思路,从约束的角度去考虑问题可能能带来意想不到的效果。
希望能帮到您。