早期制作-圈子布局

最近,我们发布了Early Game Alarm 2.0,它以7种语言提供并采用了全新的设计,因此我们决定就在(几乎)3年的开发中遇到的各种问题分享我们的经验。 我以前的帖子是关于页面布局的 ,我们曾用来向用户介绍可用的游戏和游戏包。 这将继续解决自定义布局的问题,我将使用一个简单的项目来说明我们如何在主屏幕上排列圆形警报。

在上面的图像中,您可以看到它在应用程序中的外观,在下面的图像中,我们将尝试在本教程中实现。 您可以从此链接下载完整的代码。

因此,任务很简单:我们正在收集特定半径的蓝色圆圈。 半径值显示在中间的标签上。 好的,它们不一定非要是蓝色-您可以选择任何想要的颜色。 让我们开始吧!

注意1:您可以 Byteout Software博客 找到具有 更好格式 的原始文章

注意2:本教程适用于已经具有自动布局和集合视图经验的开发人员。 我们不会详细介绍每个步骤。 对于初学者的教程,我建议从 Ray Wenderlich 教程开始。

步骤1:情节提要设置

创建新项目后,在情节提要中将获得一个视图控制器(类ViewController)。 对于本演示,我们将使用该演示。

让我们在Storyboard中添加以下内容:1个集合视图和1个标签在集合视图单元格中。

集合视图设置:

  • 让我们将集合视图的顶部,底部,尾部和前部约束设置为0值。
  • 不要忘记将ViewController连接为数据源并为集合视图委托。

单元格设置:

  • 在超级视图(单元格)中水平和垂直居中放置标签
  • 为背景和文本设置漂亮的字体和颜色
  • 将单元格标识符设置为“ CircleCell”
  • 创建自定义UICollectionViewCell(例如CircleCell)并连接插座。 例如,如果您不知道如何,可以查看此帖子。

步骤2:资料来源

现在,要实际显示单元格,我们需要实现UICollectionViewDataSource方法。 在ViewController.m中添加以下代码:

// ViewController.m @interface ViewController () @property (strong, nonatomic) NSMutableArray *dataSource; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.dataSource = [[NSMutableArray alloc] initWithObjects:@120, @160, @80, @120, @80, @140, @100, @200, nil]; } #pragma mark -  - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { return self.dataSource.count; } - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { CircleCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"CircleCell" forIndexPath:indexPath]; if (cell == nil) { cell = [[CircleCell alloc] init]; } cell.circleLabel.text = [NSString stringWithFormat:@"%@", self.dataSource[indexPath.row]]; cell.layer.cornerRadius = cell.frame.size.width / 2; return cell; } @end 

这是什么意思 首先,我们在viewDidLoad方法中创建了半径数组(只是一个随机示例)。 然后,我们实现了两种强制性数据源方法:

  • 第一个获取将在屏幕上显示的单元格数
  • 第二个是出队和配置实际单元。 在这里,我们只是将适当的值设置为标签的文本和角半径。

生成并运行,您应该看到类似以下内容的内容:

如果圆圈不规则,则可以在情节提要中调整单元格的尺寸-将其设置为正方形。

好的,所以我们制作了显示正确值的圆形。 但是,尺寸都是错误的。 因此,让我们继续执行步骤3。

步骤3:调整单元大小

为每个单元(您可能已经知道)设置不同大小的最简单方法是实现UICollectionViewFlowLayoutDelegate的函数: collectionView:layout:sizeForItemAtIndexPath:。

像这样更改类声明

 // ViewController.m @interface ViewController () 

然后添加:

 // ViewController.m - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { return CGSizeMake([self.dataSource[indexPath.row] doubleValue], [self.dataSource[indexPath.row] doubleValue]); } 

生成并运行。 圆现在应该是正确的尺寸。 但是,圆的布置不是最佳的。 您可以看到它们所在的网格。其中一些之间有很多未使用的空间。

步骤4:自订

根据文档,如果布局看起来不像是网格或基于行的中断布局,则我们应该将UICollectionViewLayout子类化,并实现以下功能:

  • collectionViewContentSize
  • layoutAttributesForElementsInRect:
  • layoutAttributesForItemAtIndexPath:
  • prepareLayout

顾名思义,第一个用于获取集合视图的总内容大小,例如滚动视图的内容大小。 在需要时调用后两个以检索布局信息。

第四个是所有魔术发生的地方。 在prepareLayout中,我们需要计算集合视图中每个项目的位置和尺寸(框架)并保存(缓存)它们,以便前面的两个函数在需要时可以使用它们。 听起来复杂吗? 您将(希望)一路理解它。

让我们将新类添加到其父类为UICollectionViewLayout的项目(CircleLayout)中。

首先,我们需要定义变量以存储所有布局信息,并初始化类:

 // CirclesLayout.h @property (nonatomic, strong) NSDictionary *layoutInfo; @property (nonatomic) float contentHeight; @property (nonatomic) UIEdgeInsets viewInsets; 
 // CirclesLayout.m - (id)init { self = [super init]; if (self) { [self setup]; } return self; } - (id)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if (self) { [self setup]; } return self; } - (void)setup { self.viewInsets = UIEdgeInsetsMake(20, 20, 20, 20); // top, left, bottom, right } 

然后,让我们看看如何在那些必需的功能中使用缓存的布局信息:

 // CirclesLayout.m - (CGSize)collectionViewContentSize { return CGSizeMake(self.collectionView.bounds.size.width, self.contentHeight); } - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath { return self.layoutInfo[indexPath]; } - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect { NSMutableArray *allAttributes = [NSMutableArray arrayWithCapacity:self.layoutInfo.count]; [self.layoutInfo enumerateKeysAndObjectsUsingBlock:^(NSIndexPath *indexPath, UICollectionViewLayoutAttributes *attributes, BOOL *innerStop) { if (CGRectIntersectsRect(rect, attributes.frame)) { [allAttributes addObject:attributes]; } }]; return allAttributes; } 

第一个和第二个非常简单。 在第三个中,我们遍历每个indexPath的布局属性,如果它们在给定的矩形内,则返回它们。
现在,我们只需要计算这些布局属性即可。 如前所述,这是在prepareLayout函数中完成的。

 // CirclesLayout.h @property (nonatomic) float initialX; @property (nonatomic) float initialY; 
 // CirclesLayout.m - (void)prepareLayout { NSMutableDictionary *newLayoutInfo = [NSMutableDictionary dictionary]; self.contentHeight = self.viewInsets.top; self.initialX = self.viewInsets.left; self.initialY = self.viewInsets.top; NSIndexPath *indexPath; NSInteger itemCount = [self.collectionView numberOfItemsInSection:0]; for (NSInteger item = 0; item < itemCount; item++) { indexPath = [NSIndexPath indexPathForItem:item inSection:0]; UICollectionViewLayoutAttributes *itemAttributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]; itemAttributes.frame = [self frameForCircleAtIndexPath:indexPath layoutInfo:newLayoutInfo]; newLayoutInfo[indexPath] = itemAttributes; self.contentHeight = MAX(self.contentHeight, (itemAttributes.frame.origin.y + itemAttributes.frame.size.height)); } self.contentHeight += self.viewInsets.bottom; self.layoutInfo = newLayoutInfo; } 

要计算每个圆的框架,我们需要检查新圆是否与n个前碰撞。 只要它做到了(不满足距离标准),我们就将其原点移动某个位置增量 。 当我们确定一个圆圈可以适当地适合屏幕时,我们为其创建一个框架,将其存储在layoutInfo中 ,并更新下一个圆圈的初始位置。

 // CirclesLayout.h #define kPositionIncrement 5 
 // CirclesLayoyt.m - (CGRect)frameForCircleAtIndexPath:(NSIndexPath *)indexPath layoutInfo:(NSMutableDictionary *)newLayoutInfo { float originX = self.initialX, originY = self.initialY; float width = [self sizeForItemAtIndexPath:indexPath].width; CGRect circle; circle = CGRectMake(originX, originY, width, width); while (![self distanceConditionForItem:circle AtIndexPath:indexPath InLayout:newLayoutInfo]) { originX += kPositionIncrement; if (originX + width + self.viewInsets.right > self.collectionView.bounds.size.width) { originX = self.viewInsets.left; originY += kPositionIncrement; } circle = CGRectMake(originX, originY, width, width); } // Set initial X i Y for the next circle. self.initialX = originX + (width / 2); self.initialY = originY; return CGRectMake(originX, originY, width, width); } 

尺寸

我们已经在ViewController中定义了圆圈的大小; 它由我们的数据源决定。 因此,我们可以使用类似的方法在此处获取此信息:

 - (CGSize)sizeForItemAtIndexPath:(NSIndexPath *)indexPath { if ([[self.collectionView.delegate class] conformsToProtocol:@protocol(UICollectionViewDelegateFlowLayout)]) { return [(id)self.collectionView.delegate collectionView:self.collectionView layout:self sizeForItemAtIndexPath:indexPath]; } return CGSizeMake(0, 0); } 

位置

对于此特定的布局设置,足以检查圆是否与最多4个前驱碰撞。
数学提醒:如果圆心之间的距离大于圆心的半径与圆弧之间的可选距离之和,则圆不会碰撞。

 // CirclesLayout.h #define kMinSpaceBetweenCircles 10 #define kMaxPredecessorNum 4 
 // CirclesLayout.m - (BOOL)distanceConditionForItem:(CGRect)circle AtIndexPath:(NSIndexPath *)indexPath InLayout:(NSMutableDictionary *)newLayoutInfo { BOOL condition = YES; long numPredecessors = indexPath.row; if (indexPath.row >= kMaxPredecessorNum) { numPredecessors = kMaxPredecessorNum; } for (int i = 1; i = r1 + r2 + delta); } 

最后步骤

转到情节提要,并将集合视图设置为使用我们刚刚制作的自定义布局。

构建并运行,仅此而已!