轉自:http://www.cocoachina.com/ios/20150804/12878.htmlhtml
iOS9當即要公佈了 爲了我司APP的兼容性問題 特地把手上的iOS Mac XCode都升級到了最新的beta版 而後發現iOS9的多任務管理器風格大變 變成了如下這樣的樣子ios
我突然想起來以前的文章提到我最愛的UI控件iCarousel要實現相似這樣的效果事實上是很是easy的 一時興起就花時間試驗了一下 效果還不錯 因此接下來我就介紹一下iCarousel的高級使用方法: 怎樣使用iCarousel的本身定義方式來實現iOS9的多任務管理器效果
git
模型github
首先來看一下iOS9的多任務管理器究竟是什麼樣子函數
而後咱們簡單的來建個模 這個步驟很是重要 將會影響咱們以後的計算 首先咱們把東西擺正
佈局
而後按比例用線切割一下
post
這裏可以看到 假設咱們以正中間的卡片(設定序號爲0)爲參照物的話 最右邊卡片(序號爲1)的位移就是中心卡片寬度的4/5 最左邊的卡片(序號爲-2)的位移就是中心卡片的寬度的2/5 注意:這兩個值的肯定對咱們很重要
學習
而大小*的縮放 就依照線性放大**便可了 由於計算很是easy 這裏就很少贅述了動畫
細心的人可能會注意到 事實上iOS9中的中心卡片 並不是居中的 而是靠右的 那麼咱們再把整體佈局調整一下atom
這樣就幾乎相同是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;
}
|
當你執行這段代碼的時候哦 你會發現顯示出來是如下這個樣子的 並且劃也劃不動(掀桌:這是什麼鬼~(/‵Д′)/~ ╧╧)
這是因爲咱們有個最重要的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;
}
|
而後滑動的時候打出的日誌是相似這種
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);
}
|
效果例如如下
咱們可以看到 已經可以滑動了 而且這個效果 就是相似iCarouselTypeLinear的效果
沒錯 事實上iCarousel所有的內置類型也都是經過這樣的方式來實現的 僅僅是分別依據offset進行了不一樣的形變 就形成了各類不一樣的效果
要說明的是 函數僅提供offset做爲參數 並無提供index來指明相應的是哪個itemView 這種優勢是可以讓人僅僅關注於詳細的形變計算 而無需計算與currentItemView之間的距離之類的
注意的是offset是元單位(就是說 offset是不包括寬度的 不過用來講明itemView的偏移係數) 下圖簡單說明了一下
當沒有滑動的時候 offset是這種
當滑動的時候 offset是這種
怎麼樣 知道了原理以後 是否是有種躍躍欲試的感受? 接下來咱們就回到主題上 看看怎樣一步步實現咱們想要的效果
計算
經過剛纔原理的介紹 可以知道 接下來的重點就是關於offset的計算
咱們首先來肯定一下函數的曲線圖 經過觀察iOS9的實例效果咱們可以知道 itemView從左向右滑的時候是愈來愈快的
因此這個曲線大概是這個樣子的
考驗你高中數學知識的時候到了 怎麼找到這樣的函數?
有種叫直角雙曲線的函數 大概公式是這個樣子
其曲線圖是這種
可以看到 位於第二象限的曲線就是咱們要的樣子 但是咱們還要調整一下才幹獲得終於的結果
由於offset爲0的時候 自己是不形變的 因此可以知道曲線是過原點(0,0)的 那麼咱們可以獲得函數的通常式
而在文章開頭咱們獲得了這樣兩組數據
最右邊卡片(序號爲1)的位移就是中心卡片寬度的4/5
最左邊的卡片(序號爲-2)的位移就是中心卡片的寬度的2/5
那麼代入上面的通常式中 咱們可以獲得兩個公式
計算可以獲得
a=5/4
b=5/8
而後咱們就可以獲得咱們終於想要的公式
看看曲線圖
而後咱們改動一下程序代碼(這段代碼事實上就是本文的關鍵所在)
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;
}
|
再看看效果
看上去已是咱們想要的效果了
只是 滑動一下就會發現問題
原來儘管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);
|
再看看效果
Bang!
咱們已經獲得了一個簡單的copycat
小結
文中的demo可以在這裏找到
可以看到 使用iCarousel 咱們僅用不到100行就實現了一個很是不錯的效果(關鍵代碼不到50行) 而無需作很是多額外的工做(固然你們就不要揪細節了 比方以漸隱取代模糊 最後一張卡片居中等問題 畢竟這不是個輪子 僅僅是教你們一種方法)
假設你們真正讀懂了這篇文章(可能我寫得不是很是清楚 建議看demo 同一時候讀iCarousel的源代碼來理解) 那麼僅僅要遇到相似卡片滑動的組件 都可以輕鬆應對了
講到這裏 我我的是很不喜歡反覆造輪子的 能用最少的代碼達到所需的要求是我一直以來的準則 而且許多經典的輪子庫(比方iCarousel)也值得你去深刻探索和學習 瞭解做者的想法和思路(站在巨人的肩膀)是一種很不錯的學習方法和開闊視野的途徑
另外 文中所用到的數學公式曲線圖生成站點是Desmos Graphing Calculator(從@KITTEN-YANG那瞄到的) 數學公式生成站點是Sciweaver(直接把前者的公式拷貝到後者的輸入框裏就可以了 因爲前者複製出來就是latex格式的公式了) 有需要的同窗可以研究一下怎樣使用 (打算研究一下Matlab的使用方法 可能更方便)