前面,咱們將佈局由線性的瀑布流佈局擴展到了圓環佈局,這使咱們使用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