你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 如何輕松實現iOS9多任務管理器效果(iCarousel高級教程)

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

編輯:IOS開發基礎

作者:裡脊串 授權本站轉載。

前言

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作為其類型

@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方法沒有實現

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

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

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

  • iCarousel並不是一個UIScrollView 也並沒有包含任何UIScrollView作為subView

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

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

  • iCarousel本身並不會改變itemView的位置 而是靠修改itemView的layer.transform來實現位移和形變

可能文字說得不太清楚 我們還是通過代碼來看一下

- (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

然後滑動的時候打出的日志是類似這樣的

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進行形變 通過形變來造成滑動的效果

這個非常大膽和另類的想法著實很奇妙! 可能我解釋得不夠好(盡力了~~) 還是通過代碼來解釋比較好

我們修改一下函數的實現

- (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

然後我們修改一下程序代碼(這段代碼其實就是本文的關鍵所在)

- (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的層級的呢? 查看源碼我們可以知道

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的各個屬性各代表什麼

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最簡單的辦法就是

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

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

那麼我們把之前iCarousel的delegate方法稍微改動一下 將當前的offset設置給最後一個參數即可(因為offset就是按順序傳進來的)

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

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved