[iOS]过渡动画之入门模仿系统

549 阅读9分钟

为了更好的阅读体验, 请前往 简书.

注意:我为过渡动画写了两篇文章:

第一篇:[[iOS]过渡动画之简单模仿系统]([iOS]过渡动画之入门模仿系统),主要分析系统简单的动画实现原理,以及讲解坐标系、绝对坐标系、相对坐标系,坐标系转换等知识,为第二篇储备理论基础。最后实现 Mac 上的文件预览动画。

第二篇:[[iOS]过渡动画之高级模仿 airbnb ]([iOS]过渡动画之高级模仿 airbnb),主要基于第一篇的理论来实现复杂的界面过渡,包括进入和退出动画的串联。最后将这个动画的实现部分与当前界面解耦,并封装为一个普适(其他类似界面也适用)的工具类。

这两篇文章将会带你学到如何实现下图 airbnb 首页类似的过渡动画,同时最重要的,你将学会怎么分析类似的动画,并且知道如何动手实现。[GitHub](newyjp/JPAnimation) 地址在这里。

<img src="https://pic2.zhimg.com/v2-c4a0e3369487936779a9fdc0434c4921_b.png" data-rawwidth="370" data-rawheight="667" class="content_image" width="370">

好,准备好了吗?现在开始第一篇。这一篇主要分析系统简单的动画实现原理,以及讲解坐标系、绝对坐标系、相对坐标系,坐标系转换等知识,为第二篇储备理论基础。最后实现 Mac 上的文件预览动画。


  1. 系统的过渡动画

我很多时候做一个东西的时候,我会先想一下,我们的老东家苹果有没有做过类似的?如果有,那肯定苹果的更靠谱。看到上面那个 airbnb 动画的时候,我首先想到 Mac 上这个文件预览的动画。

你还能想到 iPhone 上系统自带更多的类似的动画吗?

<img src="https://pic3.zhimg.com/v2-c94c58604bd87b6c9a8f9be28e399082_b.png" data-rawwidth="744" data-rawheight="490" class="origin_image zh-lightbox-thumb" width="744" data-original="https://pic3.zhimg.com/v2-c94c58604bd87b6c9a8f9be28e399082_r.png">

这个动画应该怎么实现呢?我来描述一下这个过程,你看我说的对不对。

  • 首先你要选中这个文件夹,然后当你按下 space 键的时候,会产生一个用来做动画的元素 Object ,Object 从当前选中文件夹的位置开始运动到屏幕中央(终点位置),边运动边放大。这是打开预览的过程。

  • 当你再次按下 space 键的时候,当前动画元素 Object 会从屏幕中央运动到你选中的那个文件夹的位置,边运动边缩小。这是关闭预览。

有没有从这个描述中 get 到几个关键点呢?

<img src="https://pic4.zhimg.com/v2-97f6b1dcec649b9718ba07b937730ce3_b.png" data-rawwidth="922" data-rawheight="506" class="origin_image zh-lightbox-thumb" width="922" data-original="https://pic4.zhimg.com/v2-97f6b1dcec649b9718ba07b937730ce3_r.png">

如果尝试把这些关键点和动画过程串起来,是不是就应该是下面这样?动画开始,先创建用来做动画的元素(是新产生,不是拿到文件夹进行动画,因为你也看到,之前那个文件夹它仍然在那里没有动),然后计算起点位置,在把这个元素添加到起点位置,接下来计算终点位置,然后开始做动画。

<img src="https://pic3.zhimg.com/v2-a06572a9a24a832df45f1398b4575da2_b.png" data-rawwidth="1000" data-rawheight="340" class="origin_image zh-lightbox-thumb" width="1000" data-original="https://pic3.zhimg.com/v2-a06572a9a24a832df45f1398b4575da2_r.png">

2. 坐标系、绝对坐标系、相对坐标系,坐标系转换

在实现之前,我们先来复习一下初中物理。

  • 这里我们只讨论二维坐标系,因为我们的动画是基于二维坐标系的。
  • 如下图,我们有一台 iPhone,它的坐标原点在左上角,就是白色的坐标系,我们物理里面又叫做绝对坐标系,其他的坐标系都是参考它来定位的。
  • 在我们的 iPhone 屏幕上有一个红色的矩形,它处在(60,100)的位置上(相对于绝对坐标系),它自身也有一个坐标系,让它体内的元素相对它进行定位,它的坐标系叫做相对坐标系(相对于绝对坐标系的坐标系)。
  • 在屏幕中央还有一个绿色的矩形,它相对于红色的矩形定位为(40,60)(相对坐标系的坐标)。
<img src="https://pic2.zhimg.com/v2-aea447116ba4ad8154f0965d7ba2002d_b.png" data-rawwidth="1240" data-rawheight="830" class="origin_image zh-lightbox-thumb" width="1240" data-original="https://pic2.zhimg.com/v2-aea447116ba4ad8154f0965d7ba2002d_r.png">

现在我们要计算这个绿色的矩形的绝对坐标,也就是坐标系转换。从下图计算我们可以很快算出这个值为(100, 160)。

<img src="https://pic2.zhimg.com/v2-045b22ff83bd2cc45138d991ea0e74ed_b.png" data-rawwidth="1240" data-rawheight="850" class="origin_image zh-lightbox-thumb" width="1240" data-original="https://pic2.zhimg.com/v2-045b22ff83bd2cc45138d991ea0e74ed_r.png">

3.知道上面这些有什么用?

可能你看到这里会觉得这些都很简单,还用你再说一遍?而且这些好像也没什么用,对吧?

上面说过坐标转换的问题,在实际开发中,我们的视图 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];
<img src="https://pic1.zhimg.com/v2-68fd83830f50a00b5a863f07b5da1008_b.png" data-rawwidth="1142" data-rawheight="810" class="origin_image zh-lightbox-thumb" width="1142" data-original="https://pic1.zhimg.com/v2-68fd83830f50a00b5a863f07b5da1008_r.png">
  • 同样的,如果使用第二个函数来实现将 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.

理清楚这些坐标转换是很有必要的,因为等会当视图关系变得很复杂的时候,假如不能理清楚,可能你自己都不知道在哪个坐标系,你会觉得明明自己写对了,但是代码跑起来就是错的。如果出现这种情况,还是应该回到起点来,理清楚这些坐标关系。

4.动手实现

<img src="https://pic1.zhimg.com/v2-40d8233d6ef303ef682f340d9715e460_b.png" data-rawwidth="368" data-rawheight="641" class="content_image" width="368">
  • 首先我们在 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;
}

很简单,对吧?但是我希望你是理解这个思路以后才觉得简单,而不是仅仅觉得代码实现简单,因为下一篇就没这么简单了。

5.GitHub 地址

[GitHub](newyjp/JPAnimation) 地址在这里。


我的文章集合

下面这个链接是我所有文章的一个集合目录。这些文章凡是涉及实现的,每篇文章中都有 [Github](github.com/Chris-Pan) 地址,[Github](github.com/Chris-Pan) 上都有源码。如果某篇文章刚好在你的实际开发中帮到你,又或者提供一种不同的实现思路,让你觉得有用,那就看看这句话 “坚持每天点赞的人,99%都是帅哥美女,再也不用单身了”

[我的文章集合索引](我的文章集合索引)

你还可以关注我自己维护的简书专题[[iOS开发心得](iOS开发心得 - 专题 - 简书)]( NewPan - 简书)。这个专题的文章都是实打实的干货。

如果你有问题,除了在文章最后留言,还可以在微博[@盼盼_HKbuy](Sina Visitor System)上给我留言,以及访问我的 [Github](github.com/Chris-Pan)。