iOS CollectionView 列表&網格之間切換(帶動畫)

原文地址: https://www.hlzhy.com/?p=57
前言:
最近在寫一個列表介面,這個列表能夠在列表和網格之間切換,這種需求算是比較常見的。本以為想我們是站在大牛的肩膀上程式設計,就去找了下度娘和谷哥,但是並沒有找到我想要的(找到的都是不帶動畫的切換)。既然做不了VC戰士,那就自己動手豐衣足食。在我看來, 所有的檢視變化都應該至少帶個簡單的過渡動畫 ,當然,過度使用華麗的動畫效果也會造成使用者的審美疲勞。“動畫有風險,使用需謹慎”。
依稀記得以前面試的時候被面試官問過這個問題,並被告知CollectionView自帶有列表和網格之間切換並且帶動畫的API。最終找到如下方法:
/** Summary Changes the collection view’s layout and optionally animates the change. Discussion This method makes the layout change without further interaction from the user. If you choose to animate the layout change, the animation timing and parameters are controlled by the collection view. */ - (void)setCollectionViewLayout:(UICollectionViewLayout *)layout animated:(BOOL)animated; // transition from one layout to another

最終效果(切換動畫&動畫慢放).gif
實現:
UIViewController.m
一、初始化UICollectionView
在當前控制器準備一個 BOOL
值 isList
,用來記錄當前選擇的是列表還是網格,準備兩個 UICollectionViewFlowLayout
對應列表和網格的佈局,設定一個 NOTIFIC_N_NAME
巨集,將此巨集作為 NotificationName ,稍後將以通知的方式通知Cell改變佈局。並且初始化UICollectionView。
@interface ViewController ()<UICollectionViewDelegate, UICollectionViewDataSource> @property (nonatomic, strong) UICollectionView *myCollectionView; @property (nonatomic, assign) BOOL isList; @property (nonatomic, strong) UICollectionViewFlowLayout *gridLayout; @property (nonatomic, strong) UICollectionViewFlowLayout *listLayout; @end #define NOTIFIC_N_NAME @"ViewController_changeList" @implementation ViewController -(UICollectionViewFlowLayout *)gridLayout{ if (!_gridLayout) { _gridLayout = [[UICollectionViewFlowLayout alloc] init]; CGFloat width = (self.view.frame.size.width - 5) * 0.5; _gridLayout.itemSize = CGSizeMake(width, 200 + width); _gridLayout.minimumLineSpacing = 5; _gridLayout.minimumInteritemSpacing = 5; _gridLayout.sectionInset = UIEdgeInsetsZero; } return _gridLayout; } -(UICollectionViewFlowLayout *)listLayout{ if (!_listLayout) { _listLayout = [[UICollectionViewFlowLayout alloc] init]; _listLayout.itemSize = CGSizeMake(self.view.frame.size.width, 190); _listLayout.minimumLineSpacing = 0.5; _listLayout.sectionInset = UIEdgeInsetsZero; } return _listLayout; } - (void)viewDidLoad { [super viewDidLoad]; _myCollectionView = [[UICollectionView alloc]initWithFrame:self.view.bounds collectionViewLayout:self.gridLayout]; _myCollectionView.showsVerticalScrollIndicator = NO; _myCollectionView.backgroundColor = [UIColor grayColor]; _myCollectionView.delegate = self; _myCollectionView.dataSource = self; [self.view addSubview:_myCollectionView]; [self.myCollectionView registerClass:[HYChangeableCell class] forCellWithReuseIdentifier:@"HYChangeableCell"]; //...... }
二、實現UICollectionViewDataSource
建立UICollectionViewCell,給 cell.isList
賦值, 告訴Cell當前狀態,給 cell.notificationName
賦值,用以接收切換通知。
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { HYChangeableCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"HYChangeableCell" forIndexPath:indexPath]; cell.isList = _isList; cell.notificationName = NOTIFIC_N_NAME; return cell; }
三、點選切換按鈕
通過 setCollectionViewLayout:animated:
方法重新為CollectionView佈局,並將 animated
設為YES。但是僅僅這樣是不夠的,因為這樣並不會觸發 cellForItemAtIndexPath
方法。我們還需向Cell傳送通知告訴它“你需要改變佈局了”。
-(void)changeListButtonClick{ _isList = !_isList; if (_isList) { [self.myCollectionView setCollectionViewLayout:self.listLayout animated:YES]; }else{ [self.myCollectionView setCollectionViewLayout:self.gridLayout animated:YES]; } //[self.myCollectionView reloadData]; [[NSNotificationCenter defaultCenter] postNotificationName:NOTIFIC_N_NAME object:@(_isList)]; }
UICollectionViewCell.m
基本佈局程式碼這裡就不貼上來了,需要的請在文章最後自行下載Demo檢視。
!注意:因為這裡使用的是UIView動畫,因為UIView動畫並不會根據我們肉眼所看到的動畫效果過程中來動態改變寬高,在動畫開始時其寬高就已經是結束狀態時的寬高。所以用Masonry給子檢視佈局時,約束物件儘可能的 避免Cell的右邊和底邊 。否則動畫將會出現異常,如下圖的TitleLabel,我們能看到在切換時title寬度是直接變短的,也造成其它Label以它為約束物件時動畫異常(下面紅色字型的Label,切換時會往下移位)。

title約束為右邊時動畫慢放.gif
一、重寫layoutSubviews
通過重寫layoutSubviews方法,將 [super layoutSubviews]
寫進UIView動畫中,使Cell的切換過渡動畫更平滑。
-(void)layoutSubviews{ [UIView animateWithDuration:0.3 animations:^{ [super layoutSubviews]; }]; }
二、重寫setNotificationName
重寫setNotificationName方法並註冊觀察者。實現通知方法,將通知傳來的值賦值給 isList
。
最後記得移除觀察者!
-(void)setNotificationName:(NSString *)notificationName{ if ([_notificationName isEqualToString:notificationName]) return; _notificationName = notificationName; //註冊通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(isListChange:) name:_notificationName object:nil]; } -(void)isListChange:(NSNotification *)noti{ BOOL isList = [[noti object] boolValue]; [self setIsList:isList]; } -(void)dealloc{ //移除觀察者 [[NSNotificationCenter defaultCenter] removeObserver:self]; }
三、重寫setIsList
重寫setIsList方法,通過判斷 isList
值改變子檢視的佈局。
程式碼較多,詳細程式碼請下載Demo檢視。
- 此方法內 接收到通知進入時cell的frame並不準確,此時如果需要用到self.width,則需要自行計算,例如:
-(void)setIsList:(BOOL)isList{ if (_isList == isList) return; _isList = isList; CGFloat width = _isList ? SCREEN_WIDTH : (SCREEN_WIDTH - 5) * 0.5; if (_isList) { //...... }else{ //...... } //......
-
如使用Masonry
當佈局相對簡單時,約束使用mas_updateConstraints進行更新即可。當佈局比較複雜,約束涉及到某控制元件寬,而這控制元件寬又是不固定的時候,可以考慮使用mas_remakeConstraints重做約束。
-
約束都設定完成後,最後呼叫UIView動畫更新約束。如果有用frame設定的,也將設定frame程式碼寫在UIView動畫內。
!注意:如有用masonry約束關聯了 用frame設定的檢視,則此處需要把frame設定的檢視寫在前面。
-(void)setIsList:(BOOL)isList{ //...... [UIView animateWithDuration:0.3f animations:^{ self.label3.frame = frame3; self.label4.frame = frame4; [self.contentView layoutIfNeeded]; }]; }
Demo:
-END-
如果此文章對你有幫助,希望給個:heart:。有什麼問題歡迎在評論區探討。