如何輕松實(shí)現(xiàn)iOS9多任務(wù)管理器效果(iCarousel高級(jí)教程)
iOS9馬上要發(fā)布了 為了我司APP的兼容性問(wèn)題 特意把手上的iOS Mac XCode都升級(jí)到了***的beta版 然后發(fā)現(xiàn)iOS9的多任務(wù)管理器風(fēng)格大變 變成了下面這種樣子
我忽然想起來(lái)之前的文章提到我***的UI控件iCarousel要實(shí)現(xiàn)類似這種效果其實(shí)是很簡(jiǎn)單的 一時(shí)興起就花時(shí)間試驗(yàn)了一下 效果還不錯(cuò) 所以接下來(lái)我就介紹一下iCarousel的高級(jí)用法: 如何使用iCarousel的自定義方式來(lái)實(shí)現(xiàn)iOS9的多任務(wù)管理器效果
模型
首先來(lái)看一下iOS9的多任務(wù)管理器究竟是什么樣子
然后我們簡(jiǎn)單的來(lái)建個(gè)模 這個(gè)步驟很重要 將會(huì)影響我們之后的計(jì)算 首先我們把東西擺正
然后按比例用線分割一下
這里可以看到 如果我們以正中間的卡片(設(shè)定序號(hào)為0)為參照物的話 最右邊卡片(序號(hào)為1)的位移就是中心卡片寬度的4/5 最左邊的卡片(序號(hào)為-2)的位移就是中心卡片的寬度的2/5 注意:這兩個(gè)值的確定對(duì)我們非常重要
而大小*的縮放 就按照線性放大**就行了 由于計(jì)算很簡(jiǎn)單 這里就不多贅述了
細(xì)心的人可能會(huì)注意到 其實(shí)iOS9中的中心卡片 并不是居中的 而是靠右的 那么我們?cè)侔颜w布局調(diào)整一下
這樣就差不多是iOS9的樣子了
#p#
原理
接著我們來(lái)了解一下iCarousel的基本原理
iCarousel支持如下幾種內(nèi)置顯示類型(沒(méi)用過(guò)的同學(xué)請(qǐng)務(wù)必使用pod try iCarousel來(lái)運(yùn)行一下demo)
iCarouselTypeLinear
iCarouselTypeRotary
iCarouselTypeInvertedRotary
iCarouselTypeCylinder
iCarouselTypeInvertedCylinder
iCarouselTypeWheel
iCarouselTypeInvertedWheel
iCarouselTypeCoverFlow
iCarouselTypeCoverFlow2
iCarouselTypeTimeMachine
iCarouselTypeInvertedTimeMachine
具體效果圖可以在官方Github主頁(yè)上看到 不過(guò)這幾種類型雖然好 但是也無(wú)法滿足我們現(xiàn)在的需求 沒(méi)關(guān)系 iCarousel還支持自定義類型
iCarouselTypeCustom
這就是我們今天的主角
還是代碼說(shuō)話 我們先配置一個(gè)簡(jiǎn)單的iCarousel示例 并使用iCarouselTypeCustom作為其類型
- @interface ViewController ()
- <
- iCarouselDelegate,
- iCarouselDataSource
- >
- @property (nonatomic, strong) iCarousel *carousel;
- @property (nonatomic, assign) CGSize cardSize;
- @end
- @implementation ViewController
- - (void)viewDidLoad {
- [super viewDidLoad];
- CGFloat cardWidth = [UIScreen mainScreen].bounds.size.width*5.0f/7.0f;
- self.cardSize = CGSizeMake(cardWidth, cardWidth*16.0f/9.0f);
- self.view.backgroundColor = [UIColor blackColor];
- self.carousel = [[iCarousel alloc] initWithFrame:[UIScreen mainScreen].bounds];
- [self.view addSubview:self.carousel];
- self.carousel.delegate = self;
- self.carousel.dataSource = self;
- self.carousel.type = iCarouselTypeCustom;
- self.carousel.bounceDistance = 0.2f;
- }
- - (NSInteger)numberOfItemsInCarousel:(iCarousel *)carousel
- {
- return 15;
- }
- - (CGFloat)carouselItemWidth:(iCarousel *)carousel
- {
- return self.cardSize.width;
- }
- - (UIView *)carousel:(iCarousel *)carousel viewForItemAtIndex:(NSInteger)index reusingView:(UIView *)view
- {
- UIView *cardView = view;
- if ( !cardView )
- {
- cardView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.cardSize.width, self.cardSize.height)];
- UIImageView *imageView = [[UIImageView alloc] initWithFrame:cardView.bounds];
- [cardView addSubview:imageView];
- imageView.contentMode = UIViewContentModeScaleAspectFill;
- imageView.backgroundColor = [UIColor whiteColor];
- cardView.layer.shadowPath = [UIBezierPath bezierPathWithRoundedRect:imageView.frame cornerRadius:5.0f].CGPath;
- cardView.layer.shadowRadius = 3.0f;
- cardView.layer.shadowColor = [UIColor blackColor].CGColor;
- cardView.layer.shadowOpacity = 0.5f;
- cardView.layer.shadowOffset = CGSizeMake(0, 0);
- CAShapeLayer *layer = [CAShapeLayer layer];
- layer.frame = imageView.bounds;
- layer.path = [UIBezierPath bezierPathWithRoundedRect:imageView.bounds cornerRadius:5.0f].CGPath;
- imageView.layer.mask = layer;
- }
- return cardView;
- }
當(dāng)你運(yùn)行這段代碼的時(shí)候哦 你會(huì)發(fā)現(xiàn)顯示出來(lái)是下面這個(gè)樣子的 并且劃也劃不動(dòng)(掀桌:這是什么鬼~(/‵Д′)/~ ╧╧)
這是因?yàn)槲覀冇袀€(gè)最重要的delegate方法沒(méi)有實(shí)現(xiàn)
- - (CATransform3D)carousel:(iCarousel *)carousel itemTransformForOffset:(CGFloat)offset
這個(gè)函數(shù)也是整個(gè)iCarouselTypeCustom的靈魂所在
接下來(lái)我們要簡(jiǎn)單的說(shuō)一下iCarousel的原理
iCarousel并不是一個(gè)UIScrollView 也并沒(méi)有包含任何UIScrollView作為subView
iCarousel通過(guò)UIPanGestureRecognizer來(lái)計(jì)算和維護(hù)scrollOffset這個(gè)變量
iCarousel通過(guò)scrollOffset來(lái)驅(qū)動(dòng)整個(gè)動(dòng)畫(huà)過(guò)程
iCarousel本身并不會(huì)改變itemView的位置 而是靠修改itemView的layer.transform來(lái)實(shí)現(xiàn)位移和形變
可能文字說(shuō)得不太清楚 我們還是通過(guò)代碼來(lái)看一下
- - (UIView *)carousel:(iCarousel *)carousel viewForItemAtIndex:(NSInteger)index reusingView:(UIView *)view
- {
- UIView *cardView = view;
- if ( !cardView )
- {
- cardView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.cardSize.width, self.cardSize.height)];
- ...
- ...
- //添加一個(gè)lbl
- UILabel *lbl = [[UILabel alloc] initWithFrame:cardView.bounds];
- lbl.text = [@(index) stringValue];
- [cardView addSubview:lbl];
- lbl.font = [UIFont boldSystemFontOfSize:200];
- lbl.textAlignment = NSTextAlignmentCenter;
- }
- return cardView;
- }
- - (CATransform3D)carousel:(iCarousel *)carousel itemTransformForOffset:(CGFloat)offset baseTransform:(CATransform3D)transform
- {
- NSLog(@"%f",offset);
- return transform;
- }
然后滑動(dòng)的時(shí)候打出的日志是類似這樣的
- 2015-07-28 16:53:22.330 DemoTaskTray[1834:485052] -2.999739
- 2015-07-28 16:53:22.331 DemoTaskTray[1834:485052] 2.000261
- 2015-07-28 16:53:22.331 DemoTaskTray[1834:485052] -1.999739
- 2015-07-28 16:53:22.331 DemoTaskTray[1834:485052] 3.000261
- 2015-07-28 16:53:22.331 DemoTaskTray[1834:485052] -0.999739
- 2015-07-28 16:53:22.332 DemoTaskTray[1834:485052] 0.000261
- 2015-07-28 16:53:22.332 DemoTaskTray[1834:485052] 1.000261
- 2015-07-28 16:53:22.346 DemoTaskTray[1834:485052] -3.000000
- 2015-07-28 16:53:22.347 DemoTaskTray[1834:485052] 2.000000
- 2015-07-28 16:53:22.347 DemoTaskTray[1834:485052] -2.000000
- 2015-07-28 16:53:22.348 DemoTaskTray[1834:485052] 3.000000
- 2015-07-28 16:53:22.348 DemoTaskTray[1834:485052] -1.000000
- 2015-07-28 16:53:22.348 DemoTaskTray[1834:485052] 0.000000
- 2015-07-28 16:53:22.348 DemoTaskTray[1834:485052] 1.000000
- 2015-07-28 16:53:22.363 DemoTaskTray[1834:485052] -3.000000
- 2015-07-28 16:53:22.363 DemoTaskTray[1834:485052] 2.000000
- 2015-07-28 16:53:22.363 DemoTaskTray[1834:485052] -2.000000
- 2015-07-28 16:53:22.363 DemoTaskTray[1834:485052] 3.000000
- 2015-07-28 16:53:22.364 DemoTaskTray[1834:485052] -1.000000
- 2015-07-28 16:53:22.364 DemoTaskTray[1834:485052] 0.000000
- 2015-07-28 16:53:22.364 DemoTaskTray[1834:485052] 1.000000
可以看到 所有的itemView都是居中并且重疊在一起的 我們滑動(dòng)的時(shí)候并不會(huì)改變itemView的位置 但是這個(gè)offset是會(huì)改變的 而且可以看到 所有的offset的相鄰差值都為1.0
這就是iCarousel的一個(gè)重要的設(shè)計(jì)理念 iCarousel雖然跟UIScrollView一樣都各自會(huì)維護(hù)自己的scrollOffset 但是UIScrollView在滑動(dòng)的時(shí)候改變的是自己的ViewPort 就是說(shuō) UIScrollView上的itemView是真正被放置到了他被設(shè)置的位置上 只是UIScrollView通過(guò)移動(dòng)顯示的窗口 造成了滑動(dòng)的感覺(jué)(如果不理解 請(qǐng)看這篇文章)
但是iCarousel并不是這樣 iCarousel會(huì)把所有的itemView都居中重疊放置在一起 當(dāng)scrollOffset變化時(shí) iCarousel會(huì)計(jì)算每個(gè)itemView的offset 并通過(guò)- (CATransform3D)carousel:(iCarousel *)carousel itemTransformForOffset:(CGFloat)offset baseTransform:(CATransform3D)transform這個(gè)函數(shù)來(lái)對(duì)每個(gè)itemView進(jìn)行形變 通過(guò)形變來(lái)造成滑動(dòng)的效果
這個(gè)非常大膽和另類的想法著實(shí)很奇妙! 可能我解釋得不夠好(盡力了~~) 還是通過(guò)代碼來(lái)解釋比較好
#p#
我們修改一下函數(shù)的實(shí)現(xiàn)
- - (CATransform3D)carousel:(iCarousel *)carousel itemTransformForOffset:(CGFloat)offset baseTransform:(CATransform3D)transform
- {
- NSLog(@"%f",offset);
- return CATransform3DTranslate(transform, offset * self.cardSize.width, 0, 0);
- }
效果如下
我們可以看到 已經(jīng)可以滑動(dòng)了 而且這個(gè)效果 就是類似iCarouselTypeLinear的效果
沒(méi)錯(cuò) 其實(shí)iCarousel所有的內(nèi)置類型也都是通過(guò)這種方式來(lái)實(shí)現(xiàn)的 只是分別根據(jù)offset進(jìn)行了不同的形變 就造成了各種不同的效果
要說(shuō)明的是 函數(shù)僅提供offset作為參數(shù) 并沒(méi)有提供index來(lái)指明對(duì)應(yīng)的是哪一個(gè)itemView 這樣的好處是可以讓人只關(guān)注于具體的形變計(jì)算 而無(wú)需計(jì)算與currentItemView之間的距離之類的
注意的是offset是元單位(就是說(shuō) offset是不包含寬度的 僅僅是用來(lái)說(shuō)明itemView的偏移系數(shù)) 下圖簡(jiǎn)單說(shuō)明了一下
當(dāng)沒(méi)有滑動(dòng)的時(shí)候 offset是這樣的
當(dāng)滑動(dòng)的時(shí)候 offset是這樣的
怎么樣 知道了原理之后 是不是有種躍躍欲試的感覺(jué)? 接下來(lái)我們就回到主題上 看看如何一步步實(shí)現(xiàn)我們想要的效果
計(jì)算
通過(guò)剛才原理的介紹 可以知道 接下來(lái)的重點(diǎn)就是關(guān)于offset的計(jì)算
我們首先來(lái)確定一下函數(shù)的曲線圖 通過(guò)觀察iOS9的實(shí)例效果我們可以知道 itemView從左向右滑的時(shí)候是越來(lái)越快的
所以這個(gè)曲線大概是這個(gè)樣子的
考驗(yàn)?zāi)愀咧袛?shù)學(xué)知識(shí)的時(shí)候到了 怎么找到這種函數(shù)?
有種叫直角雙曲線的函數(shù) 大概公式是這個(gè)樣子
其曲線圖是這樣的
可以看到 位于第二象限的曲線就是我們要的樣子 但是我們還要調(diào)整一下才能得到最終的結(jié)果
由于offset為0的時(shí)候 本身是不形變的 所以可以知道曲線是過(guò)原點(diǎn)(0,0)的 那么我們可以得到函數(shù)的一般式
而在文章開(kāi)頭我們得到了這樣兩組數(shù)據(jù)
最右邊卡片(序號(hào)為1)的位移就是中心卡片寬度的4/5
最左邊的卡片(序號(hào)為-2)的位移就是中心卡片的寬度的2/5
那么代入上面的一般式中 我們可以得到兩個(gè)公式
計(jì)算可以得到
a=5/4
b=5/8
然后我們就可以得到我們最終想要的公式
看看曲線圖
#p#
然后我們修改一下程序代碼(這段代碼其實(shí)就是本文的關(guān)鍵所在)
- - (CATransform3D)carousel:(iCarousel *)carousel itemTransformForOffset:(CGFloat)offset baseTransform:(CATransform3D)transform
- {
- CGFloat scale = [self scaleByOffset:offset];
- CGFloat translation = [self translationByOffset:offset];
- return CATransform3DScale(CATransform3DTranslate(transform, translation * self.cardSize.width, 0, 0), scale, scale, 1.0f);
- }
- - (void)carouselDidScroll:(iCarousel *)carousel
- {
- for ( UIView *view in carousel.visibleItemViews)
- {
- CGFloat offset = [carousel offsetForItemAtIndex:[carousel indexOfItemView:view]];
- if ( offset < -3.0 )
- {
- view.alpha = 0.0f;
- }
- else if ( offset < -2.0f)
- {
- view.alpha = offset + 3.0f;
- }
- else
- {
- view.alpha = 1.0f;
- }
- }
- }
- //形變是線性的就ok了
- - (CGFloat)scaleByOffset:(CGFloat)offset
- {
- return offset*0.04f + 1.0f;
- }
- //位移通過(guò)得到的公式來(lái)計(jì)算
- - (CGFloat)translationByOffset:(CGFloat)offset
- {
- CGFloat z = 5.0f/4.0f;
- CGFloat n = 5.0f/8.0f;
- //z/n是臨界值 >=這個(gè)值時(shí) 我們就把itemView放到比較遠(yuǎn)的地方不讓他顯示在屏幕上就可以了
- if ( offset >= z/n )
- {
- return 2.0f;
- }
- return 1/(z-n*offset)-1/z;
- }
再看看效果
看上去已經(jīng)是我們想要的效果了
不過(guò) 滑動(dòng)一下就會(huì)發(fā)現(xiàn)問(wèn)題
原來(lái)雖然itemView的大小和位移都按照我們的預(yù)期變化了 但是層級(jí)出現(xiàn)了問(wèn)題 那么iCarousel是如何調(diào)整itemView的層級(jí)的呢? 查看源碼我們可以知道
- NSComparisonResult compareViewDepth(UIView *view1, UIView *view2, iCarousel *self)
- {
- //compare depths
- CATransform3D t1 = view1.superview.layer.transform;
- CATransform3D t2 = view2.superview.layer.transform;
- CGFloat z1 = t1.m13 + t1.m23 + t1.m33 + t1.m43;
- CGFloat z2 = t2.m13 + t2.m23 + t2.m33 + t2.m43;
- CGFloat difference = z1 - z2;
- //if depths are equal, compare distance from current view
- if (difference == 0.0)
- {
- CATransform3D t3 = [self currentItemView].superview.layer.transform;
- if (self.vertical)
- {
- CGFloat y1 = t1.m12 + t1.m22 + t1.m32 + t1.m42;
- CGFloat y2 = t2.m12 + t2.m22 + t2.m32 + t2.m42;
- CGFloat y3 = t3.m12 + t3.m22 + t3.m32 + t3.m42;
- difference = fabs(y2 - y3) - fabs(y1 - y3);
- }
- else
- {
- CGFloat x1 = t1.m11 + t1.m21 + t1.m31 + t1.m41;
- CGFloat x2 = t2.m11 + t2.m21 + t2.m31 + t2.m41;
- CGFloat x3 = t3.m11 + t3.m21 + t3.m31 + t3.m41;
- difference = fabs(x2 - x3) - fabs(x1 - x3);
- }
- }
- return (difference < 0.0)? NSOrderedAscending: NSOrderedDescending;
- }
- - (void)depthSortViews
- {
- for (UIView *view in [[_itemViews allValues] sortedArrayUsingFunction:(NSInteger (*)(id, id, void *))compareViewDepth context:(__bridge void *)self])
- {
- [_contentView bringSubviewToFront:view.superview];
- }
- }
主要就是這個(gè)compareViewDepth的比較函數(shù)起作用 而這個(gè)函數(shù)中比較的就是CATransform3D的各個(gè)屬性值
我們來(lái)看一下CATransform3D的各個(gè)屬性各代表什么
- struct CATransform3D
- {
- CGFloat m11(x縮放), m12(y切變), m13(旋轉(zhuǎn)), m14();
- CGFloat m21(x切變), m22(y縮放), m23(), m24();
- CGFloat m31(旋轉(zhuǎn)), m32( ), m33(), m34(透視);
- CGFloat m41(x平移), m42(y平移), m43(z平移), m44();
- };
而所有CATransform3D開(kāi)頭的函數(shù)(比如CATransform3DScale CATransform3DTranslate) 改變的也就是這些值而已
回到整體 我們發(fā)現(xiàn)這個(gè)函數(shù)先比較的是t1.m13 + t1.m23 + t1.m33 + t1.m43; 而m13代表的是旋轉(zhuǎn) m23和m33暫時(shí)并沒(méi)有含義 而m43代表的是z平移 那么我們只要改變m43就可以了 而改變m43最簡(jiǎn)單的辦法就是
- CATransform3D CATransform3DTranslate (CATransform3D t, CGFloat tx,CGFloat ty, CGFloat tz)
***一個(gè)參數(shù)就是用來(lái)改變m43的
那么我們把之前iCarousel的delegate方法稍微改動(dòng)一下 將當(dāng)前的offset設(shè)置給***一個(gè)參數(shù)即可(因?yàn)閛ffset就是按順序傳進(jìn)來(lái)的)
- return CATransform3DScale(CATransform3DTranslate(transform, translation * self.cardSize.width, 0, offset), scale, scale, 1.0f);
再看看效果
Bang!
我們已經(jīng)得到了一個(gè)簡(jiǎn)單的copycat
小結(jié)
文中的demo可以在這里找到
可以看到 使用iCarousel 我們僅用不到100行就實(shí)現(xiàn)了一個(gè)非常不錯(cuò)的效果(關(guān)鍵代碼不到50行) 而無(wú)需做很多額外的工作(當(dāng)然大家就不要揪細(xì)節(jié)了 比如以漸隱代替模糊 ***一張卡片居中等問(wèn)題 畢竟這不是個(gè)輪子 只是教大家一種方法)
如果大家真正讀懂了這篇文章(可能我寫(xiě)得不是很清楚 建議看demo 同時(shí)讀iCarousel的源碼來(lái)理解) 那么只要遇到類似卡片滑動(dòng)的組件 都可以輕松應(yīng)對(duì)了
說(shuō)到這里 我個(gè)人是非常不喜歡重復(fù)造輪子的 能用最少的代碼達(dá)到所需的要求是我一直以來(lái)的準(zhǔn)則 而且很多經(jīng)典的輪子庫(kù)(比如iCarousel)也值得你去深入探索和學(xué)習(xí) 了解作者的想法和思路(站在巨人的肩膀)是一種非常不錯(cuò)的學(xué)習(xí)方法和開(kāi)闊視野的途徑
另外 文中所用到的數(shù)學(xué)公式曲線圖生成網(wǎng)站是Desmos Graphing Calculator(從@KITTEN-YANG那瞄到的) 數(shù)學(xué)公式生成網(wǎng)站是Sciweaver(直接把前者的公式復(fù)制到后者的輸入框里就可以了 因?yàn)榍罢邚?fù)制出來(lái)就是latex格式的公式了) 有需要的同學(xué)可以研究一下如何使用 (打算研究一下Matlab的用法 可能更方便)