大家也可以到這裡檢視。
UICollectionView是iOS6引入的控制元件,而UIDynamicAnimator是iOS7上新新增的框架。本文主要涵蓋3部分:
一是簡單概括UICollectionView的使用;二是自定義一個UICollectionViewLayout來實現不同的Collection佈局;
三是在自定義UICollectionViewLayout的基礎上新增UIDynamicAnimator。
1. 使用UICollectionView
因為UICollectionView在iOS6上就引入了,所以這裡就簡單的介紹下。在正式使用前,我們有必要對UICollectionView認識一下。
UICollectionView和UITableView有點類似,但又有不一樣。從上圖可以看出,組建一個UICollectionView不僅需要內容相關的物件,
如DataSource和Delegate,還需要佈局相關的物件即UICollectionViewLayout。
- Data Source:提供相關的data和view
- Delegate: 實現點選/插入/刪除等操作時需要的方法
- Layout:提供佈局view(如cell,supplementary,decoration view)需要的相關資料
熟悉UITableView的,對DataSource和Delegate應該比較親切,他們的作用和在TableView裡的完全一樣。而UICollectionViewLayout是一個新的類,
他的作用就是控制所有view的顯示。Layout會為每個view(如果需要顯示),提供一個LayoutAttribute,通過LayoutAttribute,CollectionView就
知道如何去組織了。注意LayoutAttribute除了可以提供frame資訊,還可以新增偽3D的資訊和UIKit的動態資訊。通過抽離佈局資訊,這樣很好的維護了
模組間的獨立性,而且也方便我們對layout進行重定義。理解這個框架圖有助於理解CollectionView的渲染過程以及自定義Layout。
下面我們認識下COllectionView:
上圖是UICollectionViewFlowLayout的一個佈局,我們以此進行介紹:
- Cell:如上每一個單元格就是一個cell,和UITableViewCell一樣,你可以進行自定義,新增image,label等等
- Supplementary view:圖中的Header和Footer就是Supplementary view,
- Decoration view: 圖中沒有顯示,不過顧名思義可以理解為修飾的view,如背景之類。它和Supplemetary的區別在於,後者往往是和資料相關聯的。
知道了這些,我們就可以實現一個簡單的CollectionView了。
在storeboard裡新建一個viewController,並在view上新增一個UICollectionView,collectionview的delegate和datasource都在SB裡連線好。
為了簡單,我們直接使用UICollectionViewFlowLayout:
紅色和綠色的label所在處就代表header和footer,他們都是用supplementary來表示,中間的Imageview所在處代表一個cell。
程式碼裡三者都進行了簡單的繼承自定義,注意給他們三者設定一個identifier,這樣利於重用。
然後在程式碼裡實現dataSource方法:
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { return 2; } - (NSInteger)collectionView:(UICollectionView *)view numberOfItemsInSection:(NSInteger)section; { return 20; } - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { ZJCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"ZJCell" forIndexPath:indexPath]; NSString *imgName = [NSString stringWithFormat:@"%d.JPG",indexPath.row]; cell.imageView.image = [UIImage imageNamed:imgName]; return cell; } - (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath { ZJSupplementaryView *supplementaryView = nil; NSString *text = nil; if ([kind isEqualToString:UICollectionElementKindSectionHeader]) { supplementaryView = [collectionView dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:@"CLHeader" forIndexPath:indexPath]; text = [NSString stringWithFormat:@"Header %d",indexPath.section]; supplementaryView.backgroundColor = [UIColor darkGrayColor]; } else { supplementaryView = [collectionView dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:@"CLFooter" forIndexPath:indexPath];; text = [NSString stringWithFormat:@"Footer %d",indexPath.section]; supplementaryView.backgroundColor = [UIColor lightGrayColor]; } supplementaryView.label.text = text; return supplementaryView; }
這樣一個最簡單的flow式的照片顯示就實現了,成品如下:
2 自定義Layout
Layout類中,有3個方法是必定會被依次呼叫:
-
prepareLayout: 準備所有view的layoutAttribute資訊
-
collectionViewContentSize: 計算contentsize,顯然這一步得在prepare之後進行
-
layoutAttributesForElementsInRect: 返回在可見區域的view的layoutAttribute資訊
此外,還有其他方法可能會被呼叫:
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath { } - (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath { } - (UICollectionViewLayoutAttributes *)layoutAttributesForDecorationViewOfKind:(NSString *)decorationViewKind atIndexPath:(NSIndexPath *)indexPath { } - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds { }
比如,如果沒有Decoration view,那麼相應的方法就可以不實現。
接下來我們要實現一個自定義的layout。官方文件CollectionViewPGforIOS中指出了需要自定義layout的情形:
簡單的說,就是現有的類(UICollectionViewLayout和UICollectionViewFlowLayout)不能滿足需要的情況下需要自定義。
下面我們來實現CollectionViewPGforIOS中的自定義的例子,如圖:
文件中,已經詳細的闡述了每一步需要做的事情,這裡就不多說了。但是因為文件中對於實現細節沒有涉及,因此這裡主要還是圍繞之前提到的3個方法來進行說明。
這裡假設你已經看過文件,並且知道自定義所需要的步驟。還需要宣告的是,文件中給出的圖以及下文的文字說明都是豎狀排列的,但是由於疏忽,實現的時候變成了橫向。希望因此不會給你造成混淆。
前提還需要做的準備:
1 定義Layout的子類
@interface ZJCustomLayout : UICollectionViewLayout @property (nonatomic, weak) id<ZJCustomLayoutProtocol> customDataSource; @end
@interfaceZJCustomLayout ()
{
NSInteger numberOfColumn;//here in this Sample Column equals the section
}
@property (nonatomic) NSDictionary *layoutInformation;//儲存所有view的layoutAttribute
@property (nonatomic) CGFloat maxWidth;//用於計算contentSize
@property (nonatomic) UIEdgeInsets insets;
@end
protocol是用來獲取一些資料,稍後定義。在擴充套件中定義一些屬性,用於儲存資訊。
2 定義LayoutAttribute的子類
@interface ZJCollectionViewLayoutAttributes : UICollectionViewLayoutAttributes<UIDynamicItem> @property (nonatomic) NSArray *children; @end @implementation ZJCollectionViewLayoutAttributes - (BOOL)isEqual:(id)object { ZJCollectionViewLayoutAttributes *attribute = (ZJCollectionViewLayoutAttributes *)object; if ([self.children isEqualToArray:attribute.children]) { return [super isEqual:object]; } return NO; } @end
ZJCollectionViewLayoutAttribute就是每一個cell的屬性,children表示當前cell所擁有的子cell。而isEqual是子類必須要過載的。
我們首先看一下,cell是如何佈局的:
紅色3是cell的最終位置。佈局的時候,先把最後一列的cell依次加上,如紅色1所示。
然後排前一列即第二列,先依次加上,這時最後的綠色cell有子cell,就把第三列的綠色cell位置更新。
最後排第一列,因為第一個cell有3個子cell,所以要空兩個開始排列。這時最後一個綠色cell有子cell這時就又要調整第二列以及第三列的綠色cell。
這裡cell調整的思路很清晰:先依次從上到下排列,然後再根據是否有子cell進行更新。
在實際實現中,我根據這樣的思路,設計了類似的演算法:
- 從後向前佈局每一列,每一列的cell依次從上向下佈局;
- 除最後一列的cell開始佈局時,先檢視當前列前一行的cell是否有子cell:有的話調整自己的位置
- 如果當前cell的位置進行了調整,那麼調整自己子cell的位置
很顯然,在初始化每個cell的layoutAttribute的時候,我們需要先知道每一個cell的子cell的情況,於是我們設計一個協議:
@protocol ZJCustomLayoutProtocol <NSObject> - (NSArray *)childrenAtIndexPath:(NSIndexPath *)indexPath; @end
這個和CollectionView的dataSource,delegate一樣,由viewController來提供。
接下來我們開始實現:
- (void)prepareLayout { if (self.layoutInformation) { return; } //whole size preparation NSMutableDictionary *layoutInformation = [NSMutableDictionary dictionary]; NSMutableDictionary *cellInformation = [NSMutableDictionary dictionary]; NSIndexPath *indexPath; NSInteger numSections = [self.collectionView numberOfSections]; numberOfColumn = numSections; //初始化attribute for(NSInteger section = 0; section < numSections; section++) { NSInteger numItems = [self.collectionView numberOfItemsInSection:section]; for(NSInteger item = 0; item < numItems; item++) { indexPath = [NSIndexPath indexPathForItem:item inSection:section]; ZJCollectionViewLayoutAttributes *attributes = [self attributesWithChildrenAtIndexPath:indexPath]; // attributes.zIndex = -(0 + 1); // attributes.transform = CGAffineTransformMakeRotation(.1); // attributes.transform3D = CATransform3DMakeRotation(.3, 0, 0, 1); [cellInformation setObject:attributes forKey:indexPath]; } } //從最後向前開始逐個調整attribute for(NSInteger section = numSections - 1; section >= 0; section--) { NSInteger numItems = [self.collectionView numberOfItemsInSection:section]; NSInteger totalHeight = 0; for(NSInteger item = 0; item < numItems; item++) { indexPath = [NSIndexPath indexPathForItem:item inSection:section]; ZJCollectionViewLayoutAttributes *attributes = [cellInformation objectForKey:indexPath];//1 attributes.frame = [self frameForCellAtIndexPath:indexPath withHeight:totalHeight]; // begin adjust the frame and its children's frame if (item) { NSIndexPath *previousIndex = [NSIndexPath indexPathForRow:item - 1 inSection:section]; ZJCollectionViewLayoutAttributes *previousAttribute = cellInformation[previousIndex]; CGRect rect = attributes.frame; CGRect previousRect = previousAttribute.frame; rect.origin.x = previousRect.origin.x + previousRect.size.width + CELL_ROW_SPACE; //前一個cell是否有孩子 if (previousAttribute.children) { ZJCollectionViewLayoutAttributes *preLastChildAttri = cellInformation[previousAttribute.children.lastObject]; CGRect preLastChildFrame = preLastChildAttri.frame; rect.origin.x = preLastChildFrame.origin.x + preLastChildFrame.size.width + CELL_ROW_SPACE; // rect.origin.x += (CELL_WIDTH + CELL_ROW_SPACE) * (previousAttribute.children.count - 1); } attributes.frame = rect; //調整自己的子cell if (attributes.children) { NSUInteger childrenCount = attributes.children.count; CGFloat baseOffset = rect.origin.x; for (NSUInteger count = 0; count < childrenCount; count ++) { NSIndexPath *childIndexpath = attributes.children[count];; ZJCollectionViewLayoutAttributes *childAttri = cellInformation[childIndexpath]; CGRect childRect = childAttri.frame; childRect.origin.x = baseOffset + count *(CELL_WIDTH + CELL_ROW_SPACE); childAttri.frame = childRect; } } } //記錄最大的長度(寬度) CGFloat currentWidth = attributes.frame.origin.x + attributes.frame.size.width; if (self.maxWidth < currentWidth) { self.maxWidth = currentWidth; } cellInformation[indexPath] = attributes; // totalHeight += [self.customDataSource numRowsForClassAndChildrenAtIndexPath:indexPath];//3 } } [layoutInformation setObject:cellInformation forKey:@"MyCellKind"];//5
通過這裡獲得的資料我們可以返回contentSize了。雖然高度上會有調整,但是寬度上是和section繫結的。
- (CGSize)collectionViewContentSize { CGFloat width = self.maxWidth + CELL_ROW_SPACE; CGFloat height = self.collectionView.numberOfSections * (CELL_HEIGHT + CELL_SEC_SPACE) + self.insets.top + self.insets.bottom; return CGSizeMake(width, height); }
接下來就要實現layoutAttributesForElementsInRect,這個通過CGRectIntersectsRect來選擇是否在當前的rect裡:
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect { NSMutableArray *myAttributes = [NSMutableArray arrayWithCapacity:self.layoutInformation.count]; for (NSString *key in self.layoutInformation) { NSDictionary *attributesDict = [self.layoutInformation objectForKey:key]; for (NSIndexPath *indexPath in attributesDict) { ZJCollectionViewLayoutAttributes *attributes = [attributesDict objectForKey:indexPath]; if (CGRectIntersectsRect(rect, attributes.frame)) { [myAttributes addObject:attributes]; } } } return myAttributes; }
然後在viewController類裡實現datasource,不要忘記實現我們自定義的protocol。這樣,我們就能看到所有的cell了。
接下來我們就要實現cell間的連線。連線我是作為supplementary view來處理。如果一個cell有子cell,那麼就設定view,並記錄點的相應位置,如圖:
因此仿照cell的處理方式,定義了suppleLayoutAttribute,主要用於儲存點:
@interface ZJCollectionSuppleLayoutAttributes : UICollectionViewLayoutAttributes @property (nonatomic) NSArray *pointArray; @end
然後繼承了UICollectionReusableView用於劃線:
@interface ZJClassReusableView() @property (nonatomic) NSArray *pointArray; @end @implementation ZJClassReusableView - (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { // Initialization code self.backgroundColor = [UIColor darkGrayColor]; } return self; } // Only override drawRect: if you perform custom drawing. // An empty implementation adversely affects performance during animation. - (void)drawRect:(CGRect)rect { [super drawRect:rect]; // Drawing code CGRect frame = self.frame; CGContextRef context = UIGraphicsGetCurrentContext(); CGContextSetStrokeColorWithColor(context, [UIColor whiteColor].CGColor); CGContextSetLineWidth(context, 2); NSUInteger count = self.pointArray.count; for (NSUInteger num = 0; num < count; num ++) { CGPoint point = [[self.pointArray objectAtIndex:num] CGPointValue]; CGFloat xPosition = point.x - frame.origin.x; if (num == 0) { CGContextMoveToPoint(context, xPosition, 0); CGContextAddLineToPoint(context, xPosition, rect.size.height); } else { CGContextMoveToPoint(context, xPosition, frame.size.height/2); CGContextAddLineToPoint(context, xPosition, rect.size.height); } } if (count > 1) { CGPoint first = [[self.pointArray objectAtIndex:0] CGPointValue]; CGPoint last = [[self.pointArray lastObject] CGPointValue]; CGContextMoveToPoint(context, first.x - frame.origin.x, frame.size.height/2); CGContextAddLineToPoint(context, last.x - frame.origin.x + 1, frame.size.height/2); } CGContextStrokePath(context); } - (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes { [super applyLayoutAttributes:layoutAttributes]; self.pointArray = ((ZJCollectionSuppleLayoutAttributes *)layoutAttributes).pointArray; } @end
而在customLayout中,需要新增:
//frame for supplement view NSMutableDictionary *suppleDict = [NSMutableDictionary dictionary]; for(NSInteger section = 0; section < numSections; section++) { NSInteger numItems = [self.collectionView numberOfItemsInSection:section]; for(NSInteger item = 0; item < numItems; item++) { indexPath = [NSIndexPath indexPathForItem:item inSection:section]; ZJCollectionSuppleLayoutAttributes *suppleAttri = [ZJCollectionSuppleLayoutAttributes layoutAttributesForSupplementaryViewOfKind:ZJSupplementKindDiagram withIndexPath:indexPath]; ZJCollectionViewLayoutAttributes *cellAttribute = cellInformation[indexPath]; NSArray *cellChildren = cellAttribute.children; if (cellChildren) { NSUInteger childrenCount = cellChildren.count; //calculate the frame CGRect cellFrame = cellAttribute.frame; CGRect suppleFrame = cellFrame; suppleFrame.origin.y = cellFrame.origin.y + cellFrame.size.height; suppleFrame.size.height = CELL_SEC_SPACE; NSMutableArray *mPointArray = [NSMutableArray arrayWithCapacity:childrenCount]; for (NSUInteger childNum = 0; childNum < childrenCount; childNum ++) { NSIndexPath *firstIndexPath = [cellChildren objectAtIndex:childNum]; ZJCollectionViewLayoutAttributes *firstChildAttri = cellInformation[firstIndexPath]; CGRect firstChildFrame = firstChildAttri.frame; CGPoint firstPoint = CGPointMake(firstChildFrame.origin.x + firstChildFrame.size.width /2, firstChildFrame.origin.y + firstChildFrame.size.height /2); [mPointArray addObject:[NSValue valueWithCGPoint:firstPoint]]; if (childNum == childrenCount - 1) { suppleFrame.size.width = firstChildFrame.origin.x + firstChildFrame.size.width - suppleFrame.origin.x; } } suppleAttri.frame = suppleFrame; suppleAttri.pointArray = mPointArray; } [suppleDict setObject:suppleAttri forKey:indexPath]; } }
這樣一個樹狀結構的圖就完成了。
3 新增動態行為UIKitDynamic
本身這段時間在學習UIDynamicAnimator,正好學到和collectionView的部分,覺得對CollectionView不太熟悉,就先溫習了一遍。
所以UIDynamicanimator其實是重點。我的主要參考資料是WWDC2013 221,以及collection-views-and-uidynamics。
主要實現了Cell的動態動畫,當拖動collectionView的時候,cell會晃動。
具體的新增方法我就不詳細解說了,這裡主要說明下自定義的layout新增UIDynamicAnimator需要注意的地方。
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect { .... //之前的程式碼後要新增 NSArray *array = [self.dynamicAnimator itemsInRect:rect]; [myAttributes addObjectsFromArray:array]; }
不知道為什麼一定要通過這樣的方式把新增到DynamicAnimator的Cell屬性取出來,否則cell就會不顯示。
還有就是在shouldInvalidateLayoutForBoundsChange中動態更新DynamicItem,否則動畫無從啟動。
4 總結
主要涉及UICollectionView的使用,簡單的自定義UICollectionViewLayout,以及新增UIKitDynamic。
關於CollectionView的點選,插入,刪除等操作沒有涵蓋。
另外,自定義Layout的時候沒有考慮效能,比如cell數量大的時候,現有prepare中的方式無疑會造成程式頁面變卡;
新增的動態行為沒有很好的修飾,純粹為了說明兩者結合的方法。
本文使用到的圖片都來自官方文件和本人demo的截圖。
關於UIKitDynamic,可以參閱初窺UIKit Dynamic
最後附上程式碼,請大家指正。