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

来源:http://www.cnblogs.com/jgCho/archive/2016/03/14/5275408.html
-Advertisement-
Play Games

iOS9系統下 為了我司APP的相容性問題 特意把手上的iOS Mac XCode都升級到了最新的beta版 然後發現iOS9的多任務管理器風格大變 變成了下麵這種樣子 我忽然想起來之前的文章提到我最愛的UI控制項iCarousel要實現類似這種效果其實是很簡單的 一時興起就花時間試驗了一下 效果還不


前言


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

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

模型


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

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

然後按比例用線分割一下

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

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

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

這樣就差不多是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
60
61
62
63
64
65
66
@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
28
- (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
22
23
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.0000002015-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
47
48
49
- (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
38
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
8
9
10
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的用法 可能更方便)


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 簡歷非常能反映一個人的性格和水平,相比於你在學校獲得多少獎項,工作經歷、項目經 歷、熟悉的技術等更加關鍵,如果還有博客和一些 Github 上的項目,好感度++,但記得在去面試前收拾下,我們真的會挨個文件 review 你的開源代碼的。我們還喜歡關註一些細節,比如簡歷里關鍵字的拼寫,看似無關緊要但很
  • There are only two hard things in Computer Science: cache invalidation and naming things.在電腦科學中只有兩件難事:緩存失效和命名。 – Phil Karlton 電腦語言是人和電腦之間通訊的媒介。好的代碼
  • Android Material Design
  • 游戲項目尾聲,做下總結: 1.sharesdk微信微博分享(1) 如果接入眾多渠道,選用服務端獲取代碼配置參數的方式(微信:app_id 微博: app_key, app_secret)代碼配置2.x版本需註意setPlatformConfig設置參數時Android和ios設置key不同(Andr
  • 今天發現之前自己一直有個誤區,new Runnable(run()方法){}原來它不是一定創建一個線程 如果用主線程的handler去post(Runnable),他就不會創建子線程,而是在主線程上執行的Runnable方法 如果用new Thread(Runnable).start();那他就是在
  • 在網上找到了一篇總結的非常好的文章,我這裡就貼出他的博文地址。自己就不再寫這個方面的總結了。 "Activity與Fragment通信(99%)完美解決方案"
  • Android Material Design DrawerLayout和NavigationView及Palette
  • 本文詳細整理了 Cocoa 的 Runtime 系統的知識,它使得 Objective-C 如虎添翼,具備了靈活的動態特性,使這門古老的語言煥發生機。主要內容如下: 曾經覺得Objc特別方便上手,面對著 Cocoa 中大量 API,只知道簡單的查文檔和調用。還記得初學 Objective-C 時把[
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...