iCarousel——在iOS和Mac OS應用中實現3D CoverFlow旋轉木馬效果的開源類庫


前言

iCarousel一個簡單、可高度定制的3D CoverFlow開源類庫,旨在簡化在 iPhone, iPad和Mac OS中生成各種類型的cover flow(視圖切換)效果(分頁、滾動視圖)。用戶手指划動圖片,圖片將不斷以3D的形式切換。

iCarousel

 

 

 

Github托管地址:https://github.com/nicklockwood/iCarousel

說明

      iOS開發中如果想要你實現滑動效果,可以使用icarousel這個第三方庫,還比較好用,感覺用起來有點像tableView。在網上下載這個庫,然后在界面文件中,希望混動列表的部分加上一個view,將view的類設為iCarousel,然后設置delegate和dataSource,並且在自己的controller類中,聲明要使用iCarousel的協議,包括delegate和datasource的。

          然后就是要實現的協議了,- (NSUInteger)numberOfItemsInCarousel:(iCarousel *)carousel,設置顯示在iCarousel上的元素的個數,

- (UIView *)carousel:(iCarousel *)carousel viewForItemAtIndex:(NSUInteger)index reusingView:(UIView *)view,設置在iCarousel上顯示的內容,

- (void)carouselDidEndScrollingAnimation:(iCarousel *),可以獲得icarousel上停止滑動時當前的元素,因為iCarousel有一個屬性是currentItemIndex。

- (CGFloat)carousel:(iCarousel *)carousel valueForOption:(iCarouselOption)option withDefault:(CGFloat)value,可以設置iCarousel的一些屬性,比如說可以設置可滑動顯示的內容可以循環顯示,還可以設置每個元素的高度,每次顯示多少個元素等等。

 

 

 

例子demo

 

- (void)viewDidLoad {

    [super viewDidLoad];

    // Do any additional setup after loading the view, typically from a nib.

    

//    實例化旋轉木馬視圖

    self.carousel = [[iCarousel alloc]initWithFrame:CGRectMake(0, 100, CGRectGetWidth(self.view.frame), 200)];

    

    self.carousel.dataSource = self;

    self.carousel.delegate = self;

//    🎠的樣式

    self.carousel.type = iCarouselTypeCoverFlow2;

    

    self.carousel.backgroundColor = [UIColor redColor];

    

    [self.view addSubview:self.carousel];

    

}

 

//🎠有多少個元素

- (NSInteger)numberOfItemsInCarousel:(iCarousel *)carousel{

 

    return 10;

 

}

 

//🎠具體添加的什么視圖

- (UIView *)carousel:(iCarousel *)carousel viewForItemAtIndex:(NSInteger)index reusingView:(UIView *)view{

 

    UIButton *button = (UIButton *)view;

    if (button == nil)

    {

        //no button available to recycle, so create new one

        UIImage *image = [UIImage imageNamed:@"page.png"];

        button = [UIButton buttonWithType:UIButtonTypeCustom];

        button.frame = CGRectMake(0.0f, 0.0f, image.size.width, image.size.height);

        [button setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];

        [button setBackgroundImage:image forState:UIControlStateNormal];

        button.titleLabel.font = [button.titleLabel.font fontWithSize:50];

        [button addTarget:self action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside];

    }

    

    //set button label

    [button setTitle:[NSString stringWithFormat:@"%zd", index] forState:UIControlStateNormal];

    

    return button;

 

 

}

 

 

- (void)buttonTapped:(UIButton *)sender

{

    //get item index for button

    NSInteger index = [self.carousel indexOfItemViewOrSubview:sender];

    

    [[[UIAlertView alloc] initWithTitle:@"Button Tapped"

                                message:[NSString stringWithFormat:@"You tapped button number %zd", index]

                               delegate:nil

                      cancelButtonTitle:@"OK"

                      otherButtonTitles:nil] show];

}

 

  

 

參考網址

http://www.jcodecraeer.com/IOS/2015/0928/3533.html

http://blog.csdn.net/u010850094/article/details/51679614

iCarousel詳解:http://www.th7.cn/Program/IOS/201606/879337.shtml

http://blog.sina.com.cn/s/blog_9da8030601011gcr.html

icarousel旋轉木馬的詳解--當前itemscale和alpha:http://blog.csdn.net/springjustin/article/details/51900245

 

其他:

iOS9馬上要發布了 為了我司APP的兼容性問題 特意把手上的iOS Mac XCode都升級到了最新的beta版 然后發現iOS9的多任務管理器風格大變 變成了下面這種樣子

pic_001.gif

我忽然想起來之前的文章提到我最愛的UI控件iCarousel要實現類似這種效果其實是很簡單的 一時興起就花時間試驗了一下 效果還不錯 所以接下來我就介紹一下iCarousel的高級用法: 如何使用iCarousel的自定義方式來實現iOS9的多任務管理器效果

模型

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

1438572789606303.jpg

然后我們簡單的來建個模 這個步驟很重要 將會影響我們之后的計算 首先我們把東西擺正

pic_003.png

然后按比例用線分割一下

pic_004.png

這里可以看到 如果我們以正中間的卡片(設定序號為0)為參照物的話 最右邊卡片(序號為1)的位移就是中心卡片寬度的4/5 最左邊的卡片(序號為-2)的位移就是中心卡片的寬度的2/5 注意:這兩個值的確定對我們非常重要

而大小*的縮放 就按照線性放大**就行了 由於計算很簡單 這里就不多贅述了

細心的人可能會注意到 其實iOS9中的中心卡片 並不是居中的 而是靠右的 那么我們再把整體布局調整一下

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的用法 可能更方便)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM