转自:http://www.cocoachina.com/ios/20150804/12878.htmlhtml
iOS9当即要公布了 为了我司APP的兼容性问题 特地把手上的iOS Mac XCode都升级到了最新的beta版 而后发现iOS9的多任务管理器风格大变 变成了如下这样的样子ios
我突然想起来以前的文章提到我最爱的UI控件iCarousel要实现相似这样的效果事实上是很是easy的 一时兴起就花时间试验了一下 效果还不错 因此接下来我就介绍一下iCarousel的高级使用方法: 怎样使用iCarousel的本身定义方式来实现iOS9的多任务管理器效果
git
模型github
首先来看一下iOS9的多任务管理器究竟是什么样子函数
而后咱们简单的来建个模 这个步骤很是重要 将会影响咱们以后的计算 首先咱们把东西摆正
布局
而后按比例用线切割一下
post
这里可以看到 假设咱们以正中间的卡片(设定序号为0)为參照物的话 最右边卡片(序号为1)的位移就是中心卡片宽度的4/5 最左边的卡片(序号为-2)的位移就是中心卡片的宽度的2/5 注意:这两个值的肯定对咱们很重要
学习
而大小*的缩放 就依照线性放大**便可了 由于计算很是easy 这里就很少赘述了动画
细心的人可能会注意到 事实上iOS9中的中心卡片 并不是居中的 而是靠右的 那么咱们再把整体布局调整一下atom
这样就几乎相同是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的使用方法 可能更方便)