http://www.jianshu.com/p/c59a5c92f859
http://www.jianshu.com/p/45ff718090a8
前言
这几天有时间看了下UICollectionView的东西,才发觉它真的非常强大,很有必要好好学习学习。以前虽然用过几次,但没有系统的整理总结过。这两天我为UICollectionView做一个比较全面的整理。包括基本使用,自定义布局,自定义插入删除动画,自定义转场动画等几部分。好了,开始。
UICollectionView相对于UITableView可以说是青出于蓝而胜于蓝,它和UITableView很相似,但它要更加强大。
UITableView的布局形式比较单一,局限于行列表,而UICollectionView的强大之处在于把视图布局分离出来成为一个独立的类,你想实现怎样的视图布局,就子类化这个类并在其中实现。
UICollectionView基础
- UICollectionViewFlowLayout:视图布局对象(流视图:一行排满,自动排到下行),继承自UICollectionViewLayout。
UICollectionViewLayout有个collectionView属性,
所有的视图布局对象都继承自UICollectionViewLayout。若我们要自定义布局对象,我们一般继承UICollectionViewFlowLayout就可以了。 - 需要实现三个协议;UICollectionViewDataSource(数据源)、UICollectionViewDelegateFlowLayout(视图布局)、UICollectionViewDelegate。
可以看得出,除了视图布局,UICollectionView几乎和UITableView一样,但这也正是它的强大之处。
1.创建UICollectionView视图
- (void)loadCollectionView
{
_customLayout = [[CustomCollectionViewLayout alloc] init]; // 自定义的布局对象
_collectionView = [[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:_customLayout];
_collectionView.backgroundColor = [UIColor whiteColor];
_collectionView.dataSource = self;
_collectionView.delegate = self;
[self.view addSubview:_collectionView];
// 注册cell、sectionHeader、sectionFooter
[_collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:cellId];
[_collectionView registerClass:[UICollectionReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:headerId];
[_collectionView registerClass:[UICollectionReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:footerId];
}
需要注意的是这几行代码的位置,及const的位置。(我经常搞乱)
@implementation YWViewController
// 注意const的位置
static NSString *const cellId = @"cellId";
static NSString *const headerId = @"headerId";
static NSString *const footerId = @"footerId";
- (void)viewDidLoad
{
2.实现UICollectionViewDataSource的几个代理方法
#pragma mark ---- UICollectionViewDataSource
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
return 1;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return _section0Array.count;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = [_collectionView dequeueReusableCellWithReuseIdentifier:cellId forIndexPath:indexPath];
cell.backgroundColor = [UIColor purpleColor];
return cell;
}
// 和UITableView类似,UICollectionView也可设置段头段尾
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath
{
if([kind isEqualToString:UICollectionElementKindSectionHeader])
{
UICollectionReusableView *headerView = [_collectionView dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:headerId forIndexPath:indexPath];
if(headerView == nil)
{
headerView = [[UICollectionReusableView alloc] init];
}
headerView.backgroundColor = [UIColor grayColor];
return headerView;
}
else if([kind isEqualToString:UICollectionElementKindSectionFooter])
{
UICollectionReusableView *footerView = [_collectionView dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:footerId forIndexPath:indexPath];
if(footerView == nil)
{
footerView = [[UICollectionReusableView alloc] init];
}
footerView.backgroundColor = [UIColor lightGrayColor];
return footerView;
}
return nil;
}
- (BOOL)collectionView:(UICollectionView *)collectionView canMoveItemAtIndexPath:(NSIndexPath *)indexPath
{
return YES;
}
- (void)collectionView:(UICollectionView *)collectionView moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath*)destinationIndexPath
{
}
#pragma mark ---- UICollectionViewDelegateFlowLayout
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
return (CGSize){cellWidth,cellWidth};
}
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section
{
return UIEdgeInsetsMake(5, 5, 5, 5);
}
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section
{
return 5.f;
}
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section
{
return 5.f;
}
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section
{
return (CGSize){ScreenWidth,44};
}
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section
{
return (CGSize){ScreenWidth,22};
}
#pragma mark ---- UICollectionViewDelegate
- (BOOL)collectionView:(UICollectionView *)collectionView shouldHighlightItemAtIndexPath:(NSIndexPath *)indexPath
{
return YES;
}
// 点击高亮
- (void)collectionView:(UICollectionView *)collectionView didHighlightItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
cell.backgroundColor = [UIColor greenColor];
}
// 选中某item
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
}
// 长按某item,弹出copy和paste的菜单
- (BOOL)collectionView:(UICollectionView *)collectionView shouldShowMenuForItemAtIndexPath:(NSIndexPath *)indexPath
{
return YES;
}
// 使copy和paste有效
- (BOOL)collectionView:(UICollectionView *)collectionView canPerformAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(nullable id)sender
{
if ([NSStringFromSelector(action) isEqualToString:@"copy:"] || [NSStringFromSelector(action) isEqualToString:@"paste:"])
{
return YES;
}
return NO;
}
//
- (void)collectionView:(UICollectionView *)collectionView performAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(nullable id)sender
{
if([NSStringFromSelector(action) isEqualToString:@"copy:"])
{
// NSLog(@"-------------执行拷贝-------------");
[_collectionView performBatchUpdates:^{
[_section0Array removeObjectAtIndex:indexPath.row];
[_collectionView deleteItemsAtIndexPaths:@[indexPath]];
} completion:nil];
}
else if([NSStringFromSelector(action) isEqualToString:@"paste:"])
{
NSLog(@"-------------执行粘贴-------------");
}
}
UICollectionView自定义布局
要自定义UICollectionView布局,就要子类化UICollectionViewLayout,然后重写它的一些方法以达到我们自定义布局的需求。下来我们来看看UICollectionViewLayout类里一些比较重要的方法:
- - (void)prepareLayout;为layout显示做准备工作,你可以在该方法里设置一些属性。
- - (CGSize)collectionViewContentSize;返回layout的size。
- - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect;返回在collectionView的可见范围内(bounds)所有item对应的layoutAttrure对象装成的数组。collectionView的每个item都对应一个专门的UICollectionViewLayoutAttributes类型的对象来表示该item的一些属性,比如bounds,size,transform,alpha等。
- - (UICollectionViewLayoutAttributes )layoutAttributesForItemAtIndexPath:(NSIndexPath )indexPath;传入indexPath,返回该indexPath对应的layoutAtture对象。
- - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds; 当当前layout的布局发生变动时,是否重写加载该layout。默认返回NO,若返回YES,则重新执行这俩方法:
- - (void)prepareLayout;
- - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect;
- - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity;返回layout“最终”的偏移量,何谓“最终”,手指离开屏幕时layout的偏移量不是最终的,因为它有惯性,当它停止时才是“最终”偏移量。
下面这两个方法一般用于自定义插入删除时的动画,后面再说。
-
- (UICollectionViewLayoutAttributes )initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath )itemIndexPath;
-
- (nullable UICollectionViewLayoutAttributes )finalLayoutAttributesForDisappearingItemAtIndexPath:(NSIndexPath )itemIndexPath;
本Demo的代码虽然子类化了UICollectionViewLayout,但是主要是用于自定义插入删除动画,所以本段没什么代码展示。
UICollectionView插入删除的操作及动画
插入删除的操作
添加在哪触发:
UIBarButtonItem *btnItem = [[UIBarButtonItem alloc] initWithTitle:@"添加"
style:UIBarButtonItemStylePlain
target:self
action:@selector(addItemBtnClick:)];
self.navigationItem.rightBarButtonItem = btnItem;
添加的实现:
// 添加(插入item)
- (void)addItemBtnClick:(UIBarButtonItem *)btnItem
{
[_collectionView performBatchUpdates:^{
// 构造一个indexPath
NSIndexPath *indePath = [NSIndexPath indexPathForItem:_section0Array.count inSection:0];
[_collectionView insertItemsAtIndexPaths:@[indePath]]; // 然后在此indexPath处插入给collectionView插入一个item
[_section0Array addObject:@"x"]; // 保持collectionView的item和数据源一致
} completion:nil];
}
因为是练习Demo,所以暂时把删除的触发源写在了长按某Item弹出菜单的copy按钮里。实际中你可以自定义UICollectionViewCell,添加长按手势,长按抖动出现叉号,然后删除等,随你怎么做。
// copy and paste 的实现
- (void)collectionView:(UICollectionView *)collectionView performAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(nullable id)sender
{
if([NSStringFromSelector(action) isEqualToString:@"copy:"])
{
// NSLog(@"-------------执行拷贝-------------");
[_collectionView performBatchUpdates:^{
[_section0Array removeObjectAtIndex:indexPath.row];
[_collectionView deleteItemsAtIndexPaths:@[indexPath]];
} completion:nil];
}
else if([NSStringFromSelector(action) isEqualToString:@"paste:"])
{
NSLog(@"-------------执行粘贴-------------");
}
}
插入删除的动画
上面已经提到了在UICollectionViewLayout类中有两个用于自定义动画的方法,两个方法分别表示动画的起始状态和终止状态,我们可以分别在方法里设置layoutAttrure来实现某种动画效果。
苹果选择了一种安全的途径去实现一个简单的淡入淡出动画作为所有布局的默认动画。如果你想实现自定义动画,最好的办法是子类化 UICollectionViewFlowLayout 并且在适当的地方实现你的动画。
一般来说,我们对布局属性从初始状态到结束状态进行线性插值来计算 collection view 的动画参数。然而,新插入或者删除的元素并没有最初或最终状态来进行插值。要计算这样的 cells 的动画,collection view 将通过 initialLayoutAttributesForAppearingItemAtIndexPath: 以及 finalLayoutAttributesForDisappearingItemAtIndexPath: 方法来询问其布局对象,以获取最初的和最后的属性。苹果默认的实现中,对于特定的某个 indexPath,返回的是它的通常的位置,但 alpha 值为 0.0,这就产生了一个淡入或淡出动画。
简而言之,就是苹果自带了插入删除时Item的淡入淡出的动画,若你想自定义更炫的动画,就子类化UICollectionViewFlowLayout类,并重写以下两个方法:
// 初始状态
- (nullable UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath
{
UICollectionViewLayoutAttributes *attr = [self layoutAttributesForItemAtIndexPath:itemIndexPath];
attr.center = CGPointMake(CGRectGetMidX(self.collectionView.bounds), CGRectGetMaxY(self.collectionView.bounds));
attr.transform = CGAffineTransformRotate(CGAffineTransformMakeScale(0.2, 0.2), M_PI);
return attr;
}
// 终结状态
- (nullable UICollectionViewLayoutAttributes *)finalLayoutAttributesForDisappearingItemAtIndexPath:(NSIndexPath *)itemIndexPath
{
UICollectionViewLayoutAttributes *attr = [self layoutAttributesForItemAtIndexPath:itemIndexPath];
attr.alpha = 0.0f;
return attr;
}
UICollectionView的转场动画
UICollectionViewLayout布局详解
首先我们先看一下 我们今天要最终实现的效果图
UICollectionView的简单介绍
- UICollectionView的结构
Cells Supplementary Views 追加视图 (类似Header或者Footer) Decoration Views 装饰视图 (用作背景展示)
-
由两个方面对UICollectionView进行支持。
- 和tableView一样,即提供数据的UICollectionViewDataSource以及处理用户交互的UICollectionViewDelegate。
- 另一方面,对于cell的样式和组织方式,由于collectionView比tableView要复杂得多,因此没有按照类似于tableView的style的方式来定义,而是专门使用了一个类来对collectionView的布局和行为进行描述,这就是
UICollectionViewLayout
。
-
而我们主要讲UICollectionViewLayout,因为这不仅是collectionView和tableView的最重要求的区别,也是整个UICollectionView的精髓所在。
- 如果对UICollectionView的基本构成要素和使用方法还不清楚的话,可以查看:UICollectionView详解中进行一些了解。
*
UICollectionViewLayoutAttributes类的介绍
@property (nonatomic) CGRect frame
@property (nonatomic) CGPoint center
@property (nonatomic) CGSize size
@property (nonatomic) CATransform3D transform3D
@property (nonatomic) CGFloat alpha
@property (nonatomic) NSInteger zIndex
@property (nonatomic, getter=isHidden) BOOL hidden
可以看到,UICollectionViewLayoutAttributes的实例中包含了诸如边框,中心点,大小,形状,透明度,层次关系和是否隐藏等信息。
1.一个cell对应一个UICollectionViewLayoutAttributes对象
2.UICollectionViewLayoutAttributes对象决定了cell的摆设位置(frame)
自定义的UICollectionViewLayout
- UICollectionViewLayout的功能为向UICollectionView提供布局信息,不仅包括cell的布局信息,也包括追加视图和装饰视图的布局信息。实现一个自定义layout的常规做法是继承UICollectionViewLayout类,然后重载下列方法:
-(void)prepareLayout
准备方法被自动调用,以保证layout实例的正确。
-(CGSize)collectionViewContentSize
返回collectionView的内容的尺寸
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
1.返回rect中的所有的元素的布局属性
2.返回的是包含UICollectionViewLayoutAttributes的NSArray
3.UICollectionViewLayoutAttributes可以是cell,追加视图或装饰视图的信息,通过不同的UICollectionViewLayoutAttributes初始化方法可以得到不同类型的UICollectionViewLayoutAttributes:
1)layoutAttributesForCellWithIndexPath:
2)layoutAttributesForSupplementaryViewOfKind:withIndexPath:
3)layoutAttributesForDecorationViewOfKind:withIndexPath:
-(UICollectionViewLayoutAttributes )layoutAttributesForItemAtIndexPath:(NSIndexPath )indexPath
返回对应于indexPath的位置的cell的布局属性
-(UICollectionViewLayoutAttributes )layoutAttributesForSupplementaryViewOfKind:(NSString )kind atIndexPath:(NSIndexPath *)indexPath
返回对应于indexPath的位置的追加视图的布局属性,如果没有追加视图可不重载
-(UICollectionViewLayoutAttributes * )layoutAttributesForDecorationViewOfKind:(NSString)decorationViewKind atIndexPath:(NSIndexPath )indexPath
返回对应于indexPath的位置的装饰视图的布局属性,如果没有装饰视图可不重载
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
当边界发生改变时,是否应该刷新布局。如果YES则在边界变化(一般是scroll到其他地方)时,将重新计算需要的布局信息。
- 调用顺序
1)-(void)prepareLayout 设置layout的结构和初始需要的参数等。
2) -(CGSize) collectionViewContentSize 确定collectionView的所有内容的尺寸。
3)-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect初始的layout的外观将由该方法返回的UICollectionViewLayoutAttributes来决定。
4)在需要更新layout时,需要给当前layout发送
1)-invalidateLayout, 该消息会立即返回,并且预约在下一个loop的时候刷新当前layout
2)-prepareLayout,
3)依次再调用-collectionViewContentSize和-layoutAttributesForElementsInRect来生成更新后的布局。
LineLayout:流水布局实例http://www.jianshu.com/p/a16ffb4bbcc2
圆形布局CircleLayout http://www.jianshu.com/p/83e31a2f18d9
方行布局http://www.jianshu.com/p/58e06e1f2f6d