怎樣輕鬆實現iOS9多任務管理器效果(iCarousel高級教程)

轉自:http://www.cocoachina.com/ios/20150804/12878.htmlhtml

iOS9當即要公佈了 爲了我司APP的兼容性問題 特地把手上的iOS Mac XCode都升級到了最新的beta版 而後發現iOS9的多任務管理器風格大變 變成了如下這樣的樣子ios

pic_001.gif

我突然想起來以前的文章提到我最愛的UI控件iCarousel要實現相似這樣的效果事實上是很是easy的 一時興起就花時間試驗了一下 效果還不錯 因此接下來我就介紹一下iCarousel的高級使用方法: 怎樣使用iCarousel的本身定義方式來實現iOS9的多任務管理器效果
git

模型github

首先來看一下iOS9的多任務管理器究竟是什麼樣子函數

1438572789606303.jpg

而後咱們簡單的來建個模 這個步驟很是重要 將會影響咱們以後的計算 首先咱們把東西擺正
佈局

pic_003.png

而後按比例用線切割一下
post

pic_004.png

這裏可以看到 假設咱們以正中間的卡片(設定序號爲0)爲參照物的話 最右邊卡片(序號爲1)的位移就是中心卡片寬度的4/5 最左邊的卡片(序號爲-2)的位移就是中心卡片的寬度的2/5 注意:這兩個值的肯定對咱們很重要
學習

而大小*的縮放 就依照線性放大**便可了 由於計算很是easy 這裏就很少贅述了動畫

細心的人可能會注意到 事實上iOS9中的中心卡片 並不是居中的 而是靠右的 那麼咱們再把整體佈局調整一下atom

pic_005.png

這樣就幾乎相同是iOS9的樣子了

原理

接着咱們來了解一下iCarousel的基本原理

iCarousel支持例如如下幾種內置顯示類型(沒用過的同窗請務必使用pod try iCarousel來執行一下demo)

  • iCarouselTypeLinear

  • iCarouselTypeRotary

  • iCarouselTypeInvertedRotary

  • iCarouselTypeCylinder

  • iCarouselTypeInvertedCylinder

  • iCarouselTypeWheel

  • iCarouselTypeInvertedWheel

  • iCarouselTypeCoverFlow

  • iCarouselTypeCoverFlow2

  • iCarouselTypeTimeMachine

  • iCarouselTypeInvertedTimeMachine

詳細效果圖可以在官方Github主頁上看到 只是這幾種類型儘管好 但是也沒法知足咱們現在的需求 不要緊 iCarousel還支持本身定義類型

  • iCarouselTypeCustom

這就是咱們今天的主角

仍是代碼說話 咱們先配置一個簡單的iCarousel演示樣例 並使用iCarouselTypeCustom做爲其類型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@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;
}

當你執行這段代碼的時候哦 你會發現顯示出來是如下這個樣子的 並且劃也劃不動(掀桌:這是什麼鬼~(/‵Д′)/~ ╧╧)

pic_006.jpg

這是因爲咱們有個最重要的delegate方法沒有實現

1
- (CATransform3D)carousel:(iCarousel *)carousel itemTransformForOffset:(CGFloat)offset

這個函數也是整個iCarouselTypeCustom的靈魂所在

接下來咱們要簡單的說一下iCarousel的原理

  • iCarousel並不是一個UIScrollView 也並無包括不論什麼UIScrollView做爲subView

  • iCarousel經過UIPanGestureRecognizer來計算和維護scrollOffset這個變量

  • iCarousel經過scrollOffset來驅動整個動畫過程

  • iCarousel自己並不會改變itemView的位置 而是靠改動itemView的layer.transform來實現位移和形變

可能文字說得不太清楚 咱們仍是經過代碼來看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
- (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)];
         
         ...
         ...
         
         //加入一個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;
}

pic_007.jpg

而後滑動的時候打出的日誌是相似這種

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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都是居中而且重疊在一塊兒的 咱們滑動的時候並不會改變itemView的位置 但是這個offset是會改變的 而且可以看到 所有的offset的相鄰差值都爲1.0

這就是iCarousel的一個重要的設計理念 iCarousel儘管跟UIScrollView同樣都各自會維護本身的scrollOffset 但是UIScrollView在滑動的時候改變的是本身的ViewPort 就是說 UIScrollView上的itemView是真正被放置到了他被設置的位置上 僅僅是UIScrollView經過移動顯示的窗體 形成了滑動的感受(假設不理解 請看這篇文章)

但是iCarousel並不是這樣 iCarousel會把所有的itemView都居中重疊放置在一塊兒 當scrollOffset變化時 iCarousel會計算每個itemView的offset 並經過- (CATransform3D)carousel:(iCarousel *)carousel itemTransformForOffset:(CGFloat)offset baseTransform:(CATransform3D)transform這個函數來對每個itemView進行形變 經過形變來形成滑動的效果

這個很是大膽和另類的想法着實很是奇異! 可能我解釋得不夠好(盡力了~~) 仍是經過代碼來解釋比較好

咱們改動一下函數的實現

1
2
3
4
5
6
- (CATransform3D)carousel:(iCarousel *)carousel itemTransformForOffset:(CGFloat)offset baseTransform:(CATransform3D)transform
{
     NSLog(@ "%f" ,offset);
     
     return  CATransform3DTranslate(transform, offset * self.cardSize.width, 0, 0);
}

效果例如如下

pic_008.jpg

咱們可以看到 已經可以滑動了 而且這個效果 就是相似iCarouselTypeLinear的效果

沒錯 事實上iCarousel所有的內置類型也都是經過這樣的方式來實現的 僅僅是分別依據offset進行了不一樣的形變 就形成了各類不一樣的效果

要說明的是 函數僅提供offset做爲參數 並無提供index來指明相應的是哪個itemView 這種優勢是可以讓人僅僅關注於詳細的形變計算 而無需計算與currentItemView之間的距離之類的

注意的是offset是元單位(就是說 offset是不包括寬度的 不過用來講明itemView的偏移係數) 下圖簡單說明了一下

當沒有滑動的時候 offset是這種

pic_009.png

當滑動的時候 offset是這種

pic_010.png

怎麼樣 知道了原理以後 是否是有種躍躍欲試的感受? 接下來咱們就回到主題上 看看怎樣一步步實現咱們想要的效果

計算

經過剛纔原理的介紹 可以知道 接下來的重點就是關於offset的計算

咱們首先來肯定一下函數的曲線圖 經過觀察iOS9的實例效果咱們可以知道 itemView從左向右滑的時候是愈來愈快的

因此這個曲線大概是這個樣子的

pic_011.png

考驗你高中數學知識的時候到了 怎麼找到這樣的函數?

有種叫直角雙曲線的函數 大概公式是這個樣子

pic_012.png

其曲線圖是這種

pic_013.png

可以看到 位於第二象限的曲線就是咱們要的樣子 但是咱們還要調整一下才幹獲得終於的結果

由於offset爲0的時候 自己是不形變的 因此可以知道曲線是過原點(0,0)的 那麼咱們可以獲得函數的通常式

pic_014.png

而在文章開頭咱們獲得了這樣兩組數據

  • 最右邊卡片(序號爲1)的位移就是中心卡片寬度的4/5

  • 最左邊的卡片(序號爲-2)的位移就是中心卡片的寬度的2/5

那麼代入上面的通常式中 咱們可以獲得兩個公式

pic_015.png

pic_016.png

計算可以獲得

a=5/4

b=5/8

而後咱們就可以獲得咱們終於想要的公式

pic_017.png

看看曲線圖

pic_018.png

而後咱們改動一下程序代碼(這段代碼事實上就是本文的關鍵所在)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
- (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;
}
//位移經過獲得的公式來計算
- (CGFloat)translationByOffset:(CGFloat)offset
{
     CGFloat z = 5.0f/4.0f;
     CGFloat n = 5.0f/8.0f;
     
     //z/n是臨界值 >=這個值時 咱們就把itemView放到比較遠的地方不讓他顯示在屏幕上就可以了
     if  ( offset >= z/n )
     {
         return  2.0f;
     }
     
     return  1/(z-n*offset)-1/z;
}

再看看效果

pic_019.jpg

看上去已是咱們想要的效果了

只是 滑動一下就會發現問題

pic_020.jpg

原來儘管itemView的大小和位移都依照咱們的預期變化了 但是層級出現了問題 那麼iCarousel是怎樣調整itemView的層級的呢? 查看源代碼咱們可以知道

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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];
     }
}

主要就是這個compareViewDepth的比較函數起做用 而這個函數中比較的就是CATransform3D的各個屬性值

咱們來看一下CATransform3D的各個屬性各表明什麼

1
2
3
4
5
6
7
struct CATransform3D
{
CGFloat     m11(x縮放),     m12(y切變),     m13(旋轉),     m14();
CGFloat     m21(x切變),     m22(y縮放),     m23(),     m24();
CGFloat     m31(旋轉),      m32( ),        m33(),     m34(透視);
CGFloat     m41(x平移),     m42(y平移),     m43(z平移),     m44();
};

而所有CATransform3D開頭的函數(比方CATransform3DScale CATransform3DTranslate) 改變的也就是這些值而已

回到整體 咱們發現這個函數先比較的是t1.m13 + t1.m23 + t1.m33 + t1.m43; 而m13表明的是旋轉 m23和m33臨時並無含義 而m43表明的是z平移 那麼咱們僅僅要改變m43就可以了 而改變m43最簡單的辦法就是

1
CATransform3D CATransform3DTranslate (CATransform3D t, CGFloat tx,CGFloat ty, CGFloat tz)

最後一個參數就是用來改變m43的

那麼咱們把以前iCarousel的delegate方法略微修改一下 將當前的offset設置給最後一個參數就能夠(因爲offset就是按順序傳進來的)

1
return  CATransform3DScale(CATransform3DTranslate(transform, translation * self.cardSize.width, 0, offset), scale, scale, 1.0f);

再看看效果

pic_021.gif

Bang!

咱們已經獲得了一個簡單的copycat

小結

文中的demo可以在這裏找到

可以看到 使用iCarousel 咱們僅用不到100行就實現了一個很是不錯的效果(關鍵代碼不到50行) 而無需作很是多額外的工做(固然你們就不要揪細節了 比方以漸隱取代模糊 最後一張卡片居中等問題 畢竟這不是個輪子 僅僅是教你們一種方法)

假設你們真正讀懂了這篇文章(可能我寫得不是很是清楚 建議看demo 同一時候讀iCarousel的源代碼來理解) 那麼僅僅要遇到相似卡片滑動的組件 都可以輕鬆應對了

講到這裏 我我的是很不喜歡反覆造輪子的 能用最少的代碼達到所需的要求是我一直以來的準則 而且許多經典的輪子庫(比方iCarousel)也值得你去深刻探索和學習 瞭解做者的想法和思路(站在巨人的肩膀)是一種很不錯的學習方法和開闊視野的途徑

另外 文中所用到的數學公式曲線圖生成站點是Desmos Graphing Calculator(從@KITTEN-YANG那瞄到的) 數學公式生成站點是Sciweaver(直接把前者的公式拷貝到後者的輸入框裏就可以了 因爲前者複製出來就是latex格式的公式了) 有需要的同窗可以研究一下怎樣使用 (打算研究一下Matlab的使用方法 可能更方便)

相關文章
相關標籤/搜索