您的位置:首页 > 产品设计 > UI/UE

自定义UICollectionViewLayout并添加UIDynamic

2015-07-15 22:48 615 查看
UICollectionView是iOS6引入的控件,而
UIDynamicAnimator是iOS7上新添加的框架。本文主要涵盖3部分:


一是简单概括UICollectionView的使用;二是自定义一个UICollectionViewLayout来实现不同的Collection布局;


三是在自定义UICollectionViewLayout的基础上添加UIDynamicAnimator。


1.使用UICollectionView

因为UICollectionView在iOS6上就引入了,所以这里就简单的介绍下。在正式使用前,我们有必要对UICollectionView认识一下。



UICollectionView和UITableView有点类似,但又有不一样。从上图可以看出,组建一个UICollectionView不仅需要内容相关的对象,

如DataSource和Delegate,还需要布局相关的对象即UICollectionViewLayout。

DataSource:提供相关的data和view
Delegate:实现点击/插入/删除等操作时需要的方法
Layout:提供布局view(如cell,supplementary,decorationview)需要的相关数据

熟悉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等等
Supplementaryview:图中的Header和Footer就是Supplementaryview,
Decorationview:图中没有显示,不过顾名思义可以理解为修饰的view,如背景之类。它和Supplemetary的区别在于,后者往往是和数据相关联的。

知道了这些,我们就可以实现一个简单的CollectionView了。

在storeboard里新建一个viewController,并在view上添加一个UICollectionView,collectionview的delegate和datasource都在SB里连接好。

为了简单,我们直接使用UICollectionViewFlowLayout:









红色和绿色的label所在处就代表header和footer,他们都是用supplementary来表示,中间的Imageview所在处代表一个cell。

代码里三者都进行了简单的继承自定义,注意给他们三者设置一个identifier,这样利于重用。

然后在代码里实现dataSource方法:

-(NSInteger)numberOfSectionsInCollectionView:(UICollectionView*)collectionView { return2; } -(NSInteger)collectionView:(UICollectionView*)viewnumberOfItemsInSection:(NSInteger)section; { return20; } -(UICollectionViewCell*)collectionView:(UICollectionView*)collectionViewcellForItemAtIndexPath:(NSIndexPath*)indexPath { ZJCell*cell=[collectionViewdequeueReusableCellWithReuseIdentifier:@"ZJCell"forIndexPath:indexPath]; NSString*imgName=[NSStringstringWithFormat:@"%d.JPG",indexPath.row]; cell.imageView.image=[UIImageimageNamed:imgName]; returncell; } -(UICollectionReusableView*)collectionView:(UICollectionView*)collectionViewviewForSupplementaryElementOfKind:(NSString*)kindatIndexPath:(NSIndexPath*)indexPath { ZJSupplementaryView*supplementaryView=nil; NSString*text=nil; if([kindisEqualToString:UICollectionElementKindSectionHeader]) { supplementaryView=[collectionViewdequeueReusableSupplementaryViewOfKind:kindwithReuseIdentifier:@"CLHeader"forIndexPath:indexPath]; text=[NSStringstringWithFormat:@"Header%d",indexPath.section]; supplementaryView.backgroundColor=[UIColordarkGrayColor]; } else { supplementaryView=[collectionViewdequeueReusableSupplementaryViewOfKind:kindwithReuseIdentifier:@"CLFooter"forIndexPath:indexPath];; text=[NSStringstringWithFormat:@"Footer%d",indexPath.section]; supplementaryView.backgroundColor=[UIColorlightGrayColor]; } supplementaryView.label.text=text; returnsupplementaryView; }

这样一个最简单的flow式的照片显示就实现了,成品如下:


  


2自定义Layout

Layout类中,有3个方法是必定会被依次调用:

prepareLayout:准备所有view的layoutAttribute信息

collectionViewContentSize:计算contentsize,显然这一步得在prepare之后进行

layoutAttributesForElementsInRect:返回在可见区域的view的layoutAttribute信息

此外,还有其他方法可能会被调用:

-(UICollectionViewLayoutAttributes*)layoutAttributesForItemAtIndexPath:(NSIndexPath*)indexPath{}-(UICollectionViewLayoutAttributes*)layoutAttributesForSupplementaryViewOfKind:(NSString*)kindatIndexPath:(NSIndexPath*)indexPath{}-(UICollectionViewLayoutAttributes*)layoutAttributesForDecorationViewOfKind:(NSString*)decorationViewKindatIndexPath:(NSIndexPath*)indexPath{}-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{}


比如,如果没有Decorationview,那么相应的方法就可以不实现。

接下来我们要实现一个自定义的layout。官方文档CollectionViewPGforIOS中指出了需要自定义layout的情形:



简单的说,就是现有的类(UICollectionViewLayout和UICollectionViewFlowLayout)不能满足需要的情况下需要自定义。

下面我们来实现CollectionViewPGforIOS中的自定义的例子,如图:



文档中,已经详细的阐述了每一步需要做的事情,这里就不多说了。但是因为文档中对于实现细节没有涉及,因此这里主要还是围绕之前提到的3个方法来进行说明。

这里假设你已经看过文档,并且知道自定义所需要的步骤。还需要声明的是,文档中给出的图以及下文的文字说明都是竖状排列的,但是由于疏忽,实现的时候变成了横向。希望因此不会给你造成混淆。

前提还需要做的准备:

1定义Layout的子类

@interfaceZJCustomLayout:UICollectionViewLayout@property(nonatomic,weak)id<ZJCustomLayoutProtocol>customDataSource;@end


@interfaceZJCustomLayout()

{


NSIntegernumberOfColumn;//hereinthisSampleColumnequalsthesection


}


@property(nonatomic)NSDictionary*layoutInformation;//存储所有view的layoutAttribute

@property(nonatomic)CGFloatmaxWidth;//用于计算contentSize

@property(nonatomic)UIEdgeInsetsinsets;

@end


protocol是用来获取一些数据,稍后定义。在扩展中定义一些属性,用于存储信息。

2定义LayoutAttribute的子类

@interfaceZJCollectionViewLayoutAttributes:UICollectionViewLayoutAttributes<UIDynamicItem>@property(nonatomic)NSArray*children;@end

@implementationZJCollectionViewLayoutAttributes-(BOOL)isEqual:(id)object{ZJCollectionViewLayoutAttributes*attribute=(ZJCollectionViewLayoutAttributes*)object;if([self.childrenisEqualToArray:attribute.children]){return[superisEqual:object];}returnNO;}@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的情况,于是我们设计一个协议:

@protocolZJCustomLayoutProtocol<NSObject>

-(NSArray*)childrenAtIndexPath:(NSIndexPath*)indexPath;@end


这个和CollectionView的dataSource,delegate一样,由viewController来提供。

接下来我们开始实现:

-(void)prepareLayout{if(self.layoutInformation){return;}//wholesizepreparation
NSMutableDictionary*layoutInformation=[NSMutableDictionarydictionary];NSMutableDictionary*cellInformation=[NSMutableDictionarydictionary];NSIndexPath*indexPath;NSIntegernumSections=[self.collectionViewnumberOfSections];numberOfColumn=numSections;//初始化attribute
for(NSIntegersection=0;section<numSections;section++){NSIntegernumItems=[self.collectionViewnumberOfItemsInSection:section];for(NSIntegeritem=0;item<numItems;item++){indexPath=[NSIndexPathindexPathForItem:iteminSection:section];ZJCollectionViewLayoutAttributes*attributes=[selfattributesWithChildrenAtIndexPath:indexPath];//attributes.zIndex=-(0+1);//attributes.transform=CGAffineTransformMakeRotation(.1);//attributes.transform3D=CATransform3DMakeRotation(.3,0,0,1);
[cellInformationsetObject:attributesforKey:indexPath];}}//从最后向前开始逐个调整attribute
for(NSIntegersection=numSections-1;section>=0;section--){NSIntegernumItems=[self.collectionViewnumberOfItemsInSection:section];NSIntegertotalHeight=0;for(NSIntegeritem=0;item<numItems;item++){indexPath=[NSIndexPathindexPathForItem:iteminSection:section];ZJCollectionViewLayoutAttributes*attributes=[cellInformationobjectForKey:indexPath];//1
attributes.frame=[selfframeForCellAtIndexPath:indexPathwithHeight:totalHeight];//beginadjusttheframeanditschildren'sframe
if(item){NSIndexPath*previousIndex=[NSIndexPathindexPathForRow:item-1inSection:section];ZJCollectionViewLayoutAttributes*previousAttribute=cellInformation[previousIndex];CGRectrect=attributes.frame;CGRectpreviousRect=previousAttribute.frame;rect.origin.x=previousRect.origin.x+previousRect.size.width+CELL_ROW_SPACE;//前一个cell是否有孩子
if(previousAttribute.children){ZJCollectionViewLayoutAttributes*preLastChildAttri=cellInformation[previousAttribute.children.lastObject];CGRectpreLastChildFrame=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){NSUIntegerchildrenCount=attributes.children.count;CGFloatbaseOffset=rect.origin.x;for(NSUIntegercount=0;count<childrenCount;count++){NSIndexPath*childIndexpath=attributes.children[count];;ZJCollectionViewLayoutAttributes*childAttri=cellInformation[childIndexpath];CGRectchildRect=childAttri.frame;childRect.origin.x=baseOffset+count*(CELL_WIDTH+CELL_ROW_SPACE);childAttri.frame=childRect;}}}//记录最大的长度(宽度)
CGFloatcurrentWidth=attributes.frame.origin.x+attributes.frame.size.width;if(self.maxWidth<currentWidth){self.maxWidth=currentWidth;}cellInformation[indexPath]=attributes;//totalHeight+=[self.customDataSourcenumRowsForClassAndChildrenAtIndexPath:indexPath];//3
}}[layoutInformationsetObject:cellInformationforKey:@"MyCellKind"];//5


通过这里获得的数据我们可以返回contentSize了。虽然高度上会有调整,但是宽度上是和section绑定的。

-(CGSize)collectionViewContentSize{CGFloatwidth=self.maxWidth+CELL_ROW_SPACE;CGFloatheight=self.collectionView.numberOfSections*(CELL_HEIGHT+CELL_SEC_SPACE)+self.insets.top+self.insets.bottom;returnCGSizeMake(width,height);}


接下来就要实现layoutAttributesForElementsInRect,这个通过CGRectIntersectsRect来选择是否在当前的rect里:

-(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect{NSMutableArray*myAttributes=[NSMutableArrayarrayWithCapacity:self.layoutInformation.count];for(NSString*keyinself.layoutInformation){NSDictionary*attributesDict=[self.layoutInformationobjectForKey:key];for(NSIndexPath*indexPathinattributesDict){ZJCollectionViewLayoutAttributes*attributes=[attributesDictobjectForKey:indexPath];if(CGRectIntersectsRect(rect,attributes.frame)){[myAttributesaddObject:attributes];}}}returnmyAttributes;}


然后在viewController类里实现datasource,不要忘记实现我们自定义的protocol。这样,我们就能看到所有的cell了。

接下来我们就要实现cell间的连线。连线我是作为supplementaryview来处理。如果一个cell有子cell,那么就设置view,并记录点的相应位置,如图:



因此仿照cell的处理方式,定义了suppleLayoutAttribute,主要用于存储点:

@interfaceZJCollectionSuppleLayoutAttributes:UICollectionViewLayoutAttributes@property(nonatomic)NSArray*pointArray;@end


然后继承了UICollectionReusableView用于划线:

@interfaceZJClassReusableView()@property(nonatomic)NSArray*pointArray;@end

@implementationZJClassReusableView-(id)initWithFrame:(CGRect)frame{self=[superinitWithFrame:frame];if(self){//Initializationcode
self.backgroundColor=[UIColordarkGrayColor];}returnself;}//OnlyoverridedrawRect:ifyouperformcustomdrawing.//Anemptyimplementationadverselyaffectsperformanceduringanimation.
-(void)drawRect:(CGRect)rect{[superdrawRect:rect];//Drawingcode
CGRectframe=self.frame;CGContextRefcontext=UIGraphicsGetCurrentContext();CGContextSetStrokeColorWithColor(context,[UIColorwhiteColor].CGColor);CGContextSetLineWidth(context,2);NSUIntegercount=self.pointArray.count;for(NSUIntegernum=0;num<count;num++){CGPointpoint=[[self.pointArrayobjectAtIndex:num]CGPointValue];CGFloatxPosition=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){CGPointfirst=[[self.pointArrayobjectAtIndex:0]CGPointValue];CGPointlast=[[self.pointArraylastObject]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{[superapplyLayoutAttributes:layoutAttributes];self.pointArray=((ZJCollectionSuppleLayoutAttributes*)layoutAttributes).pointArray;}@end


而在customLayout中,需要添加:

//frameforsupplementview
NSMutableDictionary*suppleDict=[NSMutableDictionarydictionary];for(NSIntegersection=0;section<numSections;section++){NSIntegernumItems=[self.collectionViewnumberOfItemsInSection:section];for(NSIntegeritem=0;item<numItems;item++){indexPath=[NSIndexPathindexPathForItem:iteminSection:section];ZJCollectionSuppleLayoutAttributes*suppleAttri=[ZJCollectionSuppleLayoutAttributeslayoutAttributesForSupplementaryViewOfKind:ZJSupplementKindDiagramwithIndexPath:indexPath];ZJCollectionViewLayoutAttributes*cellAttribute=cellInformation[indexPath];NSArray*cellChildren=cellAttribute.children;if(cellChildren){NSUIntegerchildrenCount=cellChildren.count;//calculatetheframe
CGRectcellFrame=cellAttribute.frame;CGRectsuppleFrame=cellFrame;suppleFrame.origin.y=cellFrame.origin.y+cellFrame.size.height;suppleFrame.size.height=CELL_SEC_SPACE;NSMutableArray*mPointArray=[NSMutableArrayarrayWithCapacity:childrenCount];for(NSUIntegerchildNum=0;childNum<childrenCount;childNum++){NSIndexPath*firstIndexPath=[cellChildrenobjectAtIndex:childNum];ZJCollectionViewLayoutAttributes*firstChildAttri=cellInformation[firstIndexPath];CGRectfirstChildFrame=firstChildAttri.frame;CGPointfirstPoint=CGPointMake(firstChildFrame.origin.x+firstChildFrame.size.width/2,firstChildFrame.origin.y+firstChildFrame.size.height/2);[mPointArrayaddObject:[NSValuevalueWithCGPoint:firstPoint]];if(childNum==childrenCount-1){suppleFrame.size.width=firstChildFrame.origin.x+firstChildFrame.size.width-suppleFrame.origin.x;}}suppleAttri.frame=suppleFrame;suppleAttri.pointArray=mPointArray;}[suppleDictsetObject:suppleAttriforKey:indexPath];}}


这样一个树状结构的图就完成了。


3添加动态行为UIKitDynamic

本身这段时间在学习UIDynamicAnimator,正好学到和collectionView的部分,觉得对CollectionView不太熟悉,就先温习了一遍。

所以UIDynamicanimator其实是重点。我的主要参考资料是WWDC2013221,以及collection-views-and-uidynamics。

主要实现了Cell的动态动画,当拖动collectionView的时候,cell会晃动。

具体的添加方法我就不详细解说了,这里主要说明下自定义的layout添加UIDynamicAnimator需要注意的地方。

-(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect{....//之前的代码后要添加
NSArray*array=[self.dynamicAnimatoritemsInRect:rect];[myAttributesaddObjectsFromArray:array];}


不知道为什么一定要通过这样的方式把添加到DynamicAnimator的Cell属性取出来,否则cell就会不显示。

还有就是在shouldInvalidateLayoutForBoundsChange中动态更新DynamicItem,否则动画无从启动。


4总结

主要涉及UICollectionView的使用,简单的自定义UICollectionViewLayout,以及添加UIKitDynamic。

关于CollectionView的点击,插入,删除等操作没有涵盖。

另外,自定义Layout的时候没有考虑性能,比如cell数量大的时候,现有prepare中的方式无疑会造成程序页面变卡;

添加的动态行为没有很好的修饰,纯粹为了说明两者结合的方法。

本文使用到的图片都来自官方文档和本人demo的截图。

关于UIKitDynamic,可以参阅初窥UIKitDynamic

最后附上代码,请大家指正。

CollectionSample
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: