可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
This is an example view:
I want to calculate a frame with a CGPoint
where I can spawn another card(UIView
) without touching any existing card. Ofcourse it is optional since the view can be full of cards, therefore there is no free spot.
This is how I can see any card on the screen and my function how it is now:
func freeSpotCalculator() -> CGPoint?{
var takenSpots = [CGPoint]()
for card in playableCards{
takenSpots.append(card.center)
}
}
I have no idea where to start and how to calculate a random CGPoint
on the screen. The random frame has the same width
and height
as a card in on the screen.
回答1:
The naive approach to this is very simple, but could be problematic once the screen fills up. Generate a random CGPoint with x coordinate between 0 and the screen width and a y coordinate between 0 and the screen height. Check if a rectangle with a center at that point intersects any existing view. If it does not, you have your random position.
Where this gets problematic is when the screen starts to fill up. At that point you could be trying many many random points before finding a place to put the card. You could also reach a situation where no more cards will fit. How do you know that you have reached that? Will your loop generating the random points just run forever?
A smarter solution is to keep track of the free spaces on the screen. Always generate your random points roughly within these free spaces. You could do this using a grid if approximate is close enough. Is there a card occupying each grid location? Then when the largest free space is smaller than the size of your card rectangle, you know you're done. It's a lot more work than the naive approach, but it's faster when the screen starts to fill up and you'll know for sure when you're done.
If you know that you will always have more screen space than the cards can possibly take up, the naive approach is fine.
回答2:
The idea
You know the width and height of your container UIView. And, each card has the same width and height. I would go about this by calculating a grid.
Even though you want to display cards randomly, relying on a grid will give you a standardized array of centers that you can use to generate the appearance of randomness (place a card at any random center that is a part of the grid, for example).
If you were to place a card at truly any random location, you might just want to use CGRectIntersectsRect(card1.frame, card2.frame)
to detect collisions.
The pattern
First, let's store the card width and height as constants.
let cardWidth = card.bounds.size.width
let cardHeight = card.bounds.size.height
As a basic proof of concept, let's say your container view width is 250 points. Let's say the card width is 5 points. That means you can fit 250 / 5 = 50 cards in one row, where one row has the height of one card.
The number of centers in a row = the number of cards in that row. Each center is the same distance apart. In the following diagram (if I can even call it that), the [
and ]
represent edges of a row. The -|-
represents a card, where |
is the center of the card.
[ - | - - | - - | - - | - - | - ]
Notice how every center is two dashes away from the next center. The only consideration is that the center next to the edge is one dash away from the edge. In terms of cards, each center is one whole card away from the next, and the centers next to the edges are one half card away from the edges.
The key to the pattern
This pattern means that the x position of any card center in a specific row = (cardWidth / 2) + (the card index * cardWidth)
. In fact, this pseudo-equation works for y positions as well.
The code
Here's some Swift that creates an array of centers using this method.
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))
}
}
This code should create a grid of centers for your cards. You could build a function around it that returns a random center for a card to be placed and keeps track of used centers.
Potential improvements
First, I think that the centers
array could be made a matrix ([[CGPoint]]()
) for more logical storage of points.
Second, this code currently makes the assumption that the width and height of the container view are divisible by the card width and height. For example, a container width of 177 and a card width of 5 would result in some problems. The code could be fixed a number of different ways to account for this.
回答3:
Best solution simplest/performance is to display card randomly BUT inside a grid. The trick is to have the grid bigger than the card size, so the card position inside the grid will be random.
Easy to check which position is occupy and cards will be on "random" frames.
1- Create a Collection View Controller with the total number of card u want to display (lets say.. max card that enter in the screen?)
2- Set the prototype cell size bigger than the card. If the card is 50x80 then the cell should be 70x110.
3- Add a UIImageView to the cell with constraints, this will be your card image
4- Create a UICollectionViewCell, with a method that set the card frames randomly inside the cell (modify the constraints)
Done!
Cells with no card will have no image or an empty cell as you wish. So to add a new card, just do a random between the empty cells and add the card with its random coordinates inside the cell.
Your UICollectionViewCell would like like this
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
}
}
And your Collections View Controller should like like this
Collection View Image
回答4:
As I can see in the picture all your cards are aligned at the bottom of the View. so if you generate a random y position from 0 to origin of your cards row - one card height
you can simply get a CGPoint based on the frame of your view and size of the cards.
回答5:
If you want to randomly place cards along the screen, you could do something like this:
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 )
The cards will then be positioned randomly above the existing cards.
回答6:
I am not sure if you are planning to place all the cards in an orderly way. But if you do, you could do it like this.
Once the view is loaded, get all the possible card positions and store them in a map together with a number used as the key. Then you could generate a random number from 0 to the total number of possible card positions that you stored in the map. Then every time you occupy a position, clear a value from the map.
回答7:
You can try with CAShapeLayer and UIBezierPath.
- Create a CAShapeLayer for your main view where you will be adding sub views. Let's call it as main shape layer. This will be helpful to check the new view estimated is within the main view.
- Create a UIBezierPath instance. Whenever a valid new sub view is found, add the edges to this path.
- Create a random point within the main view.
- Create a CGRect based on random point as center of your sub view. Let’s call it as estimated view frame.
- Check the estimated view frame is completely visible in your main view. Else go to step 3.
- Check your 4 edges of your estimated view frame with path object. If any one of the edge is inside the path, go to step 3.
- If 4 edges are not inside the path, the estimated view frame is the new view’s frame.
- Create a new subview and add it to your main view.
- Add the edges of new view to path.
I have created a sample project in swift with the above logic.
https://github.com/mcabasheer/find-free-space-in-uiview
You can change the width and height of new view. I have added a condition to stop looking for next free space after trying 50 times. This will help to avoid infinite loop.
As @davecom highlighted, taking a random number to add new view will waste the space and you will run out of space quickly. If you are able to maintain the free available space, you can add more sub views.