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

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

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

pic_001.gif

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

模型github

首先来看一下iOS9的多任务管理器究竟是什么样子函数

1438572789606303.jpg

而后咱们简单的来建个模 这个步骤很是重要 将会影响咱们以后的计算 首先咱们把东西摆正
布局

pic_003.png

而后按比例用线切割一下
post

pic_004.png

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

而大小*的缩放 就依照线性放大**便可了 由于计算很是easy 这里就很少赘述了动画

细心的人可能会注意到 事实上iOS9中的中心卡片 并不是居中的 而是靠右的 那么咱们再把整体布局调整一下atom

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

相关文章
相关标签/搜索