[iOS]過渡動畫之入門模仿系統

NewPan發表於2019-02-06
[iOS]過渡動畫之入門模仿系統

注意:我為過渡動畫寫了兩篇文章:
第一篇:[iOS]過渡動畫之簡單模仿系統,主要分析系統簡單的動畫實現原理,以及講解座標系、絕對座標系、相對座標系,座標系轉換等知識,為第二篇儲備理論基礎。最後實現 Mac 上的檔案預覽動畫。

第二篇:[iOS]過渡動畫之高階模仿 airbnb ,主要基於第一篇的理論來實現複雜的介面過渡,包括進入和退出動畫的串聯。最後將這個動畫的實現部分與當前介面解耦,並封裝為一個普適(其他類似介面也適用)的工具類。

這兩篇文章將會帶你學到如何實現下圖 airbnb 首頁類似的過渡動畫,同時最重要的,你將學會怎麼分析類似的動畫,並且知道如何動手實現。GitHub 地址在這裡。

[iOS]過渡動畫之入門模仿系統

好,準備好了嗎?現在開始第一篇。這一篇主要分析系統簡單的動畫實現原理,以及講解座標系、絕對座標系、相對座標系,座標系轉換等知識,為第二篇儲備理論基礎。最後實現 Mac 上的檔案預覽動畫。

01. 系統的過渡動畫

我很多時候做一個東西的時候,我會先想一下,我們的老東家蘋果有沒有做過類似的?如果有,那肯定蘋果的更靠譜。看到上面那個 airbnb 動畫的時候,我首先想到 Mac 上這個檔案預覽的動畫。

你還能想到 iPhone 上系統自帶更多的類似的動畫嗎?

[iOS]過渡動畫之入門模仿系統

這個動畫應該怎麼實現呢?我來描述一下這個過程,你看我說的對不對。

  • 首先你要選中這個資料夾,然後當你按下 space 鍵的時候,會產生一個用來做動畫的元素 Object ,Object 從當前選中資料夾的位置開始運動到螢幕中央(終點位置),邊運動邊放大。這是開啟預覽的過程。

  • 當你再次按下 space 鍵的時候,當前動畫元素 Object 會從螢幕中央運動到你選中的那個資料夾的位置,邊運動邊縮小。這是關閉預覽。

有沒有從這個描述中 get 到幾個關鍵點呢?

[iOS]過渡動畫之入門模仿系統

如果嘗試把這些關鍵點和動畫過程串起來,是不是就應該是下面這樣?動畫開始,先建立用來做動畫的元素(是新產生,不是拿到資料夾進行動畫,因為你也看到,之前那個資料夾它仍然在那裡沒有動),然後計算起點位置,在把這個元素新增到起點位置,接下來計算終點位置,然後開始做動畫。

[iOS]過渡動畫之入門模仿系統

02.座標系、絕對座標系、相對座標系,座標系轉換

在實現之前,我們先來複習一下初中物理。

  • 這裡我們只討論二維座標系,因為我們的動畫是基於二維座標系的。
  • 如下圖,我們有一臺 iPhone,它的座標原點在左上角,就是白色的座標系,我們物理裡面又叫做絕對座標系,其他的座標系都是參考它來定位的。
  • 在我們的 iPhone 螢幕上有一個紅色的矩形,它處在(60,100)的位置上(相對於絕對座標系),它自身也有一個座標系,讓它體內的元素相對它進行定位,它的座標系叫做相對座標系(相對於絕對座標系的座標系)。
  • 在螢幕中央還有一個綠色的矩形,它相對於紅色的矩形定位為(40,60)(相對座標系的座標)。
[iOS]過渡動畫之入門模仿系統

現在我們要計算這個綠色的矩形的絕對座標,也就是座標系轉換。從下圖計算我們可以很快算出這個值為(100, 160)。

[iOS]過渡動畫之入門模仿系統

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];
複製程式碼
[iOS]過渡動畫之入門模仿系統
  • 同樣的,如果使用第二個函式來實現將 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.動手實現

[iOS]過渡動畫之入門模仿系統
  • 首先我們在 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 地址在這裡。

NewPan 的文章集合

下面這個連結是我所有文章的一個集合目錄。這些文章凡是涉及實現的,每篇文章中都有 Github 地址,Github 上都有原始碼。

NewPan 的文章集合索引

如果你有問題,除了在文章最後留言,還可以在微博 @盼盼_HKbuy 上給我留言,以及訪問我的 Github

相關文章