前面,咱们将布局由线性的瀑布流布局扩展到了圆环布局,这使咱们使用UICollectionView的布局思路大大迈进了一步,此次,咱们玩的更加炫一些,想办法将布局应用的空间,你是否还记得,在管理布局的item的具体属性的类UICollectionViewLayoutAttributrs类中,有transform3D这个属性,经过这个属性的设置,咱们真的能够在空间的坐标系中进行布局设计。iOS系统的控件中,也并不是没有这样的先例,UIPickerView就是很好的一个实例,这篇博客,咱们就经过使用UICollectionView实现一个相似系统的UIPickerView的布局视图,来体会UICollectionView在3D控件布局的魅力。系统的pickerView效果以下:数组
万丈的高楼也是由一砖一瓦堆砌而成,在咱们彻底模拟系统pickerView前,咱们应该先将视图的布局摆放这一问题解决。咱们依然来建立一个类,继承于UICollectionViewLayout:dom
@interface MyLayout : UICollectionViewLayout @end
对于.m文件的内容,前几篇博客中咱们都是在prepareLayout中进行布局的静态设置,那是由于咱们前几篇博客中的布局都是静态的,布局并不会随着咱们的手势操做而发生太大的变化,所以咱们所有在prepareLayout中一次配置完了。而咱们此次要讨论的布局则不一样,pickerView会随着咱们手指的拖动而进行滚动,所以UICollectionView中的每个item的布局是在不断变化的,因此此次,咱们采用动态配置的方式,在layoutAttributesForItemAtIndexPath方法中进行每一个item的布局属性设置。函数
至于layoutAttributesForItemAtIndexPath方法,它也是UICollectionViewLayout类中的方法,用于咱们自定义时进行重写,至于为何动态布局要在这里面配置item的布局属性,后面咱们会了解到。布局
在编写咱们的布局类以前,先作好准备工做,在viewController中,实现以下代码:spa
- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. MyLayout * layout = [[MyLayout alloc]init]; UICollectionView * collect = [[UICollectionView alloc]initWithFrame:CGRectMake(0, 0, 320, 400) collectionViewLayout:layout]; collect.delegate=self; collect.dataSource=self; [collect registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"cellid"]; [self.view addSubview:collect]; } -(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView{ return 1; } -(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{ return 10; } -(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{ UICollectionViewCell * cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"cellid" forIndexPath:indexPath]; cell.backgroundColor = [UIColor colorWithRed:arc4random()%255/255.0 green:arc4random()%255/255.0 blue:arc4random()%255/255.0 alpha:1]; UILabel * label = [[UILabel alloc]initWithFrame:CGRectMake(0, 0, 250, 80)]; label.text = [NSString stringWithFormat:@"我是第%ld行",(long)indexPath.row]; [cell.contentView addSubview:label]; return cell; }
上面我建立了10个Item,而且在每一个Item上添加了一个标签,标写是第几行。设计
在咱们自定义的布局类中重写layoutAttributesForElementsInRect,在其中返回咱们的布局数组:代理
-(NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect{ NSMutableArray * attributes = [[NSMutableArray alloc]init]; //遍历设置每一个item的布局属性 for (int i=0; i<[self.collectionView numberOfItemsInSection:0]; i++) { [attributes addObject:[self layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:i inSection:0]]]; } return attributes; }
以后,在咱们布局类中重写layoutAttributesForItemAtIndexPath方法:code
-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath{ //建立一个item布局属性类 UICollectionViewLayoutAttributes * atti = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]; //获取item的个数 int itemCounts = (int)[self.collectionView numberOfItemsInSection:0]; //设置每一个item的大小为260*100 atti.size = CGSizeMake(260, 100); /* 后边介绍的代码添加在这里 */ return atti; }
上面的代码中,咱们什么都没有作,下面咱们一步步来实现3D的滚轮效果。orm
首先,咱们先将全部的item的位置都设置为collectionView的中心:对象
atti.center = CGPointMake(self.collectionView.frame.size.width/2, self.collectionView.frame.size.height/2);
这时,若是咱们运行程序的话,全部item都将一层层贴在屏幕的中央,以下:
很丑对吧,以后咱们来设置每一个item的3D效果,在上面的布局方法中添加以下代码:
//建立一个transform3D类 //CATransform3D是一个相似矩阵的结构体 //CATransform3DIdentity建立空得矩阵 CATransform3D trans3D = CATransform3DIdentity; //这个值设置的是透视度,影响视觉离投影平面的距离 trans3D.m34 = -1/900.0; //下面这些属性 后面会具体介绍 //这个是3D滚轮的半径 CGFloat radius = 50/tanf(M_PI*2/itemCounts/2); //计算每一个item应该旋转的角度 CGFloat angle = (float)(indexPath.row)/itemCounts*M_PI*2; //这个方法返回一个新的CATransform3D对象,在原来的基础上进行旋转效果的追加 //第一个参数为旋转的弧度,后三个分别对应x,y,z轴,咱们须要以x轴进行旋转 trans3D = CATransform3DRotate(trans3D, angle, 1.0, 0, 0); //进行设置 atti.transform3D = trans3D;
对于上面的radius属性,运用了一些简单的几何和三角函数的知识。若是咱们将系统的pickerView沿着y轴旋转90°,你会发现侧面的它是一个规则的正多边形,这里的radius就是这个多边形中心到其边的垂直距离,也是内切圆的半径,全部的item拼成了一个正多边形,示例以下:
经过简单的数学知识,h/2弦对应的角的弧度为2*pi/(边数)/2,在根据三角函数相关知识可知,这个角的正切值为h/2/radius,这就是咱们radius的由来。
对于angle属性,它是每个item的x轴旋转度数,若是咱们将全部item的中心都放在一点,经过旋转让它们散开以下图所示:
每一个item旋转的弧度就是其索引/(2*pi)。
经过上面的设置,咱们再运行代码,效果以下:
仔细观察咱们能够发现,item以x中轴线进行了旋转平均布局,侧面的效果就是咱们上面的简笔画那样,下面要进行咱们的第三步了,将这个item,所有沿着其Z轴向前拉,就能够成为咱们滚轮的效果,示例图以下:
咱们继续在刚才的代码后面添加这行代码:
//这个方法也返回一个transform3D对象,追加平移效果,后面三个参数,对应平移的x,y,z轴,咱们沿z轴平移 trans3D = CATransform3DTranslate(trans3D, 0, 0, radius);
再次运行,效果以下:
布局的效果咱们已经完成了,离成功很近了对吧,只是如今的布局是静态的,咱们不能滑动这个滚轮,咱们还须要用动态滑动作一些处理。
经过上面的努力,咱们已经静态布局出了一个相似pickerView的滚轮,如今咱们再来添加滑动滚动的效果
首先,咱们须要给collectionView一个滑动的范围,咱们以一屏collectionView的滑动距离来当作滚轮滚动一下的参照,咱们在布局类中的以下方法中返回滑动区域:
-(CGSize)collectionViewContentSize{ return CGSizeMake(self.collectionView.frame.size.width, self.collectionView.frame.size.height*[self.collectionView numberOfItemsInSection:0]); }
这时咱们的collectionView已经能够进行滑动,可是并非咱们想要的效果,滚轮并无滚动,而是随着滑动出了屏幕,所以,咱们须要在滑动的时候不停的动态布局,将滚轮始终固定在collectionView的中心,先须要在布局类中实现以下方法:
//返回yes,则一有变化就会刷新布局 -(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{ return YES; }
将上面的布局的中心点设置加上一个动态的偏移量:
atti.center = CGPointMake(self.collectionView.frame.size.width/2, self.collectionView.frame.size.height/2+self.collectionView.contentOffset.y);
如今在运行,会发现滚轮会随着滑动始终固定在中间,可是仍是不如人意,滚轮并无转动起来,咱们还须要动态的设置每一个item的旋转角度,这样连续看起来,滚轮就转了起来,在上面设置布局的方法中,咱们在添加一些处理:
//获取当前的偏移量 float offset = self.collectionView.contentOffset.y; //在角度设置上,添加一个偏移角度 float angleOffset = offset/self.collectionView.frame.size.height; CGFloat angle = (float)(indexPath.row+angleOffset)/itemCounts*M_PI*2;
再看看效果,没错,就是这么简单,滚轮已经转了起来。
咱们再进一步,若是滚动能够循环,这个控件将更加炫酷,添加这样的逻辑也很简单,经过监测scrollView的偏移量,咱们能够对齐进行处理,由于collectionView继承于scrollView,咱们能够直接在ViewController中实现其代理方法,以下:
-(void)scrollViewDidScroll:(UIScrollView *)scrollView{ //小于半屏 则放到最后一屏多半屏 if (scrollView.contentOffset.y<200) { scrollView.contentOffset = CGPointMake(0, scrollView.contentOffset.y+10*400); //大于最后一屏多一屏 放回第一屏 }else if(scrollView.contentOffset.y>11*400){ scrollView.contentOffset = CGPointMake(0, scrollView.contentOffset.y-10*400); } }
由于我们的环状布局,上面的逻辑恰好能够无缝对接,可是会有新的问题,一开始运行,滚轮就是出如今最后一个item的位置,而不是第一个,而且有些相关的地方,咱们也须要一些适配:
在viewController中:
//一开始将collectionView的偏移量设置为1屏的偏移量 collect.contentOffset = CGPointMake(0, 400);
在layout类中:
//将滚动范围设置为(item总数+2)*每屏高度 -(CGSize)collectionViewContentSize{ return CGSizeMake(self.collectionView.frame.size.width, self.collectionView.frame.size.height*([self.collectionView numberOfItemsInSection:0]+2)); }
//将计算的具体item角度向前递推一个 CGFloat angle = (float)(indexPath.row+angleOffset-1)/itemCounts*M_PI*2;
OK,咱们终于大功告成了,能够发现,实现这样一个布局效果炫酷的控件,代码其实并无多少,相比,数学逻辑要比编写代码自己困难,这十分相似数学中的几何问题,若是你弄清了逻辑,解决是分分钟的事,咱们能够经过这样的一个思路,设计更多3D或者平面特效的布局方案,抽奖的转动圆盘,书本的翻页,甚至立体的标签云,UICollectionView均可以实现,这篇博客中的代码在下面的链接中,疏漏之处,欢迎指正!
http://pan.baidu.com/s/1jGCmbKM
专一技术,热爱生活,交流技术,也作朋友。
——珲少 QQ群:203317592