1.目录
- 认识
UICollectionViewLayoutAttributes
和UICollectionViewLayout
; - 熟悉几个常用的方法;
- 自定义瀑布流;
- 自定义环形心动动画;
2.知识储备
2.1认识UICollectionViewLayoutAttributes
下面是UICollectionViewLayoutAttributes
的信息,其实跟我们平时用到的UIView有很多相同的属性,它的作用就是描述UICollectionViewCell、SupplementaryView、DecorationView
这些控件在UICollectionView
上面显示的样式、位置等信息,详细的信息可以看代码里的注释。
@interface UICollectionViewLayoutAttributes : NSObject <NSCopying, UIDynamicItem>
@property (nonatomic) CGRect frame;// 同UIView的frame
@property (nonatomic) CGPoint center;// 同UIView的center
@property (**nonatomic) CGSize size;// 同UIView的size
@property (nonatomic) CATransform3D transform3D;// 用于3D变幻操作
@property (nonatomic) CGRect bounds API_AVAILABLE(ios(7.0));// 同UIView的bounds
@property (nonatomic) CGAffineTransform transform API_AVAILABLE(ios(7.0));// 用于旋转、缩放、平移2D操作
@property (nonatomic) CGFloat alpha;// 同UIView的alpha
@property (nonatomic) NSInteger zIndex; // default is 0 Z轴
@property (nonatomic, getter=isHidden) BOOL hidden; // As an optimization, UICollectionView might not create a view for items whose hidden attribute is YES 同UIView的hidden
@property (nonatomic, strong) NSIndexPath *indexPath;// 位置信息
@property (nonatomic, readonly) UICollectionElementCategory representedElementCategory; // 类型,区分是UICollectionViewCell、SupplementaryView、DecorationView
@property (nonatomic, readonly, nullable) NSString *representedElementKind; // nil when representedElementCategory is UICollectionElementCategoryCell // 类型标识字符串,当时cell的时候是空的
// cell初始化方法
+ (instancetype)layoutAttributesForCellWithIndexPath:(NSIndexPath *)indexPath;
// SupplementaryView初始化方法
+ (instancetype)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind withIndexPath:(NSIndexPath *)indexPath;
// DecorationView初始化方法
+ (instancetype)layoutAttributesForDecorationViewOfKind:(NSString *)decorationViewKind withIndexPath:(NSIndexPath *)indexPath;
@end
通过上面的一些注释,我们发现UICollectionViewLayoutAttributes
这个类还是比较简单,大部分属性都跟我们平常用到的基本一样。每个UICollectionViewCell、SupplementaryView、DecorationView
和UICollectionViewLayoutAttributes
就是一对一的关系,当cell
需要显示在屏幕上的时候,就会取出对应的UICollectionViewLayoutAttributes
对象,告诉UICollectionView
需要显示多大,透明度是多少,位置在哪里,需不需要变换操作等。
2.2认识UICollectionViewLayout
@interface UICollectionViewLayout : NSObject <NSCoding>
- (instancetype)init NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER;
@property (nullable, nonatomic, readonly) UICollectionView *collectionView;
- (void)invalidateLayout; // 告诉collection需要重新布局
- (void)invalidateLayoutWithContext:(UICollectionViewLayoutInvalidationContext *)context API_AVAILABLE(ios(7.0));
- (void)registerClass:(nullable Class)viewClass forDecorationViewOfKind:(NSString *)elementKind;
- (void)registerNib:(nullable UINib *)nib forDecorationViewOfKind:(NSString *)elementKind;
@end
通过上面的的源码,我们看到UICollectionViewLayout
就简单的几个方法,超出了我们对他的认知,其实不是这样的;UICollectionViewLayout
真正在开发中常用对方法是它下面的几个扩展Hooks。
UISubclassingHooks
子类重写会调用的;UIUpdateSupportHooks
布局更新会调用;UIReorderingSupportHooks
响应的时候调用,比如cell互相调换位置;
3.常用方法
平时我们用到的UICollectionViewFlowLayout
其实基本上就是重写了UISubclassingHooks
里面的方法来实现简单的网格布局,这里介绍几个常用的方法的作用。
1.这个方法子类重写必须调用父类的`prepareLayout`,
当`collection`上的`cell`第一次显示在屏幕上或者调用`invalidateLayout`方法都会走这个方法,
这个方法里通常我们会把所有`cell`对应的`UICollectionViewLayoutAttributes`计算好放入一个数组进行保存。
- (void)prepareLayout;
2.这四个方法是用来确定布局信息的
// 返回可视区域内的布局数组
- (nullable NSArray< __kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect;
// 返回指定位置上的布局
- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath;
// SupplementaryView的布局
- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath;
// DecorationView的布局
- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForDecorationViewOfKind:(NSString*)elementKind atIndexPath:(NSIndexPath *)indexPath;
3.当collectionView的bounds发生变化的时候调用,YES会重新布局,NO不会
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds;
4.返回滚动区域,不设置这个就无法滚动
-(CGSize)collectionViewContentSize;
5.返回指定位置的布局
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath;
我们自定义布局都是通过继承UICollectionViewLayout
然后重写上面的5个方法来实现我们自己想要的布局效果,一些比较复杂的效果还需要其他的方法,这里水平有限,暂时先不考虑复杂效果。
4.自定义布局
4.1自定义瀑布流
创建一个SNWaterLayout
继承自UICollectionViewLayout
,然后实现上面的5个方法就能实现一个简单的瀑布流。
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@protocol SNWaterLayoutDelegate <NSObject>
/// 返回item的高度
/// @param collectionView collectionView
/// @param indexPath 位置
- (CGFloat)collectionView:(UICollectionView*)collectionView heightForItemAtIndexPath:(NSIndexPath*) indexPath;
@end
@interface SNWaterLayout : UICollectionViewLayout
@property (nonatomic, assign) NSInteger columns; // 多少列
@property (nonatomic, assign) CGFloat itemSpace; // 间距(这里的间距是上下左右都是一样的,如果需要自定义程度更高的,需要UIEdgeInsets或者类似行间距,列间距这种)
@property (nonatomic, weak) id<SNWaterLayoutDelegate> delegate; // 代理
@end
NS_ASSUME_NONNULL_END
#import "SNWaterLayout.h"
@interface SNWaterLayout()
@property (nonatomic, strong) NSMutableArray *attributesArray; // 保存所有布局
@property (nonatomic, strong) NSMutableArray *offsetYArray; // 记录最后一行每个Cell的底部位置
@end
@implementation SNWaterLayout
/// 初始化
- (instancetype)init {
if(self = [super init]) {
// 初始化
self.attributesArray = [NSMutableArray new];
self.offsetYArray = [NSMutableArray new];
self.columns = 3;
self.itemSpace = 10.0;
}
return self;
}
/// 计算好所有的布局
- (void)prepareLayout {
// 这里必须要调用父类
[super prepareLayout];
// 列小于等于0直接返回
if (self.columns <= 0) {
return;
}
// 1.初始化偏移数组,比如有5列,就用数组长度为5初始化为0
for (int i = 0; i < self.columns; i++) {
[self.offsetYArray addObject:@0];
}
// 2.初始化cell的宽度、高度
CGFloat itemW = (self.collectionView.bounds.size.width - (self.columns - 1) * self.itemSpace) / (self.columns * 1.0);
CGFloat itemH = 0;
// 3.获取多少个item(只做单组布局)
NSInteger items = [self.collectionView numberOfItemsInSection:0];
for (int row = 0; row < items; row++) {
// 4.创建UICollectionViewLayoutAttributes
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:0];
UICollectionViewLayoutAttributes *layoutAttributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
// 5.计算UICollectionViewLayoutAttributes在collectionview中的位置
if ([self.delegate respondsToSelector: @selector(collectionView:heightForItemAtIndexPath:)]) {
itemH = [self.delegate collectionView:self.collectionView heightForItemAtIndexPath:indexPath];
}
// 6.设置frame
CGFloat x = (row % self.columns) * (itemW + self.itemSpace);
CGFloat y = ([self.offsetYArray[row % self.columns] floatValue] + self.itemSpace * (row / self.columns == 0 ? 0 : 1));
layoutAttributes.frame = CGRectMake(x, y, itemW, itemH);
// 7.保存最后一行的cell的最大Y值
self.offsetYArray[row % self.columns] = @(y + itemH);
// 8.保存布局
[self.attributesArray addObject:layoutAttributes];
}
}
/// 返回可视区域内的cell的布局数组
- (NSArray< __kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {
NSMutableArray *allAttributes = [NSMutableArray new];
// 遍历所有的布局,找出在当前屏幕显示的返回
[self.attributesArray enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes * _Nonnull layoutAttributes, NSUInteger idx, BOOL * _Nonnull stop) {
// 判断是否在可视区域内
if (CGRectIntersectsRect(rect, layoutAttributes.frame)) {
[allAttributes addObject:layoutAttributes];
}
}];
return allAttributes;
}
/// 当collectionView的bounds发生变化的时候调用,YES会重新布局,NO不会
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
return YES;
}
/// 返回滚动区域
- (CGSize)collectionViewContentSize {
CGFloat maxHeight = 0;
// 找出最后一行cell中最大的Y值,就是能滚动的最大区域
for (int i = 0; i < self.offsetYArray.count; i++) {
CGFloat offsetY = [self.offsetYArray[i] floatValue];
if (offsetY > maxHeight) {
maxHeight = offsetY;
}
}
return CGSizeMake(self.collectionView.bounds.size.width, maxHeight);
}
/// 返回单个cell的布局
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row < self.attributesArray.count) {
return self.attributesArray[indexPath.row];
}
return nil;
}
创建一个MYCollectionViewController
继承自UICollectionViewController
,现在我们用自定义的SNWaterLayout
来看看实际效果如何。
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface MYCollectionViewController : UICollectionViewController
@end
NS_ASSUME_NONNULL_END
#import "MYCollectionViewController.h"
#import "SNWaterLayout.h"
@interface MYCollectionViewController ()<SNWaterLayoutDelegate>
@end
@implementation MYCollectionViewController
static NSString * const reuseIdentifier = @"Cell";
-(instancetype)init{
SNWaterLayout *layout = [[SNWaterLayout alloc]init];
layout.columns = 3;
layout.itemSpace = 10;
layout.delegate = self;
return [self initWithCollectionViewLayout:layout];
}
- (void)viewDidLoad {
[super viewDidLoad];
[self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:reuseIdentifier];
self.collectionView.backgroundColor = [UIColor whiteColor];
/*
默认为YES,如果YES,则使用系统标准重新排序手势来驱动集合视图重新排序
这也是我继承UICollectionViewController的原因,通过这个简单的属性就能实现cell的移动和兑换位置。
*/
self.installsStandardGestureForInteractiveMovement = YES;
// 添加长按手势
UILongPressGestureRecognizer *longPre = [[UILongPressGestureRecognizer alloc] initWithTarget:**self** action: **@selector**(handleLongGesture:)];
[self.collectionView addGestureRecognizer:longPre];
}
// 通过不同的状态调用几个方法就能实现重排
- (void)handleLongGesture:(UILongPressGestureRecognizer *)gesture {
switch (gesture.state) {
case UIGestureRecognizerStateBegan:
{
NSIndexPath *selectedIndexPath = [self.collectionView indexPathForItemAtPoint:[gesture locationInView:self.collectionView]];
// 开始移动
[self.collectionView beginInteractiveMovementForItemAtIndexPath:selectedIndexPath];
}
break;
case UIGestureRecognizerStateChanged:
{
// 移动中
[self.collectionView updateInteractiveMovementTargetPosition:[gesture locationInView:self.collectionView]];
}
break;
case UIGestureRecognizerStateEnded:
{
// 移动结束
[self.collectionView endInteractiveMovement];
}
break;
default:
[self.collectionView cancelInteractiveMovement];
break;
}
}
/// 这个方法必须要写,要不然移动无效
- (void)collectionView:(UICollectionView *)collectionView moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath {
// 这里实现内部数据的调换
}
#pragma mark <UICollectionViewDataSource>
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
return 1;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return 50;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:reuseIdentifier forIndexPath:indexPath];
cell.backgroundColor = [UIColor greenColor];
UILabel *textLb = nil;
if (![cell viewWithTag:1001]) {
textLb = [UILabel new];
[cell.contentView addSubview:textLb];
textLb.tag = 1001;
textLb.frame = cell.bounds;
textLb.textAlignment = NSTextAlignmentCenter;
textLb.textColor = [UIColor redColor];
}
textLb.text = [NSString stringWithFormat:@"%zd", indexPath.row];
return cell;
}
- (CGFloat)collectionView:(UICollectionView *)collectionView heightForItemAtIndexPath:(NSIndexPath *)indexPath {
return 50 + arc4random() % 100;
}
@end
显示的效果如下:
4.2自定义环形心动动画
通过上面的自定义瀑布流,我们能对自定义Layout
已经基本熟悉了;现在我们做一个环形的布局,这个需求背景是,所有的用户显示在环形上,最中间的是自己,当点击某一个其他的用户的时候,发送一个爱心动画。
每一环就是一组,每组上面有不同数量的用户(数量是固定的),点击下面的
发送爱心
按钮,从某一个外层用户发送一个爱心沿着虚线送到自己的头像的一个动画效果。这里有两个难点:
- 环形布局的计算;
- 动画路径的算法查找;
圆环布局
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface MyLayout : UICollectionViewLayout
@property(nonatomic, assign)CGRect myFrame; // 滚动区域
@property (nonatomic, strong) NSMutableArray *attributeAttay; // 所有的布局
@property (nonatomic, strong, readonly) NSMutableArray *sectionArray; // 保存每组的布局
@end
NS_ASSUME_NONNULL_END
#import "MyLayout.h"
@interface MyLayout()
@property(nonatomic, assign) int items;
@property(nonatomic, assign) int sections;
@property (nonatomic, strong, readwrite) NSMutableArray *sectionArray;
@end
@implementation MyLayout
-(void)prepareLayout{
[super prepareLayout];
// 获取item的个数
self.sections = (int)[self.collectionView numberOfSections];
// 初始化数组
self.sectionArray = [[NSMutableArray alloc] init];
self.attributeAttay = [[NSMutableArray alloc]init];
// 圆半径
CGFloat radius = 60;
// 计算圆心位置
CGPoint center = CGPointMake([self collectionViewContentSize].width/2, [self collectionViewContentSize].height/2);
// 初始化最大的item的大小
CGFloat itemMaxW = 40;
// 缩放比例
CGFloat mutip = 0.8;
// 每组的间隔
CGFloat stepW = 30;
// 初始cell的宽度
CGFloat currentW = itemMaxW;
// 遍历每组
for (int section = 0; section < self.sections; section++) {
self.items = (int)[self.collectionView numberOfItemsInSection:section];
if (self.items == 0) {
return;
}
// 每外一层,半径加5
stepW = stepW + 5;
NSMutableArray *tempArray = [NSMutableArray new];
for (int item = 0; item < self.items; item++) {
// 创建
UICollectionViewLayoutAttributes * attris = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:[NSIndexPath indexPathForItem:item inSection:section]];
[tempArray addObject:attris];
/**
根据缩放比例,比如第0组,是1倍,
第一组缩放0.8倍
第二组缩放0.8*0.8倍
后面的以此类推,这只是我这边自己的算法,你可以根据自己的需要调整
*/
currentW = itemMaxW * pow(mutip, section);
attris.size = CGSizeMake(currentW, currentW);
// 计算每个item的中心坐标
if (section == 0) {
attris.center = center;
[_attributeAttay addObject:attris];
} else {
// 计算每个cell的角度偏移量
CGFloat stepAng = (2 * M_PI) / self.items;
// 根据原点和偏移角度,计算对应的x,y,这里减去M_PI_2是为了从正上方开始布局
float x = center.x + cosf(stepAng * item - M_PI_2)*(radius + stepW * section -currentW * 0.5);
float y = center.y + sinf(stepAng * item - M_PI_2)*(radius + stepW * section - currentW * 0.5);
attris.center = CGPointMake(x, y);
[_attributeAttay addObject:attris];
}
}
[self.sectionArray addObject:tempArray];
}
}
//设置内容区域的大小
-(CGSize)collectionViewContentSize{
return CGSizeMake(self.myFrame.size.width, self.myFrame.size.height);
}
//返回设置数组
-(NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect{
return _attributeAttay;
}
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
return YES;
}
- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
NSArray *sectionArray = self.sectionArray[indexPath.section];
UICollectionViewLayoutAttributes * attris = sectionArray[indexPath.row];
return attris;
}
@end
对比上面的瀑布流布局,其实代码都是差不多的,只是每种样式的prepareLayout
内部的布局计算不同。
爱心动画
我们在控制器里,用环形布局创建一个UICollectionView
,再添加一个按钮,随机从最外层发送一个爱心到中间的位置,这里涉及比较复杂的计算,这里先简单的说一下我的路径计算的思路。
- 从最外层的某一个点
A
用户出发, - 依次遍历里面的一层,找出距离A头像位置最近的一个用户的头像
B
,记录min_attri
, - 找出B点的前一个用户头像
C
、后一个用户头像D
, - 计算
B-C
的中心点center1
,B-D
的中心点center2
, - 计算
A-center1
的距离distance1
,计算A-center2
的距离distance2
, - 比较
distance1
和distance2
谁最近,如果distance1 < distance2
, 那么nearAttri = preAttri
,反之nearAttri = afterAttri
; - 计算
min_attri
到nearAttri
的中心点,并用attri_center
记录, - 用
points
数组保存nearAttri.center
,这个点其实就是我们绘画路径的控制点。
下面我们看一下具体代码实现,然后再回过头来看上面的思路,就比较清晰了。
爱心视图HeartView
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface HeartView : UIView
@end
NS_ASSUME_NONNULL_END
#import "HeartView.h"
@interface HeartView()
@property (nonatomic, strong) UIImageView *imageView;
@end
@implementation HeartView
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
self.backgroundColor = [UIColor clearColor];
self.imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 55, 55)];
self.imageView.image = [UIImage imageNamed:@"image 79"];
// 动画相关的设置
self.imageView.transform = CGAffineTransformMakeRotation(M_PI);
[self addSubview:self.imageView];
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"];
animation.values = @[@0.6, @1.0, @1.2, @1.0, @0.6];
animation.repeatCount = MAXFLOAT;
animation.duration = 1.5;
animation.fillMode = kCAFillModeForwards;
animation.autoreverses = NO;
animation.removedOnCompletion = NO;
[self.layer addAnimation:animation forKey:nil];
}
return self;
}
@end
创建环形UICollectionView
#import "ViewController.h"
#import "MyLayout.h"
#import "HeartView.h"
@interface ViewController ()<UICollectionViewDelegate, UICollectionViewDataSource>
@property (nonatomic, strong) MyLayout * layout;
@property (nonatomic, strong) CAShapeLayer *shaperLayer;
@property (nonatomic, strong) HeartView *redView;
@end
- (**void**)viewDidLoad {
[super viewDidLoad];
// 添加CollectionView
[self addCircleRingView];
}
- (void)addCircleRingView {
// 初始化
MyLayout * layout = [[MyLayout alloc] init];
layout.myFrame = self.view.bounds;
UICollectionView *collect = [[UICollectionView alloc]initWithFrame:self.view.bounds collectionViewLayout:layout];
collect.delegate = self;
collect.dataSource = self;
self.collect = collect;
[collect registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"cellid"];
[self.view addSubview:collect];
self.layout = layout;
collect.backgroundColor = [UIColor whiteColor];
// 用户路径绘制
CAShapeLayer *shaperLayer = [CAShapeLayer new];
shaperLayer.frame = collect.bounds;
shaperLayer.fillColor = nil;
shaperLayer.strokeColor = [UIColor grayColor].CGColor;
shaperLayer.backgroundColor = [UIColor clearColor].CGColor;
[self.view.layer addSublayer:shaperLayer];
self.shaperLayer = shaperLayer;
// 按钮
UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
btn.backgroundColor = [UIColor redColor];
[btn setTitle:@"发送爱心" forState:UIControlStateNormal];
[self.view addSubview:btn];
btn.frame = CGRectMake(100, self.view.bounds.size.height - 100, self.view.bounds.size.width - 200 , 50);
[btn addTarget:self action: @selector(btnAction) forControlEvents:UIControlEventTouchUpInside];
// 爱心
self.redView = [HeartView new];
[self.view addSubview:self.redView];
}
- (void)btnAction {
// 随机产生row
NSInteger row = arc4random() % 18;
[self addHeartAnimationWithRow:row];
}
- (void)addHeartAnimationWithRow:(NSInteger)row {
self.redView.frame = CGRectMake(110, 110, 55, 55);
// 获取最外面的item的位置信息
NSIndexPath *star_indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
UICollectionViewLayoutAttributes *star_attri = [self.layout layoutAttributesForItemAtIndexPath:star_indexPath];
NSIndexPath *end_indexPath = [NSIndexPath indexPathForRow:row inSection:3];
UICollectionViewLayoutAttributes *end_attri = [self.layout layoutAttributesForItemAtIndexPath:end_indexPath];
self.redView.center = end_attri.center;
NSMutableArray *attri_point = [NSMutableArray new];
NSMutableArray *points = [NSMutableArray new];
// 创建贝塞尔曲线
UIBezierPath *pathArc = [UIBezierPath bezierPath];
[pathArc moveToPoint:end_attri.center];
// 初始化位置
CGPoint star_point = end_attri.center;
for (int section = (int)(self.layout.sectionArray.count - 2); section >= 0; section--) {
NSArray *sections = self.layout.sectionArray[section];
// 两点之间的距离
CGFloat distance = MAXFLOAT;
// 记录中心点位置
CGPoint attri_center = CGPointZero;
// 记录最小间距的attri
UICollectionViewLayoutAttributes *min_attri = nil;
for (int row = 0; row < sections.count; row++) {
UICollectionViewLayoutAttributes *attri = sections[row];
// 找出跟star_point距离最近的点(x1 - x2)^2 + (y1 - y2)^2最小
CGFloat current_distance = (star_point.x - attri.center.x)*(star_point.x - attri.center.x) + (star_point.y - attri.center.y)*(star_point.y - attri.center.y);
if (current_distance < distance) {
distance = current_distance;
min_attri = attri;
}
}
// 记录跟当前点位距离最小的的点
[attri_point addObject:min_attri];
if ([[NSValue valueWithCGPoint:attri_center] isEqualToValue:[NSValue valueWithCGPoint:CGPointZero]]) {
attri_center = star_point;
}
// 找出最小距离的前一个点或者后一个点
NSInteger row = min_attri.indexPath.row;
UICollectionViewLayoutAttributes *nearAttri = nil;
if (section == 0) {
nearAttri = min_attri;
} else {
// 前一个item
NSInteger preRow = row - 1;
if (preRow < 0) {
preRow = sections.count - 1;
}
UICollectionViewLayoutAttributes *preAttri = sections[preRow];
// 后一个item
NSInteger afterRow = row + 1;
if (afterRow >= sections.count) {
afterRow = 0;
}
UICollectionViewLayoutAttributes *afterAttri = sections[afterRow];
// 计算前后两个的中点位置
CGFloat x_center_1 = (min_attri.center.x + preAttri.center.x) / 2.0;
CGFloat y_center_1 = (min_attri.center.y + preAttri.center.y) / 2.0;
CGFloat x_center_2 = (min_attri.center.x + afterAttri.center.x) / 2.0;
CGFloat y_center_2 = (min_attri.center.y + afterAttri.center.y) / 2.0;
// 找出最近距离的点,前后两个item的中心点,看哪个中心点跟当前的点的距离最近,找到这个最近的item
long long distance1 = [self distancWithPoint1:CGPointMake(x_center_1, y_center_1) point2:attri_center];
long long distance2 = [**self** distancWithPoint1:CGPointMake(x_center_2, y_center_2) point2:attri_center];
if (distance1 < distance2) {
nearAttri = preAttri;
} else {
nearAttri = afterAttri;
}
}
// 求出两点之间的中点
CGFloat x_center = (min_attri.center.x + nearAttri.center.x) / 2.0;
CGFloat y_center = (min_attri.center.y + nearAttri.center.y) / 2.0;
// 记录上一次的点位
attri_center = CGPointMake(x_center, y_center);
[points addObject:[NSValue valueWithCGPoint:nearAttri.center]];
}
// 注意这里因为我只做了最外出,所以一定会有两个点,具体情况可能不同
CGPoint p1 = [points[0] CGPointValue];
CGPoint p2 = [points[1] CGPointValue];
[pathArc addCurveToPoint:star_attri.center controlPoint1:p1 controlPoint2:p2];
// 线绘制
pathArc.lineWidth = 2;
pathArc.lineCapStyle = kCGLineCapSquare;
pathArc.lineJoinStyle = kCGLineJoinRound;
// 设置线宽,线间距
[self.shaperLayer setLineDashPattern:[NSArray arrayWithObjects:[NSNumber numberWithInt:4], [NSNumber numberWithInt:4], nil]];
self.shaperLayer.path = pathArc.CGPath;
// 动画
CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
pathAnimation.path = pathArc.CGPath; // 加入贝塞尔路径
pathAnimation.rotationMode = kCAAnimationRotateAuto;
pathAnimation.fillMode = kCAFillModeForwards;
pathAnimation.autoreverses = NO;
pathAnimation.removedOnCompletion = NO;
pathAnimation.duration = 2.0;
[self.redView.layer addAnimation:pathAnimation forKey:@"position"];
}
- (long long)distancWithPoint1:(CGPoint)point1 point2:(CGPoint)point2 {
long long distance = (point1.x - point2.x)*(point1.x - point2.x) + (point1.y - point2.y)*(point1.y - point2.y);
return distance;
}
#pragma mark - UICollectionViewDataSource
-(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView{
return 4;
}
-(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
switch (section) {
case 0:
return 1;
break;
case 1:
return 6;
break;
case 2:
return 12;
break;
case 3:
return 18;
break;
default:
return 0;
break;
}
return 0;
}
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
UICollectionViewCell * cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"cellid" forIndexPath:indexPath];
cell.backgroundColor = [UIColor colorWithRed:arc4random()%255/255.0 green:arc4random()%255/255.0 blue:arc4random()%255/255.0 alpha:1];
UICollectionViewLayoutAttributes *attri = [self.layout layoutAttributesForItemAtIndexPath:indexPath];
UILabel *lb = nil;
if ((lb = [cell viewWithTag:100]) == nil) {
lb = [[UILabel alloc] initWithFrame:CGRectMake(5, 5, 20, 20)];
lb.text = [NSString stringWithFormat:@"%zd", indexPath.row];
lb.tag = 100;
lb.textColor = UIColor.whiteColor;
lb.center = CGPointMake(cell.frame.size.width / 2.0, cell.frame.size.height / 2.0);
[cell.contentView addSubview:lb];
}
cell.layer.cornerRadius = attri.size.width / 2.0;
cell.layer.masksToBounds = YES;
return cell;
}
5.结尾
首先对UICollectionViewLayout
和UICollectionViewLayoutAttributes
两个类的基本认识,熟悉它的常用的方法,最后通过两个实例加以运用,希望能够加深对UICollectionView
自定义布局的理解,要是文中有错误或者不妥的地方,请大佬们留言哈,只是一个Demo所以代码不够规范!📢📢📢 特别提醒一下,最后的环形爱心动画,目前只做了最外层到最里层的动画效果,如果需要从任意一层到中心点的动画,可能需要不同的算法,上面的思路只是一个简易版,后续有哪位大佬实现了,可以留言给我参考参考。