iOS之流布局UICollectionView全系列教程

一、簡介

UICollectionView是iOS6之后引入的一個新的UI控件,它和UITableView有著諸多的相似之處,其中許多代理方法都十分類似。簡單來說,UICollectionView是比UITbleView更加強大的一個UI控件,有如下幾個方面:

1、支持水平和垂直兩種方向的布局

2、通過layout配置方式進行布局

3、類似于TableView中的cell特性外,CollectionView中的Item大小和位置可以自由定義

4、通過layout布局回調的代理方法,可以動態的定制每個item的大小和collection的大體布局屬性

5、更加強大一點,完全自定義一套layout布局方案,可以實現意想不到的效果

這篇博客,我們主要討論CollectionView使用原生layout的方法和相關屬性,其他特點和更強的制定化,會在后面的博客中介紹

二、先來實現一個最簡單的九宮格類布局

在了解UICollectionView的更多屬性前,我們先來使用其進行一個最簡單的流布局試試看,在controller的viewDidLoad中添加如下代碼:

//創建一個layout布局類
UICollectionViewFlowLayout * layout = [[UICollectionViewFlowLayout alloc]init];
//設置布局方向為垂直流布局
layout.scrollDirection = UICollectionViewScrollDirectionVertical;
//設置每個item的大小為100*100
layout.itemSize = CGSizeMake(100, 100);
//創建collectionView 通過一個布局策略layout來創建
UICollectionView * collect = [[UICollectionView alloc]initWithFrame:self.view.frame collectionViewLayout:layout];
//代理設置
collect.delegate=self;
collect.dataSource=self;
//注冊item類型 這里使用系統的類型
[collect registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"cellid"];
   
[self.view addSubview:collect];

這里有一點需要注意,collectionView在完成代理回調前,必須注冊一個cell,類似如下:

[collect registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"cellid"];

這和tableView有些類似,又有些不同,因為tableView除了注冊cell的方法外,還可以通過臨時創建來做:


//tableView在從復用池中取cell的時候,有如下兩種方法
//使用這種方式如果復用池中無,是可以返回nil的,我們在臨時創建即可
- (nullable __kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier;
//6.0后使用如下的方法直接從注冊的cell類獲取創建,如果沒有注冊 會崩潰
- (__kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(6_0);

我們可以分析:因為UICollectionView是iOS6.0之前的新類,因此這里統一了從復用池中獲取cell的方法,沒有再提供可以返回nil的方式,并且在UICollectionView的回調代理中,只能使用從復用池中獲取cell的方式進行cell的返回,其他方式會崩潰,例如:


//這是正確的方法
-(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];
    return cell;
}
 
//這樣做會崩潰
-(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];
    UICollectionViewCell * cell = [[UICollectionViewCell alloc]init];
    return cell;
}

上面錯誤的方式會崩潰,信息如下,讓我們使用從復用池中取cell的方式:


上面的設置完成后,我們來實現如下幾個代理方法:

這里與TableView的回調方式十分類似


//返回分區個數
-(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView{
    return 1;
}
//返回每個分區的item個數
-(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
    return 10;
}
//返回每個item
-(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];
    return cell;
}

效果如下:

同樣,如果內容的大小超出一屏,和tableView類似是可以進行視圖滑動的。

還有一點細節,我們在上面設置布局方式的時候設置了垂直布局:

layout.scrollDirection = UICollectionViewScrollDirectionVertical;
//這個是水平布局
//layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;

這樣系統會在一行充滿后進行第二行的排列,如果設置為水平布局,則會在一列充滿后,進行第二列的布局,這種方式也被稱為流式布局

三、UICollectionView中的常用方法和屬性

//通過一個布局策略初識化CollectionView
- (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout;
 
//獲取和設置collection的layout
@property (nonatomic, strong) UICollectionViewLayout *collectionViewLayout;
 
//數據源和代理
@property (nonatomic, weak, nullable) id <UICollectionViewDelegate> delegate;
@property (nonatomic, weak, nullable) id <UICollectionViewDataSource> dataSource;
 
//從一個class或者xib文件進行cell(item)的注冊
- (void)registerClass:(nullable Class)cellClass forCellWithReuseIdentifier:(NSString *)identifier;
- (void)registerNib:(nullable UINib *)nib forCellWithReuseIdentifier:(NSString *)identifier;
 
//下面兩個方法與上面相似,這里注冊的是頭視圖或者尾視圖的類
//其中第二個參數是設置 頭視圖或者尾視圖 系統為我們定義好了這兩個字符串
//UIKIT_EXTERN NSString *const UICollectionElementKindSectionHeader NS_AVAILABLE_IOS(6_0);
//UIKIT_EXTERN NSString *const UICollectionElementKindSectionFooter NS_AVAILABLE_IOS(6_0);
- (void)registerClass:(nullable Class)viewClass forSupplementaryViewOfKind:(NSString *)elementKind withReuseIdentifier:(NSString *)identifier;
- (void)registerNib:(nullable UINib *)nib forSupplementaryViewOfKind:(NSString *)kind withReuseIdentifier:(NSString *)identifier;
 
//這兩個方法是從復用池中取出cell或者頭尾視圖
- (__kindof UICollectionViewCell *)dequeueReusableCellWithReuseIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath;
- (__kindof UICollectionReusableView *)dequeueReusableSupplementaryViewOfKind:(NSString *)elementKind withReuseIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath;
 
//設置是否允許選中 默認yes
@property (nonatomic) BOOL allowsSelection;
 
//設置是否允許多選 默認no
@property (nonatomic) BOOL allowsMultipleSelection;
 
//獲取所有選中的item的位置信息
- (nullable NSArray<NSIndexPath *> *)indexPathsForSelectedItems; 
 
//設置選中某一item,并使視圖滑動到相應位置,scrollPosition是滑動位置的相關參數,如下:
/*
typedef NS_OPTIONS(NSUInteger, UICollectionViewScrollPosition) {
    //無
    UICollectionViewScrollPositionNone                 = 0,
    //垂直布局時使用的 對應上中下
    UICollectionViewScrollPositionTop                  = 1 << 0,
    UICollectionViewScrollPositionCenteredVertically   = 1 << 1,
    UICollectionViewScrollPositionBottom               = 1 << 2,
    //水平布局時使用的  對應左中右
    UICollectionViewScrollPositionLeft                 = 1 << 3,
    UICollectionViewScrollPositionCenteredHorizontally = 1 << 4,
    UICollectionViewScrollPositionRight                = 1 << 5
};
*/
- (void)selectItemAtIndexPath:(nullable NSIndexPath *)indexPath animated:(BOOL)animated scrollPosition:(UICollectionViewScrollPosition)scrollPosition;
 
//將某一item取消選中
- (void)deselectItemAtIndexPath:(NSIndexPath *)indexPath animated:(BOOL)animated;
 
//重新加載數據
- (void)reloadData;
 
//下面這兩個方法,可以重新設置collection的布局,后面的方法多了一個布局完成后的回調,iOS7后可以用
//使用這兩個方法可以產生非常炫酷的動畫效果
- (void)setCollectionViewLayout:(UICollectionViewLayout *)layout animated:(BOOL)animated;
- (void)setCollectionViewLayout:(UICollectionViewLayout *)layout animated:(BOOL)animated completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(7_0);
 
//下面這些方法更加強大,我們可以對布局更改后的動畫進行設置
//這個方法傳入一個布局策略layout,系統會開始進行布局渲染,返回一個UICollectionViewTransitionLayout對象
//這個UICollectionViewTransitionLayout對象管理動畫的相關屬性,我們可以進行設置
- (UICollectionViewTransitionLayout *)startInteractiveTransitionToCollectionViewLayout:(UICollectionViewLayout *)layout completion:(nullable UICollectionViewLayoutInteractiveTransitionCompletion)completion NS_AVAILABLE_IOS(7_0);
//準備好動畫設置后,我們需要調用下面的方法進行布局動畫的展示,之后會調用上面方法的block回調
- (void)finishInteractiveTransition NS_AVAILABLE_IOS(7_0);
//調用這個方法取消上面的布局動畫設置,之后也會進行上面方法的block回調
- (void)cancelInteractiveTransition NS_AVAILABLE_IOS(7_0);
 
//獲取分區數
- (NSInteger)numberOfSections;
 
//獲取某一分區的item數
- (NSInteger)numberOfItemsInSection:(NSInteger)section;
 
//下面兩個方法獲取item或者頭尾視圖的layout屬性,這個UICollectionViewLayoutAttributes對象
//存放著布局的相關數據,可以用來做完全自定義布局,后面博客會介紹
- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath;
- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath;
 
//獲取某一點所在的indexpath位置
- (nullable NSIndexPath *)indexPathForItemAtPoint:(CGPoint)point;
 
//獲取某個cell所在的indexPath
- (nullable NSIndexPath *)indexPathForCell:(UICollectionViewCell *)cell;
 
//根據indexPath獲取cell
- (nullable UICollectionViewCell *)cellForItemAtIndexPath:(NSIndexPath *)indexPath;
 
//獲取所有可見cell的數組
- (NSArray<__kindof UICollectionViewCell *> *)visibleCells;
 
//獲取所有可見cell的位置數組
- (NSArray<NSIndexPath *> *)indexPathsForVisibleItems;
 
//下面三個方法是iOS9中新添加的方法,用于獲取頭尾視圖
- (UICollectionReusableView *)supplementaryViewForElementKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(9_0);
- (NSArray<UICollectionReusableView *> *)visibleSupplementaryViewsOfKind:(NSString *)elementKind NS_AVAILABLE_IOS(9_0);
- (NSArray<NSIndexPath *> *)indexPathsForVisibleSupplementaryElementsOfKind:(NSString *)elementKind NS_AVAILABLE_IOS(9_0);
 
//使視圖滑動到某一位置,可以帶動畫效果
- (void)scrollToItemAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(UICollectionViewScrollPosition)scrollPosition animated:(BOOL)animated;
 
//下面這些方法用于動態添加,刪除,移動某些分區獲取items
- (void)insertSections:(NSIndexSet *)sections;
- (void)deleteSections:(NSIndexSet *)sections;
- (void)reloadSections:(NSIndexSet *)sections;
- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection;
 
- (void)insertItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths;
- (void)deleteItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths;
- (void)reloadItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths;
- (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath;

iOS流布局UICollectionView系列二——UICollectionView的代理方法

一、引言

在上一篇博客中,介紹了最基本的UICollectionView的使用和其中我們常用的屬性和方法,也介紹了瀑布流布局的過程與思路,這篇博客來討論關于UICollectionView的代理方法的使用。

二、UICollectionViewDataSource協議

這個協議主要用于collectionView相關數據的處理,包含方法如下:

首先,有兩個方法是我們必須實現的:

設置每個分區的Item個數

  • (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section;

設置返回每個item的屬性

  • (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath;

下面的方法是可選實現的:

雖然這個方法是可選的,一般我們都會去實現,設置分區數

  • (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView;

對頭視圖或者尾視圖進行設置

  • (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath;

設置某個item是否可以被移動,返回NO則不能移動

  • (BOOL)collectionView:(UICollectionView *)collectionView canMoveItemAtIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(9_0);

移動item的時候,會調用這個方法

  • (void)collectionView:(UICollectionView *)collectionView moveItemAtIndexPath:(NSIndexPath )sourceIndexPath toIndexPath:(NSIndexPath)destinationIndexPath;

三、UICollectionViewDelegate協議

這個協議用來設置和處理collectionView的功能和一些邏輯,所有方法都是可選實現:

是否允許某個Item的高亮,返回NO,則不能進入高亮狀態

  • (BOOL)collectionView:(UICollectionView *)collectionView shouldHighlightItemAtIndexPath:(NSIndexPath *)indexPath;

當item高亮時觸發的方法

  • (void)collectionView:(UICollectionView *)collectionView didHighlightItemAtIndexPath:(NSIndexPath *)indexPath;

結束高亮狀態時觸發的方法

  • (void)collectionView:(UICollectionView *)collectionView didUnhighlightItemAtIndexPath:(NSIndexPath *)indexPath;

是否可以選中某個Item,返回NO,則不能選中

  • (BOOL)collectionView:(UICollectionView *)collectionView shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath;

是否可以取消選中某個Item

  • (BOOL)collectionView:(UICollectionView *)collectionView shouldDeselectItemAtIndexPath:(NSIndexPath *)indexPath;

已經選中某個item時觸發的方法

  • (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath;

取消選中某個Item時觸發的方法

  • (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath;

將要加載某個Item時調用的方法

  • (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(8_0);

將要加載頭尾視圖時調用的方法

  • (void)collectionView:(UICollectionView *)collectionView willDisplaySupplementaryView:(UICollectionReusableView *)view forElementKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(8_0);

已經展示某個Item時觸發的方法

  • (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath;

已經展示某個頭尾視圖時觸發的方法

  • (void)collectionView:(UICollectionView *)collectionView didEndDisplayingSupplementaryView:(UICollectionReusableView *)view forElementOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath;

這個方法設置是否展示長按菜單

  • (BOOL)collectionView:(UICollectionView *)collectionView shouldShowMenuForItemAtIndexPath:(NSIndexPath *)indexPath;

長按菜單中可以觸發一下類復制粘貼的方法,效果如下:

這個方法用于設置要展示的菜單選項

  • (BOOL)collectionView:(UICollectionView *)collectionView canPerformAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(nullable id)sender;

這個方法用于實現點擊菜單按鈕后的觸發方法,通過測試,只有copy,cut和paste三個方法可以使用

  • (void)collectionView:(UICollectionView *)collectionView performAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(nullable id)sender;

通過下面的方式可以將點擊按鈕的方法名打印出來:

-(void)collectionView:(UICollectionView *)collectionView performAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender{    
    NSLog(@"%@",NSStringFromSelector(action));
}

collectionView進行重新布局時調用的方法

  • (nonnull UICollectionViewTransitionLayout *)collectionView:(UICollectionView *)collectionView transitionLayoutForOldLayout:(UICollectionViewLayout *)fromLayout newLayout:(UICollectionViewLayout *)toLayout;

iOS流布局UICollectionView系列三——使用FlowLayout進行更靈活布局

一、引言

前面的博客介紹了UICollectionView的相關方法和其協議中的方法,但對布局的管理類UICollectionViewFlowLayout沒有著重探討,這篇博客介紹關于布局的相關設置和屬性方法。通過layout的設置,我們可以編寫更加靈活的布局效果。

二、將九宮格式的布局進行升級

在第一篇博客中,通過UICollectionView,我們很輕松的完成了一個九宮格的布局,但是如此中規中矩的布局方式,有時候并不能滿足我們的需求,有時我們需要每一個Item展示不同的大小,代碼如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc]init];
    layout.scrollDirection = UICollectionViewScrollDirectionVertical;
    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];
    
    
}
//設置每個item的大小,雙數的為50*50 單數的為100*100
-(CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath{
    if (indexPath.row%2==0) {
        return CGSizeMake(50, 50);
    }else{
        return CGSizeMake(100, 100);
    }
}
 
//代理相應方法
-(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView{
    return 1;
}
-(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
    return 100;
}
-(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];
    return cell;
}

效果如下:

現在的布局效果是不是炫酷了許多。

三、UICollectionViewFlowLayout相關屬性方法

UICollectionViewFlowLayout是系統提供給我們一個封裝好的流布局設置類,其中有一些布局屬性我們可以進行設置:

設置行與行之間的間距最小距離

@property (nonatomic) CGFloat minimumLineSpacing;

設置列與列之間的間距最小距離

@property (nonatomic) CGFloat minimumInteritemSpacing;

設置每個item的大小

@property (nonatomic) CGSize itemSize;

設置每個Item的估計大小,一般不需要設置

@property (nonatomic) CGSize estimatedItemSize NS_AVAILABLE_IOS(8_0);

設置布局方向

@property (nonatomic) UICollectionViewScrollDirection scrollDirection;

這個UICollectionViewScrollDirection的枚舉如下:

typedef NS_ENUM(NSInteger, UICollectionViewScrollDirection) {    
    UICollectionViewScrollDirectionVertical,//水平布局    
    UICollectionViewScrollDirectionHorizontal//垂直布局
};

設置頭視圖尺寸大小

@property (nonatomic) CGSize headerReferenceSize;

設置尾視圖尺寸大小

@property (nonatomic) CGSize footerReferenceSize;

設置分區的EdgeInset

@property (nonatomic) UIEdgeInsets sectionInset;

這個屬性可以設置分區的偏移量,例如我們在剛才的例子中添加如下設置:

 layout.sectionInset = UIEdgeInsetsMake(20, 20, 20, 20);

效果如下,會看到分區的邊界閃出了20像素

下面這兩個方法設置分區的頭視圖和尾視圖是否始終固定在屏幕上邊和下邊

@property (nonatomic) BOOL sectionHeadersPinToVisibleBounds NS_AVAILABLE_IOS(9_0);

@property (nonatomic) BOOL sectionFootersPinToVisibleBounds NS_AVAILABLE_IOS(9_0);

四、動態的配置layout的相關屬性UICollectionViewDelegateFlowLayout

上面的方法在創建FlowLayout時靜態的進行設置,如果我們需要動態的設置這些屬性,就像我們例子中的,每個item的大小會有差異,我們可以通過代理來實現。

UICollectionViewDelegateFlowLayout是UICollectionViewDelegate的子協議,其中常用方法如下,我們只需要實現我們需要的即可:

動態設置每個Item的尺寸大小

  • (CGSize)collectionView:(UICollectionView )collectionView layout:(UICollectionViewLayout)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath;

動態設置每個分區的EdgeInsets

  • (UIEdgeInsets)collectionView:(UICollectionView )collectionView layout:(UICollectionViewLayout)collectionViewLayout insetForSectionAtIndex:(NSInteger)section;

動態設置每行的間距大小

  • (CGFloat)collectionView:(UICollectionView )collectionView layout:(UICollectionViewLayout)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section;

動態設置每列的間距大小

  • (CGFloat)collectionView:(UICollectionView )collectionView layout:(UICollectionViewLayout)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section;

動態設置某個分區頭視圖大小

  • (CGSize)collectionView:(UICollectionView )collectionView layout:(UICollectionViewLayout)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section;

動態設置某個分區尾視圖大小

  • (CGSize)collectionView:(UICollectionView )collectionView layout:(UICollectionViewLayout)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section;

iOS流布局UICollectionView系列四——自定義FlowLayout進行瀑布流布局

一、引言

前幾篇博客從UICollectionView的基礎應用到設置UICollectionViewFlowLayout更加靈活的進行布局,但都限制在系統為我們準備好的布局框架中,還是有一些局限性,例如,如果我要進行瀑布流似的不定高布局,前面的方法就很難滿足我們的需求了,如下:

這種布局無疑在app的應用中更加廣泛,商品的展示,書架書目的展示,都會傾向于采用這樣的布局方式,當然,通過自定義FlowLayout,我們也很容易實現。

二、進行自定義瀑布流布局

首先,我們新建一個文件繼承于UICollectionViewFlowLayout:

@interface MyLayout : UICollectionViewFlowLayout

為了演示的方便,這里我不做更多的封裝,只添加一個屬性,直接讓外界將item個數傳遞進來,我們把重心方法重寫布局的方法上:

@interface MyLayout : UICollectionViewFlowLayout
@property(nonatomic,assign)int itemCount;
@end

前面說過,UICollectionViewFlowLayout是一個專門用來管理collectionView布局的類,因此,collectionView在進行UI布局前,會通過這個類的對象獲取相關的布局信息,FlowLayout類將這些布局信息全部存放在了一個數組中,數組中是UICollectionViewLayoutAttributes類,這個類是對item布局的具體設置,以后咱們在討論這個類。總之,FlowLayout類將每個item的位置等布局信息放在一個數組中,在collectionView布局時,會調用FlowLayout類layoutAttributesForElementsInRect:方法來獲取這個布局配置數組。因此,我們需要重寫這個方法,返回我們自定義的配置數組,另外,FlowLayout類在進行布局之前,會調用prepareLayout方法,所以我們可以重寫這個方法,在里面對我們的自定義配置數據進行一些設置。

簡單來說,自定義一個FlowLayout布局類就是兩個步驟:

1、設計好我們的布局配置數據 prepareLayout方法中

2、返回我們的配置數組 layoutAttributesForElementsInRect方法中

示例代碼如下:

@implementation MyLayout
{
    //這個數組就是我們自定義的布局配置數組
    NSMutableArray * _attributeAttay;
}
//數組的相關設置在這個方法中
//布局前的準備會調用這個方法
-(void)prepareLayout{
    _attributeAttay = [[NSMutableArray alloc]init];
    [super prepareLayout];
    //演示方便 我們設置為靜態的2列
    //計算每一個item的寬度
    float WIDTH = ([UIScreen mainScreen].bounds.size.width-self.sectionInset.left-self.sectionInset.right-self.minimumInteritemSpacing)/2;
    //定義數組保存每一列的高度
    //這個數組的主要作用是保存每一列的總高度,這樣在布局時,我們可以始終將下一個Item放在最短的列下面
    CGFloat colHight[2]={self.sectionInset.top,self.sectionInset.bottom};
    //itemCount是外界傳進來的item的個數 遍歷來設置每一個item的布局
    for (int i=0; i<_itemCount; i++) {
        //設置每個item的位置等相關屬性
        NSIndexPath *index = [NSIndexPath indexPathForItem:i inSection:0];
        //創建一個布局屬性類,通過indexPath來創建
        UICollectionViewLayoutAttributes * attris = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:index];
        //隨機一個高度 在40——190之間
        CGFloat hight = arc4random()%150+40;
        //哪一列高度小 則放到那一列下面
        //標記最短的列
        int width=0;
        if (colHight[0]<colHight[1]) {
            //將新的item高度加入到短的一列
            colHight[0] = colHight[0]+hight+self.minimumLineSpacing;
            width=0;
        }else{
            colHight[1] = colHight[1]+hight+self.minimumLineSpacing;
            width=1;
        }
        
        //設置item的位置
        attris.frame = CGRectMake(self.sectionInset.left+(self.minimumInteritemSpacing+WIDTH)*width, colHight[width]-hight-self.minimumLineSpacing, WIDTH, hight);
        [_attributeAttay addObject:attris];
    }
    
    //設置itemSize來確保滑動范圍的正確 這里是通過將所有的item高度平均化,計算出來的(以最高的列位標準)
    if (colHight[0]>colHight[1]) {
        self.itemSize = CGSizeMake(WIDTH, (colHight[0]-self.sectionInset.top)*2/_itemCount-self.minimumLineSpacing);
    }else{
          self.itemSize = CGSizeMake(WIDTH, (colHight[1]-self.sectionInset.top)*2/_itemCount-self.minimumLineSpacing);
    }
    
}
//這個方法中返回我們的布局數組
-(NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect{
    return _attributeAttay;
}
 
 
@end

自定義完成FlowLayout后,我們在ViewController中進行使用:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    MyLayout * layout = [[MyLayout alloc]init];
    layout.scrollDirection = UICollectionViewScrollDirectionVertical;
    layout.itemCount=100;
     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 100;
}
-(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];
    return cell;
}

運行效果就是我們引言中的截圖。

三、UICollectionViewLayoutAttributes類中我們可以配置的屬性

通過上面的例子,我們可以了解,collectionView的item布局其實是LayoutAttributes類具體配置的,這個類可以配置的布局屬性不止是frame這么簡單,其中還有許多屬性:

//配置item的布局位置
@property (nonatomic) CGRect frame;
//配置item的中心
@property (nonatomic) CGPoint center;
//配置item的尺寸
@property (nonatomic) CGSize size;
//配置item的3D效果
@property (nonatomic) CATransform3D transform3D;
//配置item的bounds
@property (nonatomic) CGRect bounds NS_AVAILABLE_IOS(7_0);
//配置item的旋轉
@property (nonatomic) CGAffineTransform transform NS_AVAILABLE_IOS(7_0);
//配置item的alpha
@property (nonatomic) CGFloat alpha;
//配置item的z坐標
@property (nonatomic) NSInteger zIndex; // default is 0
//配置item的隱藏
@property (nonatomic, getter=isHidden) BOOL hidden; 
//item的indexpath
@property (nonatomic, strong) NSIndexPath *indexPath;
//獲取item的類型
@property (nonatomic, readonly) UICollectionElementCategory representedElementCategory;
@property (nonatomic, readonly, nullable) NSString *representedElementKind; 
 
//一些創建方法
+ (instancetype)layoutAttributesForCellWithIndexPath:(NSIndexPath *)indexPath;
+ (instancetype)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind withIndexPath:(NSIndexPath *)indexPath;
+ (instancetype)layoutAttributesForDecorationViewOfKind:(NSString *)decorationViewKind withIndexPath:(NSIndexPath *)indexPath;

通過上面的屬性,可以布局出各式各樣的炫酷效果,正如一句話:沒有做不到,只有想不到。

iOS流布局UICollectionView系列五——圓環布局的實現

一、引言

前邊的幾篇博客,我們了解了UICollectionView的基本用法以及一些擴展,在不定高的瀑布流布局中,我們發現,可以通過設置具體的布局屬性類UICollectionViewLayoutAttributes來設置設置每個item的具體位置,我們可以再擴展一下,如果位置我們可以自由控制,那個布局我們也可以更加靈活,就比如創建一個如下的circleLayout:

這種布局方式在apple的官方文檔中也有介紹,是UICollectionView的一個應用示例。

二、設計一個圓環布局

先自定義一個layout類,這個類繼承于UICollectionViewLayout,UICollectionLayout是一個布局抽象基類,我們要使用自定義的布局方式,必須將其子類化,可能你還記得,我們在進行瀑布流布局的時候使用過UICollectionViewFlowLayout類,這個類就是繼承于UICollectionViewLayout類,系統為我們實現好的一個布局方案。

@interface MyLayout : UICollectionViewLayout
//這個int值存儲有多少個item
@property(nonatomic,assign)int itemCount;
@end

我們需要重寫這個類的三個方法,來進行圓環布局的設置,首先是prepareLayout,為布局做一些準備工作,使用collectionViewContentSize來設置內容的區域大小,最后使用layoutAttributesForElementsInRect方法來返回我們的布局信息字典,這個和前面瀑布流布局的思路是一樣的:

@implementation MyLayout
{
    NSMutableArray * _attributeAttay;
}
-(void)prepareLayout{
    [super prepareLayout];
    //獲取item的個數
    _itemCount = (int)[self.collectionView numberOfItemsInSection:0];
    _attributeAttay = [[NSMutableArray alloc]init];
    //先設定大圓的半徑 取長和寬最短的
    CGFloat radius = MIN(self.collectionView.frame.size.width, self.collectionView.frame.size.height)/2;
    //計算圓心位置
    CGPoint center = CGPointMake(self.collectionView.frame.size.width/2, self.collectionView.frame.size.height/2);
    //設置每個item的大小為50*50 則半徑為25
    for (int i=0; i<_itemCount; i++) {
        UICollectionViewLayoutAttributes * attris = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:[NSIndexPath indexPathForItem:i inSection:0]];
        //設置item大小
        attris.size = CGSizeMake(50, 50);
        //計算每個item的圓心位置
        /*
         .
         . .
         .   . r
         .     .
         .........
         */
        //計算每個item中心的坐標
        //算出的x y值還要減去item自身的半徑大小
        float x = center.x+cosf(2*M_PI/_itemCount*i)*(radius-25);
        float y = center.y+sinf(2*M_PI/_itemCount*i)*(radius-25);
     
        attris.center = CGPointMake(x, y);
        [_attributeAttay addObject:attris];
    }
    
   
    
}
//設置內容區域的大小
-(CGSize)collectionViewContentSize{
    return self.collectionView.frame.size;
}
//返回設置數組
-(NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect{
    return _attributeAttay;
}

在viewController中代碼如下:

- (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.layer.masksToBounds = YES;
    cell.layer.cornerRadius = 25;
    cell.backgroundColor = [UIColor colorWithRed:arc4random()%255/255.0 green:arc4random()%255/255.0 blue:arc4random()%255/255.0 alpha:1];
    return cell;
}

如上非常簡單的一些邏輯控制,我們就實現哦圓環布局,隨著item的多少,布局會自動調整,如果不是UICollectionView的功勞,實現這樣的功能,我們可能要寫上一陣子了_

iOS流布局UICollectionView系列六——將布局從平面應用到空間

一、引言

前面,我們將布局由線性的瀑布流布局擴展到了圓環布局,這使我們使用UICollectionView的布局思路大大邁進了一步,這次,我們玩的更加炫一些,想辦法將布局應用到空間。之前在管理布局的item的具體屬性的類UICollectionViewLayoutAttributrs類中,有transform3D這個屬性,通過這個屬性的設置,我們真的可以在空間的坐標系中進行布局設計。iOS系統的控件中,也并非沒有這樣的先例,UIPickerView就是很好的一個實例,這篇博客,我們就通過使用UICollectionView實現一個類似系統的UIPickerView的布局視圖,來體會UICollectionView在3D控件布局的魅力。系統的pickerView效果如下:

二、先來實現一個炫酷的滾輪空間布局

萬丈的高樓也是由一磚一瓦堆砌而成,在我們完全模擬系統pickerView前,我們應該先將視圖的布局擺放這一問題解決。我們依然來創建一個類,繼承于UICollectionViewLayout:

@interface MyLayout : UICollectionViewLayout 

@end

對于.m文件的內容,前幾篇博客中我們都是在prepareLayout中進行布局的靜態設置,那是因為我們前幾篇博客中的布局都是靜態的,布局并不會隨著我們的手勢操作而發生太大的變化,因此我們全部在prepareLayout中一次配置完了。而我們這次要討論的布局則不同,pickerView會隨著我們手指的拖動而進行滾動,因此UICollectionView中的每一個item的布局是在不斷變化的,所以這次,我們采用動態配置的方式,在layoutAttributesForItemAtIndexPath方法中進行每個item的布局屬性設置。

至于layoutAttributesForItemAtIndexPath方法,它也是UICollectionViewLayout類中的方法,用于我們自定義時進行重寫,至于為什么動態布局要在這里面配置item的布局屬性,后面我們會了解到。

在編寫我們的布局類之前,先做好準備工作,在viewController中,實現如下代碼:


- (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方法:

-(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的滾輪效果。

首先,我們先將所有的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

iOS流布局UICollectionView系列七——三維中的球型布局

一、引言

通過6篇的博客,從平面上最簡單的規則擺放的布局,到不規則的瀑布流布局,再到平面中的圓環布局,我們突破了線性布局的局限,在后面,我們將布局擴展到了空間,在Z軸上進行了平移,我們實現了一個類似UIPickerView的布局模型,其實我們還可以再進一步,類比于平面布局,picKerView只是線性排列布局在空間上的旋轉與平移,這次,我們更加充分了利用一下空間的尺寸,來設計一個圓球的布局模型。

二、將布局擴展為空間球型

在viewController中先實現一些準備代碼:

- (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.contentOffset = CGPointMake(320, 400);
    [collect registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"cellid"];
    [self.view addSubview:collect];
}
 
-(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView{
    return 1;
}
//我們返回30的標簽
-(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
    return 30;
}
-(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, 30, 30)];
    label.text = [NSString stringWithFormat:@"%ld",(long)indexPath.row];
    [cell.contentView addSubview:label];
    return cell;
}
 
- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}
//這里對滑動的contentOffset進行監控,實現循環滾動
-(void)scrollViewDidScroll:(UIScrollView *)scrollView{
    if (scrollView.contentOffset.y<200) {
        scrollView.contentOffset = CGPointMake(scrollView.contentOffset.x, scrollView.contentOffset.y+10*400);
    }else if(scrollView.contentOffset.y>11*400){
        scrollView.contentOffset = CGPointMake(scrollView.contentOffset.x, scrollView.contentOffset.y-10*400);
    }
    if (scrollView.contentOffset.x<160) {
        scrollView.contentOffset = CGPointMake(scrollView.contentOffset.x+10*320,scrollView.contentOffset.y);
    }else if(scrollView.contentOffset.x>11*320){
        scrollView.contentOffset = CGPointMake(scrollView.contentOffset.x-10*320,scrollView.contentOffset.y);
    }
}

這里面的代碼比較上一篇博客中的并沒有什么大的改動,只是做了橫坐標的兼容。

在我們的layout類中,將代碼修改成如下:

-(void)prepareLayout{
    [super prepareLayout];
    
}
//返回的滾動范圍增加了對x軸的兼容
-(CGSize)collectionViewContentSize{
    return CGSizeMake( self.collectionView.frame.size.width*([self.collectionView numberOfItemsInSection:0]+2), self.collectionView.frame.size.height*([self.collectionView numberOfItemsInSection:0]+2));
}
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{
    return YES;
}
 
-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath{
    UICollectionViewLayoutAttributes * atti = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
    //獲取item的個數
    int itemCounts = (int)[self.collectionView numberOfItemsInSection:0];
    atti.center = CGPointMake(self.collectionView.frame.size.width/2+self.collectionView.contentOffset.x, self.collectionView.frame.size.height/2+self.collectionView.contentOffset.y);
    atti.size = CGSizeMake(30, 30);
    
    CATransform3D trans3D = CATransform3DIdentity;
    trans3D.m34 = -1/900.0;
    
    CGFloat radius = 15/tanf(M_PI*2/itemCounts/2);
    //根據偏移量 改變角度
    //添加了一個x的偏移量
    float offsety = self.collectionView.contentOffset.y;
    float offsetx = self.collectionView.contentOffset.x;
    //分別計算偏移的角度
    float angleOffsety = offsety/self.collectionView.frame.size.height;
    float angleOffsetx = offsetx/self.collectionView.frame.size.width;
    CGFloat angle1 = (float)(indexPath.row+angleOffsety-1)/itemCounts*M_PI*2;
    //x,y的默認方向相反
    CGFloat angle2 = (float)(indexPath.row-angleOffsetx-1)/itemCounts*M_PI*2;
    //這里我們進行四個方向的排列
   if (indexPath.row%4==1) {
        trans3D = CATransform3DRotate(trans3D, angle1, 1.0,0, 0);
    }else if(indexPath.row%4==2){
        trans3D = CATransform3DRotate(trans3D, angle2, 0, 1, 0);
    }else if(indexPath.row%4==3){
        trans3D = CATransform3DRotate(trans3D, angle1, 0.5,0.5, 0);
    }else{
        trans3D = CATransform3DRotate(trans3D, angle1, 0.5,-0.5,0);
    }
    
    trans3D = CATransform3DTranslate(trans3D, 0, 0, radius);
    
    atti.transform3D = trans3D;
    return atti;
}
 
 
-(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;
}

布局效果如下:

滑動屏幕,這個圓球是可以進行滾動的。

TIP:這里我們只平均分配了四個方向上的布局,如果item更加小也更加多,我們可以分配到更多的方向上,使球體更加充實。

轉自https://blog.csdn.net/LVXIANGAN/article/details/73826108?utm_source=blogkpcl6

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 時間從來不是良藥,更妄論解藥。它是麻沸散嗎丁杜冷丁,是濃霧是灌木是摘下眼鏡的視界。時間不會給你結果,它只負...
    FantasyGallery閱讀 197評論 0 4
  • 文/Haijun 人機交互是研究人、計算機以及他們之間相互關系的技術。以人為中心,自然、親切和生動的交互成為發展新...
    Blue_Ocean閱讀 2,077評論 2 2
  • 現在的我,作什么都不認真,對什么都充滿好奇心,就是沉不下心來,好動,好玩,難道這就是青春期,好奇是好的,對新鮮事物...
    遠方的行人閱讀 157評論 0 0
  • 對于風,我有著別樣的情緒。它可以帶走我一切的煩惱,它可以帶我回到童真,它亦可以讓我的心靈純凈。 風猶...
    梅自香濃閱讀 186評論 0 0