怎样轻松实现iOS9多任务管理器效果(iCarousel高级教程)

转自:http://www.cocoachina.com/ios/20150804/12878.html

iOS9立即要公布了 为了我司APP的兼容性问题 特意把手上的iOS Mac XCode都升级到了最新的beta版 然后发现iOS9的多任务管理器风格大变 变成了以下这样的样子

我忽然想起来之前的文章提到我最爱的UI控件iCarousel要实现类似这样的效果事实上是非常easy的 一时兴起就花时间试验了一下 效果还不错 所以接下来我就介绍一下iCarousel的高级使用方法: 怎样使用iCarousel的自己定义方式来实现iOS9的多任务管理器效果

模型

首先来看一下iOS9的多任务管理器到底是什么样子

然后我们简单的来建个模 这个步骤非常重要 将会影响我们之后的计算 首先我们把东西摆正

然后按比例用线切割一下

这里能够看到 假设我们以正中间的卡片(设定序号为0)为參照物的话 最右边卡片(序号为1)的位移就是中心卡片宽度的4/5 最左边的卡片(序号为-2)的位移就是中心卡片的宽度的2/5 注意:这两个值的确定对我们很重要

而大小*的缩放 就依照线性放大**即可了 因为计算非常easy 这里就不多赘述了

细心的人可能会注意到 事实上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

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

时间: 2024-10-08 13:38:26

怎样轻松实现iOS9多任务管理器效果(iCarousel高级教程)的相关文章

如何轻松实现iOS9多任务管理器效果(iCarousel高级教程)

本文转载至 http://adad184.com/2015/08/01/advanced-icarousel-tutorial-copycat-of-ios9-task-tray/ 前言 iOS9马上要发布了 为了我司APP的兼容性问题 特意把手上的iOS Mac XCode都升级到了最新的beta版 然后发现iOS9的多任务管理器风格大变 变成了下面这种样子 我忽然想起来之前的文章提到我最爱的UI控件iCarousel要实现类似这种效果其实是很简单的 一时兴起就花时间试验了一下 效果还不错 所

MindManager渐变色效果设置方法教程

在MindManager中制作思维导图时,我们往往会通过一些功能工具来美化导图.那么MindManager思维导图怎么设置渐变色效果呢? MindManager中可以设置主题为渐变色的填充效果,但是,并不是通过使用填充工具.而是需要在"选项"中设置,因此,设置完成后,该效果将应用于整个导图. 1)打开MindManager 2017,单击"文件">"选项": 2)将打开的对话框切换至"视觉效果"视图: 3)找到"

swipe.js 轻松实现手机端滑动效果

插件下载地址 官网:http://www.swipejs.comgithub:https://github.com/bradbirdsall/Swipe 插件特色 swipe.js是一个比较有名的触摸滑动插件,它能够处理内容滑动,支持自定义选项,你可以让它自动滚动,控制滚动间隔,返回回调函数等.经常可见使用在移动开发中 使用方法 HTML代码如下: <div id='slider' class='swipe'> <div class='swipe-wrap'> <div>

强大的CSS:模拟下雪效果动画制作教程

下雪效果只是一类效果的名称,可以是红包雨等一些自由落体的运动效果,本文就是用纯css模拟下雪的效果,更多效果大家可以自行发挥. 1.前言 由于公司产品的活动,需要模拟类似下雪的效果.浏览器实现动画无非css3和canvas(还有gif),对比下css3和canvas的优缺点: 动画自由度:canvas胜: 复杂度:canvas胜: 兼容性:canvas胜: 性能:css3胜(requestAnimationFrame和硬件加速). 由于对于性能有一定的要求,canvas对比css3会有更多的计算

CSS:模拟下雪效果动画制作教程

下雪效果只是一类效果的名称,可以是红包雨等一些自由落体的运动效果,本文就是用纯css模拟下雪的效果,更多效果大家可以自行发挥. 1.前言 由于公司产品的活动,需要模拟类似下雪的效果.浏览器实现动画无非css3和canvas(还有gif),对比下css3和canvas的优缺点: 动画自由度:canvas胜: 复杂度:canvas胜: 兼容性:canvas胜: 性能:css3胜(requestAnimationFrame和硬件加速). 由于对于性能有一定的要求,canvas对比css3会有更多的计算

TF+K8s轻松上手丨通过Kubernetes Ingress进行高级外部应用程序连接

本文所有相关链接pdf:https://tungstenfabric.org.cn/assets/uploads/files/tf-ceg-case-2.pdf Kubernetes的Ingress文档页面将其描述为: "用于管理对集群中服务的外部访问的API对象,通常是HTTP.Ingress可以提供负载均衡.SSL终结和基于名称的虚拟主机." CNI不提供Ingress功能.这意味着Kubernetes集群管理者通常要为其集群安装.管理和支持单独的Ingress控制器解决方案. 对

平安科技移动开发二队技术周报(第十四期)

平安科技移动开发二队技术周报(第十四期) @author ASCE1885的 Github 简书 微博 CSDN 业界新闻 1)Android Studio 1.3 正式发布 Android Studio 1.3 正式进入稳定版,求稳的同学该升级了.作为今年改进最大的一个版本,Studio 1.3 包括新的 memory profiler ,改进的测试支持,并且包含了完整的 C++ 编辑和调试功能. Android开发 1)使用O-LLVM和NDK对Android应用进行混淆 Android开发

iCarousel——在iOS和Mac OS应用中实现3D CoverFlow旋转木马效果的开源类库

前言 iCarousel一个简单.可高度定制的3D CoverFlow开源类库,旨在简化在 iPhone, iPad和Mac OS中生成各种类型的cover flow(视图切换)效果(分页.滚动视图).用户手指划动图片,图片将不断以3D的形式切换. Github托管地址:https://github.com/nicklockwood/iCarousel 说明 iOS开发中如果想要你实现滑动效果,可以使用icarousel这个第三方库,还比较好用,感觉用起来有点像tableView.在网上下载这个

iOS9新特性——堆叠视图UIStackView

iOS9新特性--堆叠视图UIStackView 一.引言 随着autolayout的推广开来,更多的app开始使用自动布局的方式来构建自己的UI系统,autolayout配合storyBoard和一些第三方的框架,对于创建约束来说,已经十分方便,但是对于一些动态的线性布局的视图,我们需要手动添加的约束不仅非常多,而且如果我们需要插入或者移除其中的一些UI元素的时候,我们又要做大量的修改约束的工作,UIStackView正好可以解决这样的问题. 二.在storyBoard上初识StackView