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

前言

iCarousel一个简单、可高度定制的3D CoverFlow开源类库,旨在简化在 iPhone, iPad和Mac OS中生成各种类型的cover flow(视图切换)效果(分页、滚动视图)。用户手指划动图片,图片将不断以3D的形式切换。

iCarousel

 

 

 

Github托管地址:https://github.com/nicklockwood/iCarousel

说明

      iOS开发中如果想要你实现滑动效果,可以使用icarousel这个第三方库,还比较好用,感觉用起来有点像tableView。在网上下载这个库,然后在界面文件中,希望混动列表的部分加上一个view,将view的类设为iCarousel,然后设置delegate和dataSource,并且在自己的controller类中,声明要使用iCarousel的协议,包括delegate和datasource的。

          然后就是要实现的协议了,- (NSUInteger)numberOfItemsInCarousel:(iCarousel *)carousel,设置显示在iCarousel上的元素的个数,

- (UIView *)carousel:(iCarousel *)carousel viewForItemAtIndex:(NSUInteger)index reusingView:(UIView *)view,设置在iCarousel上显示的内容,

- (void)carouselDidEndScrollingAnimation:(iCarousel *),可以获得icarousel上停止滑动时当前的元素,因为iCarousel有一个属性是currentItemIndex。

- (CGFloat)carousel:(iCarousel *)carousel valueForOption:(iCarouselOption)option withDefault:(CGFloat)value,可以设置iCarousel的一些属性,比如说可以设置可滑动显示的内容可以循环显示,还可以设置每个元素的高度,每次显示多少个元素等等。

 

 

 

例子demo

 

- (void)viewDidLoad {

    [super viewDidLoad];

    // Do any additional setup after loading the view, typically from a nib.

    

//    实例化旋转木马视图

    self.carousel = [[iCarousel alloc]initWithFrame:CGRectMake(0, 100, CGRectGetWidth(self.view.frame), 200)];

    

    self.carousel.dataSource = self;

    self.carousel.delegate = self;

//    🎠的样式

    self.carousel.type = iCarouselTypeCoverFlow2;

    

    self.carousel.backgroundColor = [UIColor redColor];

    

    [self.view addSubview:self.carousel];

    

}

 

//🎠有多少个元素

- (NSInteger)numberOfItemsInCarousel:(iCarousel *)carousel{

 

    return 10;

 

}

 

//🎠具体添加的什么视图

- (UIView *)carousel:(iCarousel *)carousel viewForItemAtIndex:(NSInteger)index reusingView:(UIView *)view{

 

    UIButton *button = (UIButton *)view;

    if (button == nil)

    {

        //no button available to recycle, so create new one

        UIImage *image = [UIImage imageNamed:@"page.png"];

        button = [UIButton buttonWithType:UIButtonTypeCustom];

        button.frame = CGRectMake(0.0f, 0.0f, image.size.width, image.size.height);

        [button setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];

        [button setBackgroundImage:image forState:UIControlStateNormal];

        button.titleLabel.font = [button.titleLabel.font fontWithSize:50];

        [button addTarget:self action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside];

    }

    

    //set button label

    [button setTitle:[NSString stringWithFormat:@"%zd", index] forState:UIControlStateNormal];

    

    return button;

 

 

}

 

 

- (void)buttonTapped:(UIButton *)sender

{

    //get item index for button

    NSInteger index = [self.carousel indexOfItemViewOrSubview:sender];

    

    [[[UIAlertView alloc] initWithTitle:@"Button Tapped"

                                message:[NSString stringWithFormat:@"You tapped button number %zd", index]

                               delegate:nil

                      cancelButtonTitle:@"OK"

                      otherButtonTitles:nil] show];

}

 

  

 

参考网址

http://www.jcodecraeer.com/IOS/2015/0928/3533.html

http://blog.csdn.net/u010850094/article/details/51679614

iCarousel详解:http://www.th7.cn/Program/IOS/201606/879337.shtml

http://blog.sina.com.cn/s/blog_9da8030601011gcr.html

icarousel旋转木马的详解--当前itemscale和alpha:http://blog.csdn.net/springjustin/article/details/51900245

 

其他:

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作为其类型

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

 

posted on 2016-11-13 22:07  dreamDeveloper  阅读(2710)  评论(0编辑  收藏  举报

导航