本文授權轉載,作者:Sindri的小巢(簡書)
前言
從最開始動筆動畫篇的博客,至今已經過去了四個多月。這段時間回頭看了看自己之前的動畫文章,發現用來講解動畫的例子確實不那麼的賞心悅目。於是這段時間總是想著使用最基礎的動畫知識來實現一個好看的效果,卻遲遲想不到該怎麼做,直到在網上看到一個驚艷的碎片化動畫,於是自己實現之後拿來講解一下:
碎片化動畫
遮罩視圖
在UIView中有一個maskView屬性,這個屬性是我們今天實現動畫的最重要的變量。這個屬性在iOS8之後開始使用,用來表示視圖的遮罩。什麼是遮罩呢?我想了很久都沒有找到合適的比喻來介紹這個。簡單來說,一個UIView的對象,可以通過設置alpha來改變這個視圖的透明度,遮罩的實現效果也是一樣的。唯一的差別在於前者是通過修改0~1之間的值來改變透明效果,作為遮罩的視圖對象的backgroundColor、alpha、transform等等屬性都會影響到被遮蓋的視圖的透明效果。例如下面這段代碼:
UIView * viewContainer = [[UIView alloc] initWithFrame: CGRectMake(0, 0, 200, 200)]; viewContainer.backgroundColor = [UIColor blueColor]; UIView * contentView = [[UIView alloc] initWithFrame: CGRectMake(20, 20, 160, 160)]; contentView.backgroundColor = [UIColor redColor]; [viewContainer addSubview: contentView]; UIView * maskView = [[UIView alloc] initWithFrame: CGRectMake(100, 100, 35, 80)]; maskView.backgroundColor = [UIColor yellowColor]; contentView.maskView = maskView;
遮罩視圖決定了視圖的顯示內容
上面的代碼小小的改動一下,我們分別修改一下maskView和contentView的透明度,看看在遮罩透明度改變之後紅色的視圖會發生什麼變化:
修改透明度
通過實驗我們可以看到修改視圖自身的透明度或者修改maskView的透明度達成的效果是一樣的。換句話說,遮蓋視圖對於視圖自身的影響直接決定在透明度和顯示尺寸這兩個可視的屬性。
那麼,遮蓋視圖除了alpha屬性外,還有什麼屬性影響了視圖本身的顯示效果呢?
顏色
上面的透明度效果得出了一個結論。視圖本身的顯示效果取決於maskView的透明程度。在顏色不含透明空間的時候,視圖是不存在透明效果的。但是假設我們設置遮罩視圖的顏色透明度時:
maskView.backgroundColor = [UIColor colorWithWhite: 1 alpha: 0.5]; //任意顏色
顯示的效果跟直接設置alpha = 0.5的效果是一樣的。在繪制像素到屏幕上一文中可以獲知顏色渲染和alpha屬性存在的關聯
maskView的子視圖
maskView.backgroundColor = [UIColor clearColor]; UIView * sub1 = [[UIView alloc] initWithFrame: CGRectMake(0, 0, 20, 34)]; sub1.backgroundColor = [UIColor blackColor]; UIView * sub2 = [[UIView alloc] initWithFrame: CGRectMake(15, 18, 33, 40)]; sub2.backgroundColor = [UIColor blackColor]; [maskView addSubview: sub1]; [maskView addSubview: sub2];
要了解maskView的子視圖對遮罩效果的影響,我們需要排除遮罩視圖自身的干擾,因此maskView的背景顏色要設置成透明色。
子視圖對於遮罩的影響
可以看到,在遮罩自身透明的情況下,子視圖也可以實現部分遮罩視圖的效果。因此如果我們改變這些子視圖的透明度的時候,遮罩效果也同樣會發生改變。
動畫實現
回到上面展示的動畫效果,我們可以看到圖片被分割成多個長方形的小塊逐漸消失。其中,垂直方向分為上下兩份,橫向大概有15份左右。因此我們需要現在maskView上面添加2*15個子視圖,均勻分布。為了保證在動畫的時候我們能依次實現子視圖的隱藏,我們需要給子視圖加上標識:
UIView * maskView = [[UIView alloc] initWithFrame: contentView.bounds]; const NSInteger horizontalCount = 15; const NSInteger verticalCount = 2; const CGFloat fadeWidth = CGRectGetWidth(maskView.frame) / horizontalCount; const CGFloat fadeHeight = CGRectGetHeight(maskView.frame) / verticalCount; for (NSInteger line = 0; line < horizontalCount; line ++) { for (NSInteger row = 0; row < verticalCount; row++) { CGRect frame = CGRectMake(line*fadeWidth, row*fadeHeight, fadeWidth, fadeHeight); UIView * fadeView = [[UIView alloc] initWithFrame: frame]; fadeView.tag = [self viewTag: line*verticalCount+row]; fadeView.backgroundColor = [UIColor whiteColor]; [maskView addSubview: fadeView]; } } contentView.maskView = maskView;
那麼在動畫開始的時候,我們需要依次遍歷maskView上面的所有子視圖,並且讓他們依次執行動畫:
for (NSInteger line = 0; line < horizontalCount; line ++) { for (NSInteger row = 0; row < verticalCount; row++) { NSInteger idx = line*verticalCount+row; UIView * fadeView = [contentView.maskView viewWithTag: [self viewWithTag: idx]; [UIView animateWithDuration: fadeDuration delay: interval*idx options: UIViewAnimationOptionCurveLinear animations: ^{ fadeView.alpha = 0; } completion: nil]; } }
我們在實現動畫的同時,都應該考慮如何把動畫封裝出來方便以後復用。上面的碎片化動畫完全可以作為UIView的category進行封裝,以此來降低入侵性,實現低耦合的要求:
#define LXDMAXDURATION 1.2 #define LXDMINDURATION .2 #define LXDMULTIPLED .25 @interface UIView (LXDFadeAnimation) /*! * @brief 視圖是否隱藏 */ @property (nonatomic, assign, readonly) BOOL isFade; /*! * @brief 是否處在動畫中 */ @property (nonatomic, assign, readonly) BOOL isFading; /*! * @brief 垂直方塊個數。默認為3 */ @property (nonatomic, assign) NSInteger verticalCount; /*! * @brief 水平方塊個數。默認為18 */ @property (nonatomic, assign) NSInteger horizontalCount; /*! * @brief 方塊動畫之間的間隔0.2~1.2。默認0.7 */ @property (nonatomic, assign) NSTimeInterval intervalDuration; /*! * @brief 每個方塊隱藏的動畫時間0.05~0.3,最多為動畫時長的25%。默認為0.175 */ @property (nonatomic, assign) NSTimeInterval fadeAnimationDuration; - (void)configurateWithVerticalCount: (NSInteger)verticalCount horizontalCount: (NSInteger)horizontalCount interval: (NSTimeInterval)interval duration: (NSTimeInterval)duration; - (void)reverseWithComplete: (void(^)(void))complete; - (void)animateFadeWithComplete: (void(^)(void))complete; - (void)reverseWithoutAnimate; @end
在iOS中,在category中聲明的所有屬性編譯器都不會自動綁定getter和setter方法,這意味著我們需要重寫這兩種方法,而且還不能使用下劃線+變量名的方式直接訪問變量。因此我們需要導入objc/runtime.h文件使用動態時提供的objc_associateObject機制來為視圖動態增加屬性:
- (BOOL)isFade { return [objc_getAssociatedObject(self, kIsFadeKey) boolValue]; } // other getAssociatedObject method - (void)setIsFade: (BOOL)isFade { objc_setAssociatedObject(self, kIsFadeKey, @(isFade), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } // other setAssociatedObject method
有了碎片化隱藏視圖的動畫,同樣需要一個還原的動畫效果:
NSInteger fadeCount = self.verticalCount * self.horizontalCount; for (NSInteger idx = fadeCount - 1; idx >= 0; idx--) { UIView * subview = [self.maskView viewWithTag: [self subViewTag: idx]]; [UIView animateWithDuration: self.fadeAnimationDuration delay: self.intervalDuration * (fadeCount - 1 - idx) options: UIViewAnimationOptionCurveLinear animations: ^{ subview.alpha = 1; } completion: nil]; }
現在我們還要考慮一個問題:假設用戶點擊某張圖片的時候就根據視圖是否隱藏狀態來開始隱藏/顯示的動畫,當用戶多次點擊的時候,我們應該判斷是否已經處在動畫狀態,如果是,那麼不繼續執行動畫代碼。另外,在動畫開始之前,我們需要把標識動畫狀態的isFading設為YES,但是由於每個方塊隱藏都存在一個動畫,動畫的結束時間應該怎麼判斷呢?已知fadeView的個數是count,那麼當最後一個方塊隱藏即是第count個動畫完成的時候,整個碎片化動畫就結束了。所以我們需要借助一個臨時變量來記錄:
__block NSInteger timeCount = 0; //...... [UIView animateWithDuration: self.fadeAnimationDuration delay: self.intervalDuration * (fadeCount - 1 - idx) options: UIViewAnimationOptionCurveLinear animations: ^{ subview.alpha = 1; } completion: ^(BOOL finished) { if (++timeCount == fadeCount) { self.isFade = NO; self.isFading = NO; if (complete) { complete(); } } }]; //......
得到動畫結束的時間後,我們就可以增加一個block提供給調用者在動畫結束時進行其他的處理。
輪播碎片動畫
在知道了碎片動畫的實現之後,我要做一個酷炫的廣告輪播頁。同樣采用category的方式來實現,當然demo中輪播的全是本地的圖片。現在放上效果圖:
廣告輪播頁
那麼實現一個廣告頁輪播需要哪些步驟呢?
1、在當前動畫的圖片下面插入一個UIImageView來展示下一張圖片。如果可以,盡量復用這個imageView
2、添加UIPageControl來標識圖片的下標
因此我提供了一個接口傳入圖片數組執行動畫:
// 獲取動態綁定臨時展示的UIImageView - (UIImageView *)associateTempBannerWithImage: (UIImage *)image { UIImageView * tempBanner = objc_getAssociatedObject(self, kTempImageKey); if (!tempBanner) { tempBanner = [[UIImageView alloc] initWithFrame: self.frame]; objc_setAssociatedObject(self, kTempImageKey, tempBanner, OBJC_ASSOCIATION_RETAIN_NONATOMIC); [self.superview insertSubview: tempBanner belowSubview: self]; } tempBanner.image = image; return tempBanner; }
此外,pageControl一開始我加在執行動畫的imageView上面,但是在動畫執行到一半的時候,pageControl也會隨著局部隱藏動畫隱藏起來。因此根據imageView當前的坐標重新計算出合適的尺寸范圍:
- (void)associatePageControlWithCurrentIdx: (NSInteger)idx { UIPageControl * pageControl = objc_getAssociatedObject(self, kPageControlKey); if (!pageControl) { pageControl = [[UIPageControl alloc] initWithFrame: CGRectMake(self.frame.origin.x, CGRectGetHeight(self.frame) - 37 + self.frame.origin.y, CGRectGetWidth(self.frame), 37)]; [self.superview addSubview: pageControl]; pageControl.numberOfPages = self.bannerImages.count; objc_setAssociatedObject(self, kPageControlKey, pageControl, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } pageControl.currentPage = idx; }
由於每次圖片碎片化動畫執行完成之後,都需要再次執行相同的碎片動畫代碼。而動畫結束是通過block執行,即我們需要在block中嵌套使用同一個block,因此首先我們需要把這段執行代碼聲明成一個block變量。另外,需要一個聲明一個idx在每次碎片動畫完成的時候更新圖片,用__block修飾來讓我們在回調中修改這個值:
- (void)fadeBanner NSParameterAssert(self.superview); UIImageView * tempBanner = [self associateTempBannerWithImage: [UIImage imageNamed: self.bannerImages[1]]]; self.stop = NO; __block NSInteger idx = 0; __weak typeof(self) weakSelf = self; [self associatePageControlWithCurrentIdx: idx]; void (^complete)() = ^{ NSInteger updateIndex = [weakSelf updateImageWithCurrentIndex: ++idx tempBanner: tempBanner]; idx = updateIndex; [weakSelf associatePageControlWithCurrentIdx: idx]; }; // 保存block並執行動畫 objc_setAssociatedObject(self, kCompleteBlockKey, complete, OBJC_ASSOCIATION_COPY_NONATOMIC); [self animateFadeWithComplete: ^{ if (!self.stop) { complete(); } }]; } // 更新展示的圖片,並且返回下一次要展示的圖片下標 - (NSInteger)updateImageWithCurrentIndex: (NSInteger)idx tempBanner: (UIImageView *)tempBanner { if (idx >= self.bannerImages.count) { idx = 0; } self.image = [UIImage imageNamed: self.bannerImages[idx]]; [self reverseWithoutAnimate]; NSInteger nextIdx = idx + 1; if (nextIdx >= self.bannerImages.count) { nextIdx = 0; } tempBanner.image = [UIImage imageNamed: self.bannerImages[nextIdx]]; [self animateFadeWithComplete: ^{ if (!self.stop) { void (^complete)() = objc_getAssociatedObject(self, kCompleteBlockKey); complete(); } }]; return idx; }
代碼中需要注意的是,我在上面使用objc_Associate的機制保存了這個完成回調的block,這個是必要的。假設你不喜歡把更新圖片的代碼封裝出來,直接把這一步驟放到上面的complete聲明中,依舊還是要動態保存起來,否則這個block執行到第三次圖片碎片的時候就會被釋放從而導致崩潰
別忘了在每次圖片切換完成之後,將所有的子視圖遮罩還原,並且更新圖片顯示
- (void)reverseWithoutAnimate { if (self.isFading) { NSLog(@"It's animating!"); return; } for (UIView * subview in self.maskView.subviews) { subview.alpha = 1; } }
最後
從動畫篇開篇過去四個多月了,感慨時間過得好快。從下一篇開始,那就是正式要進入CoreAnimation環節的節奏了。這是一個無比強大的動畫框架,要知道本文中屌炸天的maskView其實是基於圖層的mask屬性的高級封裝。嘿嘿~~ 最後奉上本文的DEMO:LXDMaskViewAnimation,去掉點擊事件中的注釋就能看到效果了。
推薦閱讀:
動畫篇-Transform和KeyFrame動畫
動畫篇-從 UIView 動畫說起
動畫篇-layout動畫初體驗
動畫篇-layout動畫的更多使