過渡動畫之入門模仿系統
本文章將會帶你學到如何實現下圖 airbnb 首頁類似的過渡動畫,同時最重要的,你將學會怎么分析類似的動畫,并且知道如何動手實現。
好,準備好了嗎?現在開始第一篇。這一篇主要分析系統簡單的動畫實現原理,以及講解坐標系、絕對坐標系、相對坐標系,坐標系轉換等知識,為第二篇儲備理論基礎。最后實現 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)recttoView:(nullableUIView *)view;
- (CGRect)convertRect:(CGRect)rectfromView:(nullableUIView *)view;
-
第一個函數,將一個當前 View 坐標系的 frame 轉換為另一個 View 的坐標系上。比如說下圖 A 中有個 B,如果要將 B 的 frame 遷移到 C 中,就應該這么寫:
CGRecttargetFrame = [A convertRect:B.frametoView:C];
- 同樣的,如果使用第二個函數來實現將 B 的 frame 遷移到 C 中,那就應該這么寫:
CGRecttargetFrame = [C convertRect:B.framefromView:A];
- 同時需要注意,如果想要把 B 的 frame 遷移到窗口坐標(絕對坐標系,也就是白色的坐標系),那就應該這么寫:
CGRecttargetFrame = [A convertRect:B.frametoView:window]; CGRecttargetFrame = [windowconvertRect:B.framefromView:A];
CGRecttargetFrame = [A convertRect:B.frametoView:nil]; // 這個函數中,如果傳個 nil,則代表窗口 window.
理清楚這些坐標轉換是很有必要的,因為等會當視圖關系變得很復雜的時候,假如不能理清楚,可能你自己都不知道在哪個坐標系,你會覺得明明自己寫對了,但是代碼跑起來就是錯的。如果出現這種情況,還是應該回到起點來,理清楚這些坐標關系。
04.動手實現
- 首先我們在 Storyborad 中創建一個 UIImageView 用來顯示文件夾圖標。
- 看一下 @interface 中的屬性
@interface ViewController () @property (weak, nonatomic) IBOutletUIImageView *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.layerrenderInContext:UIGraphicsGetCurrentContext()]; UIImage *aImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return aImage; }
- 然后我們在 touchesBegan 方法中處理動畫。大致思路遵循我一開始描述的動畫過程。
- 一開始將要做動畫的 View 進行截圖;
- 再將我們要做動畫的 View 的 frame 遷移到窗口坐標系中,作為動畫起始位置。為什么要遷移到窗口坐標系而不是其他的坐標系呢?因為我們做動畫的元素是添加到窗口上的,并且你需要將所有動畫元素的 frame 統一一個坐標系,這樣方便我們以最高效的方式管理我們自己創建的元素。
- 計算我們的終點位置,在這個動畫里很簡單,話不多說。但是在下一個仿 airbnb 的動畫里,計算終點 frame 將成為一個挑戰(關于你高中數學知識的一個挑戰)。
- 添加動畫元素一個 UIImageView 到窗口。為什么是 UIImageView 而不是其它呢?很顯然我們動畫有放大和縮小,所以應該是一個 frame 動畫。所以我們應該選擇用 UIImageView 來呈現截圖的方式來實現動畫。
- 最后用一個系統封裝的 UIView 動畫 block 來處理動畫過程。
-(void)touchesBegan:(NSSet *)toucheswithEvent:(UIEvent *)event{ // 先將文件夾那個視圖進行截圖 UIImage *animationImage = [self snapImageForView:self.folderImageView]; // 再將文件夾視圖的坐標系遷移到窗口坐標系(絕對坐標系) CGRecttargetFrame_start = [self.folderImageView.superviewconvertRect:self.folderImageView.frametoView:nil]; // 計算動畫終點位置 CGFloattargetW = targetFrame_start.size.width*magnificateMultiple; CGFloattargetH = targetFrame_start.size.height*magnificateMultiple; CGFloattargetX = (JPScreenWidth - targetW) / 2.0; CGFloattargetY =(JPScreenHeight - targetH) / 2.0; CGRecttargetFrame_end = CGRectMake(targetX, targetY, targetW, targetH); // 添加做動畫的元素 if (!self.animationImageView.superview) { self.animationImageView.image = animationImage; self.animationImageView.frame = targetFrame_start; [self.view.windowaddSubview:self.animationImageView]; } if (self.isOpenOverView) { // 預覽動畫 [UIViewanimateWithDuration:1 delay:0. options:UIViewAnimationOptionCurveEaseInanimations:^{ self.animationImageView.frame = targetFrame_end; } completion:^(BOOL finished) { }]; } else{ // 關閉預覽動畫 [UIViewanimateWithDuration:1 delay:0. options:UIViewAnimationOptionCurveEaseOutanimations:^{ self.animationImageView.frame = targetFrame_start; } completion:^(BOOL finished) { [self.animationImageViewremoveFromSuperview]; }]; } self.isOpenOverView = !self.isOpenOverView; }
很簡單,對吧?但是我希望你是理解這個思路以后才覺得簡單,而不是僅僅覺得代碼實現簡單,因為下一篇就沒這么簡單了。
來自:http://ios.jobbole.com/92312/