iOS中动画操作多个UIView的两种不同思路

前段时间在网上看到一篇关于AutoLayout 约束动画方法的文章:《Animating Autolayout Constraints》,译文诙谐幽默,写得很不错。但是初次看完此文章,有种奇怪的感觉:虽然主旨是在描述怎样在runtime处理约束,但是总感觉这种方法非常的“离奇”,有化简为繁多此一举之嫌。可是既然Apple提供了这种方法,应该有比较理想的使用场景。所以看完之后仍然不断回味,然后突然就想到到了一种合适的场景能够完美的解释这种看似“无聊”的方法。这也为我们将来处理类似的需求时多了一种思路。这里我把上述文章和我自己的思路总结出来,给大家一个参考。欢迎看过的朋友讨论。

首先,我利用原文中的例子,在这里给大家介绍下达到同样的效果,有哪两种不同的思路。先看预期效果:

statictable.gif

当打开开关时,蓝色的View会从右边推入到黄色的View右边,然后两个View会均分屏幕并列排列。两者之间的Spacing是10 pt。
注意这里有个小细节,蓝色的View在推入时,是由远及近的,并不是一直都是挨在黄色的View旁边10pt的地方。

好,这个效果实现起来,我们一般会:

思路一:直接操作两个View的Frame:

(以下示例以IB操作为例,如果不用IB也是一样的思路)

1)IB中View的初始化

首先在IB中画好黄色的View:


InterfaceBuilder.png
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。那好,我们来考虑下面这个场景:

ASample.gif

当滑动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;

IB.png

这个时候,头文件里只需要有这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来得简单的多。

如果你还不服气,那么我们思考下这个场景:


Sample2.gif

这里,我使用中间红色方块的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好,所有的技术都是工具,这里只是利用这个例子给大家拓宽思路,在某些场合下,退一步换个思路,从约束的角度去考虑问题可能能带来意想不到的效果。

希望能帮到您。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,009评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,808评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,891评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,283评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,285评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,409评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,809评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,487评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,680评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,499评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,548评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,268评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,815评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,872评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,102评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,683评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,253评论 2 341

推荐阅读更多精彩内容