创建圆形布局
圆形布局是一种醒目的排版方式,他会将试图里的内容绕着某个中心区域来排布,这种布局方式很好的演示了如何在创建条目和删除条目的时候,把操作过程以动画形式展现出来。
通过collectionViewContentSize方法把视图内容的尺寸设为固定值。由于它明确的创建了一块固定不变的排版区域,所以集合视图不会再滚动了。代码还会在prepareLayout方法中进行计算,以便进一步向内缩小排版区域。屏幕的高度与屏幕的宽度之中较小的值,决定了圆的半径。无论屏幕方向如何改变,圆的半径总保持不变。
@implementation CircleLayout
{
NSInteger numberOfItems;
CGPoint centerPoint;
CGFloat radius;
NSMutableArray *insertedIndexPaths;
NSMutableArray * deletedIndexPaths;
}
- (void)prepareLayout
{
[super prepareLayout];
CGSize size = self.collectionView.frame.size;
numberOfItems = [self.collectionView numberOfItemsInSection:0];
centerPoint = CGPointMake(size.width / 2, size.height / 2);
radius = MIN(size.width, size.height) / 3;
insertedIndexPaths = [NSMutableArray array];
deletedIndexPaths = [NSMutableArray array];
}
- (CGSize)collectionViewContentSize
{
return self.collectionView.frame.size;
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
CGFloat progress = (float)indexPath.item / (float)numberOfItems;
CGFloat theta = 2 * M_PI * progress;
CGFloat xPosition = centerPoint.x + radius * cos(theta);
CGFloat yPosition = centerPoint.y + radius * sin(theta);
attributes.size = [self itemSize];
attributes.center = CGPointMake(xPosition, yPosition);
return attributes;
}
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSMutableArray *attributes = [NSMutableArray array];
for (NSInteger index = 0; index < numberOfItems; index ++) {
NSIndexPath *path = [NSIndexPath indexPathForItem:index inSection:0];
[attributes addObject:[self layoutAttributesForItemAtIndexPath:path]];
}
return attributes;
}
- (void)prepareForCollectionViewUpdates:(NSArray<UICollectionViewUpdateItem *> *)updateItems
{
[super prepareForCollectionViewUpdates:updateItems];
for (UICollectionViewUpdateItem *updateItem in updateItems) {
if (updateItem.updateAction == UICollectionUpdateActionInsert) {
[insertedIndexPaths addObject:updateItem.indexPathAfterUpdate];
}else if (updateItem.updateAction == UICollectionUpdateActionDelete){
[deletedIndexPaths addObject:updateItem.indexPathBeforeUpdate];
}
}
}
- (UICollectionViewLayoutAttributes *)insertionAttributesForItemAtIndexPath:(NSIndexPath *)itemIndexPath
{
UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForItemAtIndexPath:itemIndexPath];
attributes.alpha = 0;
attributes.center = centerPoint;
return attributes;
}
- (UICollectionViewLayoutAttributes *)deletionAttributesForItemAtIndexPath:(NSIndexPath *)itemIndexPath
{
UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForItemAtIndexPath:itemIndexPath];
attributes.alpha = 0;
attributes.center = centerPoint;
attributes.transform3D = CATransform3DMakeScale(0.1, 0.1, 1);
return attributes;
}
- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath
{
return [insertedIndexPaths containsObject:itemIndexPath] ? [self insertionAttributesForItemAtIndexPath:itemIndexPath] : [super initialLayoutAttributesForAppearingItemAtIndexPath:itemIndexPath];
}
- (UICollectionViewLayoutAttributes *)finalLayoutAttributesForDisappearingItemAtIndexPath:(NSIndexPath *)itemIndexPath
{
return [deletedIndexPaths containsObject:itemIndexPath] ? [self deletionAttributesForItemAtIndexPath:itemIndexPath] : [super finalLayoutAttributesForDisappearingItemAtIndexPath:itemIndexPath];
}
@end
布局对象会根据每个条目的索引路径来计算他的位置。这个例子采用的布局方式只使用了一个区段,每个条目在该区段内的顺序决定了它在圆周上的位置:
CGFloat progress = (float)indexPath.item / (float)numberOfItems;
CGFloat theta = 2 * M_PI * progress;
上述计算方式也适用于其他图形或其他形式的索引路径,只要各条目在索引路径中的位置都能调整到[0.0,1.0]这个范围内就行。对于圆形来说,此范围可以和从0至2π的弧度对应起来,而对于螺旋形的布局来说,此范围则可以和从0到3π、4π到5π的弧度对应起来。如果想按照贝塞尔曲线来布局,那么开发者需要遍历能够决定曲线形状的各个控制点,并要根据情况在其中插入其他的点。
1、实现创建条目与删除条目时的动画效果
在例子中有几个方法值得关注,他们分别制定了新插入的条目所具备的初始属性,以及刚删除的条目所应具备的最后属性。这些属性使得集合视图在添加新条目及删除现有条目的时候,能够以动画效果来表示该操作执行前后的布局变化过程。
这个例子的动画效果与苹果公司的原始范例代码一样:新添加的条目一开始会以全透明的形态出现在圆圈正中,然后会移动到它应有的位置上,在移动过程中它将逐渐淡入。而刚删除的条目则会从目前的位置移向圆心,并在此过程中逐渐缩小、淡出。运行范例代码的时候能看到这些效果了。
开发文档中的initialLayoutAttributesForAppearingItemAtIndexPath及finalLayoutAttributesForDisappearingItemAtIndexPath方法,名字起得很令人困惑,从表面上看,这些方法似乎只会针对刚插入或删除的条目来调用。但实际上,系统会向每一个条目询问它的起始属性和最终属性,而不仅仅向刚添加或删除的条目询问。所以,在添加条目和删除条目的时候,会把所添加条目及所删条目的索引路径分别记录到两个数字里面。这样的话,我们就可以只针对当前要添加或删除的条目来定制其属性了。
该机制所提供的这种方法,能够把视图中全部条目的布局属性都以动画形式表现出来,使得开发者可以按照需要添加额外的动画效果。比方说,如果有新的条目插入第三行,那么该行末尾的条目就应该移动到第四行的开头。在默认的情况下,末尾的条目会按照斜线方向移动到下一行开头,但有了这套方式之后,我们就可以把第三行末尾的单元格向右移出屏幕,然后再将其从第四行左侧移入屏幕。
2、增强圆形布局的实用性
- (void)delete
{
if (!count) {
return;
}
count --;
NSArray *selectedItems = [self.collectionView indexPathsForSelectedItems];
NSInteger itemNumber = selectedItems.count ? ((NSIndexPath *)selectedItems[0]).item : 0;
NSIndexPath *itemPath = [NSIndexPath indexPathForItem:itemNumber inSection:0];
self.itemsInSection --;
[self.collectionView performBatchUpdates:^{
[self.collectionView deleteItemsAtIndexPaths:@[itemPath]];
} completion:^(BOOL finished) {
if (count) {
[self.collectionView selectItemAtIndexPath:[NSIndexPath indexPathForItem:MAX(0, itemNumber - 1) inSection:0] animated:NO scrollPosition:UICollectionViewScrollPositionNone];
}
self.navigationItem.rightBarButtonItem.enabled = (count > 0);
self.navigationItem.leftBarButtonItem.enabled = (count < 12);
}];
}
- (void)add
{
NSInteger itemNumber = self.itemsInSection ;
NSIndexPath *itemPath = [NSIndexPath indexPathForItem:itemNumber inSection:0];
self.itemsInSection ++;
count ++;
[self.collectionView performBatchUpdates:^{
[self.collectionView insertItemsAtIndexPaths:@[itemPath]];
} completion:^(BOOL finished) {
self.navigationItem.rightBarButtonItem.enabled = (count > 0);
self.navigationItem.leftBarButtonItem.enabled = (count < 12);
}];
}