KittenYang

【源代码】手把手教你Autolayout如何使用动画(附类似Passbook效果Demo+详细分析实现原理)

今天来点硬货! 先看我们今天要实现的动画:

好的,在Autolayout没出现以前,也就是iOS6以前,要实现这个效果非常复杂,因为你要实时地去算每个视图的位置,而且还很难保持联动。但是iOS6中出现了Autolayout出现,再来看这个效果就豁然开朗了。而且随着iPhone、iPad的多种尺寸屏幕的趋势,势必推送SizeClass的大面积应用,而SizeClass的使用又和Autolayout密不可分。所以从某种意义上来说,多种尺寸屏幕的来临已经宣告了Autolayout时代的到来。所以,结论就是,iOS开发者必须告别手写代码,忘记frame,彻底拥抱Autolayout了。这也是为什么我之前花了那么多篇文章介绍Autolayout的不同方面使用方法的原因,因为它真的很重要。

今天,我们来讲Autolayout很重要的一方面。我们都知道,既然使用了Autolayout,也就是视图、控件的位置已经被约束(Constraint)限制了。比如一个图片视图(UIImageView)被约束在了离顶端30px,离左侧30px,宽60px,高60px的位置,那么它的位置就已经确定了。无论怎么旋转屏幕或者在什么尺寸的设备上都是在离顶端30px,离左侧30px,宽60px,高60px。这真的很棒,不是吗?但是你肯定发现问题了,如果我们要改变视图的位置或者使用动画(Core Animation)该怎么办呢?以前,我们就是重新设置frame。但是在Autolayout里面已经没有frame这个概念了(不信可以打印frame看一看哦)。所以我们的思路是:将计就计,重写约束(Constraint)

接下来,我们通过实现上面gif中的效果具体看一下怎么使用。

1、使用Storyboard布局初始位置

既然使用Autolayout已经成为不可逆的趋势了,那么我们有必要熟悉一下Storyboard的使用。 首先拖四个View上来:

并且设置好约束,从左到右分别是自上而下的五个视图的约束:

然后分别设置五个View的 tag 值为:0,1,2,3,4.

把五个view连接到代码文件中作为属性:

@property (strong, nonatomic) IBOutlet UIView *view1;
@property (strong, nonatomic) IBOutlet UIView *view2;
@property (strong, nonatomic) IBOutlet UIView *view3;
@property (strong, nonatomic) IBOutlet UIView *view4;
@property (strong, nonatomic) IBOutlet UIView *view5;

定义一个数组并把这5个view存进去,方面后面使用:

@property (nonatomic, strong) NSMutableArray *imageViewList;

在viewDidLoad中用循环把五个view的颜色设好,并分别添加上 UITapGestureRecognizer *tap 这个点击的手势,统一触发-(void)Tap:(UITapGestureRecognizer *)gesture 这个方法。

2.实现点击之后的动画

————2.1 关于手写约束

说到动画,不难联想到

+ (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations

这个方法。就像FLASH视频制作软件一样,我们只要设置好起始帧结束帧就可以了,之间的过程UIView会搞定的。同样的道理,对于Autolayout,我们现在已经设置好了起始约束(就是刚才你在Storyboard中设置的约束),只需要一个结束约束就行了。那么这里就涉及到了手写约束了。

手写约束的基本代码是这样的:

self.animationConstraints =  
[NSLayoutConstraint constraintsWithVisualFormat:VisualFormal
                                     options:0
                                     metrics:nil
                                       views:dic];
[self.view addConstraints:self.animationConstraints];

其中optionsmetrics基本默认,到底什么干嘛用的请自行谷歌。

views后面跟一个数组,用来给约束的对象“取外号”,比如:

NSDictionary *dic = [NSDictionary dictionaryWithObjectsAndKeys:self.view1,@"view0",  
              self.view2,@"view1",
              self.view3,@"view2",
              self.view4,@"view3",
              self.view5,@"view4",nil];

就是分别为view1,view2,view3,view4,view5这五个View取了view0,view1,view2,view3,view4 的别名。后面需要用到。

然后VisualFormal是一个新玩意,它是一个NSString,差不多长这样:

V:[view0(67)]-0-[view1(67)]-0-[view2(67)]-0-[view3(67)]-0-[view4(67)]-0-|  

看着好奇怪是不是,是这样的: *每一个 [] 中表示一个视图或一个控件;V 表示垂直约束,H 表示水平约束,比如 V:[view0(67)] 表示垂直方向上view0高67,H:[view0(67)]表示水平方向上view0宽67。然后两个视图之间的 -0- 的意思就很简单了,意思就是这两个视图之间间隔为0,比如 V:[view0(67)]-0-[view1(67)] 就表示竖直方向上 高为67的view1 紧贴在 高为67的view0的下面,因为间隔为0嘛!最后有个 "|",则表示父视图的边沿,比如 V:[view4(67)]-0-| 的意思就是 高为67的view4紧贴父视图的下边沿。 *

Visual Format Language一开始肯定有点搞不清楚,但其要自己不看例子盲写三四个马上能掌握了。

好,那么下面我先举个例子,举完你自己写一个其他的。比如我们怎么用代码约束写一个之前用Storyboard设置的约束呢? 还记得样子吗:

你能看着图写出这个约束吗?请先别往下看,自己硬着头皮想想,想不出来在看答案。

答案:

V:[view0(67)]-0-[view1(67)]-0-[view2(67)]-0-[view3(67)]-0-[view4(67)]-0-|

当然你可以这直接把这个约束存放到一个NSString里。但不知道你有没有发现,这个约束中是有规律的。基本是 [view0(67)]-0- 循环,所以我们可以这样:

NSString *VisualFormal = @"V:";  
    for (int i = 0; i < self.imageViewList.count; i++) {
        NSString *key = [@"view" stringByAppendingString:[@(i) stringValue]];
        NSString *value = [NSString stringWithFormat:@"[%@(67)]-0-", key];
        VisualFormal = [VisualFormal stringByAppendingString:value];
    }
    VisualFormal = [VisualFormal stringByAppendingString:@"|"];

for循环拼出主体 [view0(67)]-0-[view1(67)]-0-[view2(67)]-0-[view3(67)]-0-[view4(67)]-0- 然后 V: 接上主体,主体再接上 | 就得到想要的约束了。

————2.2 实现单个视图放大的手写约束

现在,我们要实现视图放大的效果了。不用怕,我们只要写出 结束帧 的约束就可以了。 先来看一个 结束帧 的布局:

你看到的只有黑色框内的全屏红色矩形,但其实下面其实还有没有放大的视图,被顶出屏幕了所以你看不到。你能写出这个布局的约束吗?老规则,先自己试着写,想不出再看答案。

约束为:(H 代表屏幕高度)

V:|-0-[view1(H)]-0-[view2(67)]-0-[view3(67)]-0-[view4(67)]-0-[view5(67)]  

类似的还有四种情况:

V:|-(-67)-[view1(67)]-0-[view2(H)]-0-[view3(67)]-0-[view4(67)]-0-[view5(67)]  

V:|-(-67*2)-[view1(67)]-0-[view2(67)]-0-[view3(H)]-0-[view4(67)]-0-[view5(67)]  

V:|-(-67*3)-[view1(67)]-0-[view2(67)]-0-[view3(67)]-0-[view4(H)]-0-[view5(67)]  

V:|-(-67*4)-[view1(67)]-0-[view2(67)]-0-[view3(67)]-0-[view4(67)]-0-[view5(H)]  

从这五个约束中我们可以总结:

1.view1的约束为: -(-67*N)-[view1(67)]
2.点击视图的约束: -0-[viewN(H)]

所以最后 结束帧 是这样:

 [self.view removeConstraints:_animationConstraints];
 NSString *VisualFormal = @"V:|";
 for (int i = 0; i < self.imageViewList.count; ++i) {
     NSString *key = [@"view" stringByAppendingString:[@(i) stringValue]];
     NSString *value = [NSString stringWithFormat:@"-0-[%@(67)]", key];
     if (i == 0) {
         value = [NSString stringWithFormat:@"-(-%ld)-[%@(67)]", 67 * _index,key];
     }
     if (i == _index) {
         value = [NSString stringWithFormat:@"-0-[%@(%f)]", key, MAXHEIGHT];
     }

     VisualFormal = [VisualFormal stringByAppendingString:value];
 }

NSDictionary *dic = [NSDictionary dictionaryWithObjectsAndKeys:self.view1,@"view0",  
              self.view2,@"view1",
              self.view3,@"view2",
              self.view4,@"view3",
              self.view5,@"view4",nil];
 NSLog(@"---------%@",VisualFormal);

 self.animationConstraints =
 [NSLayoutConstraint constraintsWithVisualFormat:VisualFormal
                                         options:0
                                         metrics:nil
                                           views:dic];
 [self.view addConstraints:self.animationConstraints];

到这里,我们已经完成了 结束帧 ,那么要让它自动补间,我们还需要 UIView Animation,这里,千万别忘了

[UIView animateWithDuration:.3f
                 animations:^{
                 //上面的结束帧代码...
                 [self.view layoutIfNeeded];
}];

好了,最后你可以到这里下载完整版源代码。


转载请注明出处,万分感谢!

KittenYang

写写代码,做做设计,看看产品。