注意:我为过渡动画写了两篇文章: 第一篇:[iOS]过渡动画之简单模仿系统,主要分析系统简单的动画实现原理,以及讲解坐标系、绝对坐标系、相对坐标系,坐标系转换等知识,为第二篇储备理论基础。最后实现 Mac 上的文件预览动画。
第二篇:[iOS]过渡动画之高级模仿 airbnb ,主要基于第一篇的理论来实现复杂的界面过渡,包括进入和退出动画的串联。最后将这个动画的实现部分与当前界面解耦,并封装为一个普适(其他类似界面也适用)的工具类。
这两篇文章将会带你学到如何实现下图 airbnb 首页类似的过渡动画,同时最重要的,你将学会怎么分析类似的动画,并且知道如何动手实现。GitHub 地址在这里。
好,准备好了吗?现在开始第一篇。这一篇主要分析系统简单的动画实现原理,以及讲解坐标系、绝对坐标系、相对坐标系,坐标系转换等知识,为第二篇储备理论基础。最后实现 Mac 上的文件预览动画。
01. 系统的过渡动画
我很多时候做一个东西的时候,我会先想一下,我们的老东家苹果有没有做过类似的?如果有,那肯定苹果的更靠谱。看到上面那个 airbnb 动画的时候,我首先想到 Mac 上这个文件预览的动画。
你还能想到 iPhone 上系统自带更多的类似的动画吗?
这个动画应该怎么实现呢?我来描述一下这个过程,你看我说的对不对。
-
首先你要选中这个文件夹,然后当你按下 space 键的时候,会产生一个用来做动画的元素 Object ,Object 从当前选中文件夹的位置开始运动到屏幕中央(终点位置),边运动边放大。这是打开预览的过程。
-
当你再次按下 space 键的时候,当前动画元素 Object 会从屏幕中央运动到你选中的那个文件夹的位置,边运动边缩小。这是关闭预览。
有没有从这个描述中 get 到几个关键点呢?
如果尝试把这些关键点和动画过程串起来,是不是就应该是下面这样?动画开始,先创建用来做动画的元素(是新产生,不是拿到文件夹进行动画,因为你也看到,之前那个文件夹它仍然在那里没有动),然后计算起点位置,在把这个元素添加到起点位置,接下来计算终点位置,然后开始做动画。
02.坐标系、绝对坐标系、相对坐标系,坐标系转换
在实现之前,我们先来复习一下初中物理。
- 这里我们只讨论二维坐标系,因为我们的动画是基于二维坐标系的。
- 如下图,我们有一台 iPhone,它的坐标原点在左上角,就是白色的坐标系,我们物理里面又叫做绝对坐标系,其他的坐标系都是参考它来定位的。
- 在我们的 iPhone 屏幕上有一个红色的矩形,它处在(60,100)的位置上(相对于绝对坐标系),它自身也有一个坐标系,让它体内的元素相对它进行定位,它的坐标系叫做相对坐标系(相对于绝对坐标系的坐标系)。
- 在屏幕中央还有一个绿色的矩形,它相对于红色的矩形定位为(40,60)(相对坐标系的坐标)。
现在我们要计算这个绿色的矩形的绝对坐标,也就是坐标系转换。从下图计算我们可以很快算出这个值为(100, 160)。
03.知道上面这些有什么用?
可能你看到这里会觉得这些都很简单,还用你再说一遍?而且这些好像也没什么用,对吧?
上面说过坐标转换的问题,在实际开发中,我们的视图 View 都是层层嵌套,所以将一个点的 frame 从一个坐标系迁移到另外一个坐标系不可能依赖于我们开发者去手动计算。因为系统需要将视图渲染到屏幕上,所以系统是知道视图关系的。好在系统提供了两个 frame 转换函数。这两个函数都是 UIView 的对象方法。
- (CGRect)convertRect:(CGRect)rect toView:(nullable UIView *)view;
- (CGRect)convertRect:(CGRect)rect fromView:(nullable UIView *)view;
复制代码
- 第一个函数,将一个当前 View 坐标系的 frame 转换为另一个 View 的坐标系上。比如说下图 A 中有个 B,如果要将 B 的 frame 迁移到 C 中,就应该这么写:
CGRect targetFrame = [A convertRect:B.frame toView:C];
复制代码
- 同样的,如果使用第二个函数来实现将 B 的 frame 迁移到 C 中,那就应该这么写:
CGRect targetFrame = [C convertRect:B.frame fromView:A];
复制代码
- 同时需要注意,如果想要把 B 的 frame 迁移到窗口坐标(绝对坐标系,也就是白色的坐标系),那就应该这么写:
CGRect targetFrame = [A convertRect:B.frame toView:window];
CGRect targetFrame = [window convertRect:B.frame fromView:A];
复制代码
或者这么写:
CGRect targetFrame = [A convertRect:B.frame toView:nil]; // 这个函数中,如果传个 nil,则代表窗口 window.
复制代码
理清楚这些坐标转换是很有必要的,因为等会当视图关系变得很复杂的时候,假如不能理清楚,可能你自己都不知道在哪个坐标系,你会觉得明明自己写对了,但是代码跑起来就是错的。如果出现这种情况,还是应该回到起点来,理清楚这些坐标关系。
##04.动手实现
-
首先我们在 Storyborad 中创建一个 UIImageView 用来显示文件夹图标。
-
看一下 @interface 中的属性
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *folderImageView;
/** 动画元素 */
@property(nonatomic, strong)UIImageView *animationImageView;
/** 是否是打开预览动画 */
@property(nonatomic, assign)BOOL isOpenOverView;
@end
复制代码
- 我们肯定需要一个截图工具:
// 将一个 view 进行截图
-(UIImage *)snapImageForView:(UIView *)view{
UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.opaque, 0);
[view.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *aImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return aImage;
}
复制代码
-
然后我们在 touchesBegan 方法中处理动画。大致思路遵循我一开始描述的动画过程。
-
一开始将要做动画的 View 进行截图;
-
再将我们要做动画的 View 的 frame 迁移到窗口坐标系中,作为动画起始位置。为什么要迁移到窗口坐标系而不是其他的坐标系呢?因为我们做动画的元素是添加到窗口上的,并且你需要将所有动画元素的 frame 统一一个坐标系,这样方便我们以最高效的方式管理我们自己创建的元素。
-
计算我们的终点位置,在这个动画里很简单,话不多说。但是在下一个仿 airbnb 的动画里,计算终点 frame 将成为一个挑战(关于你高中数学知识的一个挑战)。
-
添加动画元素一个 UIImageView 到窗口。为什么是 UIImageView 而不是其它呢?很显然我们动画有放大和缩小,所以应该是一个 frame 动画。所以我们应该选择用 UIImageView 来呈现截图的方式来实现动画。
-
最后用一个系统封装的 UIView 动画 block 来处理动画过程。
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// 先将文件夹那个视图进行截图
UIImage *animationImage = [self snapImageForView:self.folderImageView];
// 再将文件夹视图的坐标系迁移到窗口坐标系(绝对坐标系)
CGRect targetFrame_start = [self.folderImageView.superview convertRect:self.folderImageView.frame toView:nil];
// 计算动画终点位置
CGFloat targetW = targetFrame_start.size.width*magnificateMultiple;
CGFloat targetH = targetFrame_start.size.height*magnificateMultiple;
CGFloat targetX = (JPScreenWidth - targetW) / 2.0;
CGFloat targetY =(JPScreenHeight - targetH) / 2.0;
CGRect targetFrame_end = CGRectMake(targetX, targetY, targetW, targetH);
// 添加做动画的元素
if (!self.animationImageView.superview) {
self.animationImageView.image = animationImage;
self.animationImageView.frame = targetFrame_start;
[self.view.window addSubview:self.animationImageView];
}
if (self.isOpenOverView) {
// 预览动画
[UIView animateWithDuration:1 delay:0. options:UIViewAnimationOptionCurveEaseIn animations:^{
self.animationImageView.frame = targetFrame_end;
} completion:^(BOOL finished) {
}];
}
else{
// 关闭预览动画
[UIView animateWithDuration:1 delay:0. options:UIViewAnimationOptionCurveEaseOut animations:^{
self.animationImageView.frame = targetFrame_start;
} completion:^(BOOL finished) {
[self.animationImageView removeFromSuperview];
}];
}
self.isOpenOverView = !self.isOpenOverView;
}
复制代码
很简单,对吧?但是我希望你是理解这个思路以后才觉得简单,而不是仅仅觉得代码实现简单,因为下一篇就没这么简单了。
05.GitHub 地址
GitHub 地址在这里。