如何计算不接触 UIView 的随机 CGPoint
Posted
技术标签:
【中文标题】如何计算不接触 UIView 的随机 CGPoint【英文标题】:How to calculate a random CGPoint which does not touch a UIView 【发布时间】:2017-08-09 14:25:18 【问题描述】:这是一个示例视图:
我想计算一个带有CGPoint
的框架,我可以在其中生成另一张卡片(UIView
),而无需触及任何现有卡片。当然,这是可选的,因为视图可以充满卡片,因此没有空闲位置。
这就是我在屏幕上看到任何卡片的方式以及我现在的功能:
func freeSpotCalculator() -> CGPoint?
var takenSpots = [CGPoint]()
for card in playableCards
takenSpots.append(card.center)
我不知道从哪里开始以及如何计算屏幕上的随机CGPoint
。随机帧与屏幕上的卡片具有相同的width
和height
。
【问题讨论】:
您的示例中的位置非常规则:卡片底部和距离对齐。你想保持这一点,还是卡片真的必须在其超级视图范围内的随机位置生成? @JackGoossen 理想情况下,在他们的超级视图范围内,图片只是带有卡片的视图示例。这些卡片可能在其他地方。 @J.Doe,卡片可以出现在完全随机的点上,还是有像 Shaan Singh 的答案那样的网格?如果存在网格,则该答案可以解决您的问题。 【参考方案1】:这种天真的方法非常简单,但一旦屏幕填满可能会出现问题。生成一个随机 CGPoint,x 坐标在 0 和屏幕宽度之间,y 坐标在 0 和屏幕高度之间。检查以该点为中心的矩形是否与任何现有视图相交。如果没有,你有你的随机位置。
当屏幕开始填满时,就会出现问题。那时,您可能会尝试许多随机点,然后才能找到放置卡片的位置。您也可能会遇到没有更多卡片适合的情况。你怎么知道你已经达到了?生成随机点的循环会永远运行吗?
更智能的解决方案是跟踪屏幕上的可用空间。始终在这些空闲空间内大致生成随机点。如果近似值足够接近,您可以使用网格来执行此操作。是否有一张卡片占据每个网格位置?然后,当最大的可用空间小于卡片矩形的大小时,你就知道你已经完成了。这比幼稚的方法工作量大得多,但是当屏幕开始填满时它会更快,并且您会确定何时完成。
如果您知道您的屏幕空间总是超过卡片可能占用的空间,那么幼稚的方法就可以了。
【讨论】:
请注意,您不必使用网格来跟踪可用空间。您还可以使用一组矩形,并在插入重叠的卡片时拆分矩形。【参考方案2】:想法
您知道容器 UIView 的宽度和高度。而且,每张卡片的宽度和高度都相同。我会通过计算网格来解决这个问题。
即使您想随机显示卡片,依赖网格也会为您提供标准化的中心数组,您可以使用这些数组来生成随机外观(将卡片放置在属于网格一部分的任意随机中心,例如)。
如果您要将卡片放置在真正的随机位置,您可能只想使用CGRectIntersectsRect(card1.frame, card2.frame)
来检测碰撞。
模式
首先,让我们将卡片的宽度和高度存储为常量。
let cardWidth = card.bounds.size.width
let cardHeight = card.bounds.size.height
作为概念的基本证明,假设您的容器视图宽度为 250 磅。假设卡片宽度为 5 点。这意味着您可以在一行中放置 250 / 5 = 50 张卡片,其中一排的高度相当于一张卡片。
一行中的中心数=该行中的卡片数。每个中心之间的距离相同。在下图中(如果我什至可以这样称呼它),[
和 ]
代表一行的边缘。 -|-
代表一张卡片,其中|
是卡片的中心。
[ - | - - | - - | - - | - - | - ]
请注意每个中心距离下一个中心的距离如何。唯一需要考虑的是,边缘旁边的中心距离边缘只有一小段距离。就卡片而言,每个中心与下一个中心相距一整张牌,边缘旁边的中心与边缘相距半张牌。
模式的关键
此模式意味着特定行中任何卡片中心的 x 位置 = (cardWidth / 2) + (the card index * cardWidth)
。事实上,这个伪方程也适用于 y 位置。
代码
这里有一些使用这种方法创建中心数组的 Swift。
var centers = [CGPoint]()
let numberOfRows: CGFloat = containerView.bounds.size.height / cardHeight
let numberOfCardsPerRow: CGFloat = containerView.bounds.size.width / cardWidth
for row in 0 ..< Int(numberOfRows)
for card in 0 ..< Int(numberOfCardsPerRow)
// The row we are on affects the y values of all the centers
let yBasedOnRow = (cardHeight / 2) + (CGFloat(row) * cardHeight)
// The xBasedOnCard formula is effectively the same as the yBasedOnRow one
let xBasedOnCard = (cardWidth / 2) + (CGFloat(card) * cardWidth)
// Each possible center for this row gets appended to the centers array
centers.append(CGPoint(x: xBasedOnCard, y: yBasedOnRow))
此代码应为您的卡片创建一个中心网格。您可以围绕它构建一个函数,该函数返回一个随机中心用于放置卡片并跟踪使用的中心。
可能的改进
首先,我认为centers
数组可以做成一个矩阵([[CGPoint]]()
),以便对点进行更多的逻辑存储。
其次,这段代码目前假设容器视图的宽度和高度可以被卡片的宽度和高度整除。例如,容器宽度为 177,卡片宽度为 5 会导致一些问题。可以通过多种不同的方式修复代码以解决此问题。
【讨论】:
【参考方案3】:最简单/性能的最佳解决方案是在网格内随机显示卡片。诀窍是让网格大于卡片大小,因此网格内的卡片位置将是随机的。
很容易检查哪个位置被占用,卡片将在“随机”帧上。
1- 创建一个集合视图控制器,其中包含您要显示的卡片总数(比如说.. 进入屏幕的最大卡片数?)
2- 将原型单元尺寸设置为大于卡。如果卡片是 50x80,那么单元格应该是 70x110。
3- 将 UIImageView 添加到带有约束的单元格中,这将是您的卡片图像
4- 创建一个 UICollectionViewCell,其方法是在单元格内随机设置卡片框架(修改约束)
完成!
没有卡片的单元格将没有图像或您希望的空单元格。因此,要添加一张新卡片,只需在空单元格之间进行随机操作,然后将卡片及其随机坐标添加到单元格内即可。
你的 UICollectionViewCell 会喜欢这个
class CardCollectionViewCell: UICollectionViewCell
@IBOutlet weak var card: UIImageView!
override func awakeFromNib()
super.awakeFromNib()
let newX = CGFloat(arc4random_uniform(UInt32(bounds.size.width-card.bounds.size.width+1)))
let newY = CGFloat(arc4random_uniform(UInt32(bounds.size.height-card.bounds.size.height+1)))
card.leftAnchor.constraintEqualToAnchor(leftAnchor, constant: newX).active = true
card.topAnchor.constraintEqualToAnchor(rightAnchor, constant: newY).active = true
你的 Collections View Controller 应该是这样的 Collection View Image
【讨论】:
【参考方案4】:正如我在图片中看到的那样,您的所有卡片都在视图的底部对齐。因此,如果您从 0 to origin of your cards row - one card height
生成随机 y 位置,您可以简单地根据您的视图框架和卡片大小获得一个 CGPoint。
【讨论】:
可能会有另一种视图,其中更多卡片随机放置在屏幕上。图片是一个例子。如果在我展示的图片中产生了 1 张牌,那张牌确实在其他显示的牌上方。但是,当我想生成另一张卡片时,事情就变得复杂了……【参考方案5】:如果你想在屏幕上随机放置卡片,你可以这样做:
func random() -> CGFloat
return CGFloat(Float(arc4random()) / 0xFFFFFFFF)
func random(min: CGFloat, max: CGFloat) -> CGFloat
return random() * (max - min) + min
let actualX = random(min: *whatever amount*, max: *whatever amount* )
let actualY = random(min: *Height of the first card*, max: *whatever amount* )
card.position = CGPoint(x: actualX, y: actualY )
然后卡片将随机放置在现有卡片上方。
【讨论】:
OP 明确表示他不想碰现有的卡片。【参考方案6】:我不确定您是否打算将所有卡片有序放置。但如果你这样做,你可以这样做。
加载视图后,获取所有可能的卡片位置并将它们与用作键的数字一起存储在地图中。然后,您可以生成一个从 0 到您存储在地图中的可能卡片位置总数的随机数。然后每次占据一个位置,从地图中清除一个值。
【讨论】:
【参考方案7】:您可以尝试使用 CAShapeLayer 和 UIBezierPath。
-
为您的主视图创建一个 CAShapeLayer,您将在其中添加子视图。我们称它为主要形状层。这将有助于检查估计的新视图是否在主视图中。
创建一个 UIBezierPath 实例。每当找到有效的新子视图时,将边添加到此路径。
在主视图中创建一个随机点。
创建一个基于随机点作为子视图中心的 CGRect。我们称其为估计的视图框架。
检查估计的视图框架在主视图中是否完全可见。否则转到第 3 步。
使用路径对象检查估计视图框架的 4 个边缘。如果任何一条边在路径内,请转到第 3 步。
如果 4 条边不在路径内,则估计的视图框架是新视图的框架。
创建一个新的子视图并将其添加到您的主视图中。
将新视图的边缘添加到路径。
我已经使用上述逻辑快速创建了一个示例项目。
https://github.com/mcabasheer/find-free-space-in-uiview
您可以更改新视图的宽度和高度。我添加了一个条件,在尝试 50 次后停止寻找下一个可用空间。这将有助于避免无限循环。
正如@davecom 强调的那样,使用随机数添加新视图会浪费空间,并且您将很快耗尽空间。如果您能够保持可用空间,则可以添加更多子视图。
【讨论】:
以上是关于如何计算不接触 UIView 的随机 CGPoint的主要内容,如果未能解决你的问题,请参考以下文章
统计从多个 UIView 中随机抽出的 UIView 数量,即:从 9 个 UIView 中随机抽出 5 个 UIView,然后执行操作