IOS實現(xiàn)碎片化動畫詳解
碎片化效果圖

遮罩視圖
在UIView中有一個maskView屬性,這個屬性是我們今天實現(xiàn)動畫的最重要的變量。這個屬性在iOS8之后開始使用,用來表示視圖的遮罩。什么是遮罩呢?我想了很久都沒有找到合適的比喻來介紹這個。簡單來說,一個UIView的對象,可以通過設置alpha來改變這個視圖的透明度,遮罩的實現(xiàn)效果也是一樣的。唯一的差別在于前者是通過修改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;
遮罩視圖決定了視圖的顯示內(nèi)容
上面的代碼小小的改動一下,我們分別修改一下maskView和contentView的透明度,看看在遮罩透明度改變之后紅色的視圖會發(fā)生什么變化:

修改透明度.png
通過實驗我們可以看到修改視圖自身的透明度或者修改maskView的透明度達成的效果是一樣的。換句話說,遮蓋視圖對于視圖自身的影響直接決定在透明度和顯示尺寸這兩個可視的屬性。
那么,遮蓋視圖除了alpha屬性外,還有什么屬性影響了視圖本身的顯示效果呢?
顏色
上面的透明度效果得出了一個結論。視圖本身的顯示效果取決于maskView的透明程度。在顏色不含透明空間的時候,視圖是不存在透明效果的。但是假設我們設置遮罩視圖的顏色透明度時:
maskView.backgroundColor = [UIColor colorWithWhite: 1 alpha: 0.5]; //任意顏色
顯示的效果跟直接設置alpha = 0.5的效果是一樣的。在繪制像素到屏幕上中可以獲知顏色渲染和alpha屬性存在的關聯(lián)
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的背景顏色要設置成透明色

子視圖對于遮罩的影響
可以看到,在遮罩自身透明的情況下,子視圖也可以實現(xiàn)部分遮罩視圖的效果。因此如果我們改變這些子視圖的透明度的時候,遮罩效果也同樣會發(fā)生改變
動畫實現(xiàn)
回到上面展示的動畫效果,我們可以看到圖片被分割成多個長方形的小塊逐漸消失。其中,垂直方向分為上下兩份,橫向大概有15份左右。因此我們需要現(xiàn)在maskView上面添加2*15個子視圖,均勻分布。為了保證在動畫的時候我們能依次實現(xiàn)子視圖的隱藏,我們需要給子視圖加上標識:
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上面的所有子視圖,并且讓他們依次執(zhí)行動畫:
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];
}
}
我們在實現(xiàn)動畫的同時,都應該考慮如何把動畫封裝出來方便以后復用。上面的碎片化動畫完全可以作為UIView的category進行封裝,以此來降低入侵性,實現(xiàn)低耦合的要求:
#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 垂直方塊個數(shù)。默認為3 */ @property (nonatomic, assign) NSInteger verticalCount; /*! * @brief 水平方塊個數(shù)。默認為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文件使用動態(tài)時提供的objc_associateObject機制來為視圖動態(tài)增加屬性:
- (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];
}
現(xiàn)在我們還要考慮一個問題:假設用戶點擊某張圖片的時候就根據(jù)視圖是否隱藏狀態(tài)來開始隱藏/顯示的動畫,當用戶多次點擊的時候,我們應該判斷是否已經(jīng)處在動畫狀態(tài),如果是,那么不繼續(xù)執(zhí)行動畫代碼。另外,在動畫開始之前,我們需要把標識動畫狀態(tài)的isFading設為YES,但是由于每個方塊隱藏都存在一個動畫,動畫的結束時間應該怎么判斷呢?已知fadeView的個數(shù)是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提供給調用者在動畫結束時進行其他的處理。
輪播碎片動畫
在知道了碎片動畫的實現(xiàn)之后,我要做一個酷炫的廣告輪播頁。同樣采用category的方式來實現(xiàn)?,F(xiàn)在放上效果圖:

廣告輪播頁
那么實現(xiàn)一個廣告頁輪播需要哪些步驟呢?
1、在當前動畫的圖片下面插入一個UIImageView來展示下一張圖片。如果可以,盡量復用這個imageView
2、添加UIPageControl來標識圖片的下標
因此我提供了一個接口傳入圖片數(shù)組執(zhí)行動畫:
// 獲取動態(tài)綁定臨時展示的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一開始我加在執(zhí)行動畫的imageView上面,但是在動畫執(zhí)行到一半的時候,pageControl也會隨著局部隱藏動畫隱藏起來。因此根據(jù)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;
}
由于每次圖片碎片化動畫執(zhí)行完成之后,都需要再次執(zhí)行相同的碎片動畫代碼。而動畫結束是通過block執(zhí)行,即我們需要在block中嵌套使用同一個block,因此首先我們需要把這段執(zhí)行代碼聲明成一個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并執(zhí)行動畫
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聲明中,依舊還是要動態(tài)保存起來,否則這個block執(zhí)行到第三次圖片碎片的時候就會被釋放從而導致崩潰
別忘了在每次圖片切換完成之后,將所有的子視圖遮罩還原,并且更新圖片顯示
- (void)reverseWithoutAnimate
{
if (self.isFading) {
NSLog(@"It's animating!");
return;
}
for (UIView * subview in self.maskView.subviews) {
subview.alpha = 1;
}
}
總結
以上就是關于IOS實現(xiàn)碎片化動畫的全部內(nèi)容,希望本文的內(nèi)容對大家開發(fā)IOS動畫的時候能有所幫助。
相關文章
iOS開發(fā)之tableView點擊下拉擴展與內(nèi)嵌collectionView上傳圖片效果
這篇文章主要介紹了iOS開發(fā)之tableView點擊下拉擴展與內(nèi)嵌collectionView上傳圖片效果的相關資料,需要的朋友可以參考下2016-04-04
在iOS10系統(tǒng)中微信后退無法發(fā)起ajax請求的問題解決辦法
這篇文章主要介紹了在iOS10系統(tǒng)中微信后退無法發(fā)起ajax請求的問題解決辦法,一般可以通過延時發(fā)送請求解決,下面通過本文給大家分享下解決辦法,需要的朋友參考下吧2017-01-01
iOS基于UIScrollView實現(xiàn)滑動引導頁
這篇文章主要為大家詳細介紹了iOS基于UIScrollView實現(xiàn)滑動引導頁的相關資料,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-01-01
IOS 陀螺儀開發(fā)(CoreMotion框架)實例詳解
這篇文章主要介紹了IOS 陀螺儀開發(fā)實例詳解的相關資料,介紹了螺旋儀參數(shù)意義及CoreMotion框架,需要的朋友可以參考下2016-10-10

