[英]UICollisionBehavior detection between round subViews in square parentViews
I have a square containerView with a roundImageView inside of it.我有一个方形的 containerView,里面有一个 roundImageView。 The containerView is added to the UIDynamicAnimator.
containerView 被添加到 UIDynamicAnimator。 When the corners of the containerViews collide off of each other I need them to bounce off of the roundImageView, same as this question .
当 containerViews 的角相互碰撞时,我需要它们从 roundImageView 反弹,就像这个问题一样。 Inside the the customContainerView I
override collisionBoundsType... return.ellipse
but the collision is still occurs from the square and not the circle, and the views are overlapping each other.在 customContainerView 内部,我
override collisionBoundsType... return.ellipse
但碰撞仍然是从正方形而不是圆形发生的,并且视图相互重叠。
customView:自定义视图:
class CustomContainerView: UIView {
override public var collisionBoundsType: UIDynamicItemCollisionBoundsType {
return .ellipse
}
}
code:代码:
var arr = [CustomContainerView]()
var animator: UIDynamicAnimator!
var gravity: UIGravityBehavior!
var collider: UICollisionBehavior!
var bouncingBehavior : UIDynamicItemBehavior!
override func viewDidLoad() {
super.viewDidLoad()
addSubViews()
addAnimatorAndBehaviors()
}
func addAnimatorAndBehaviors() {
animator = UIDynamicAnimator(referenceView: self.view)
gravity = UIGravityBehavior(items: arr)
animator.addBehavior(gravity)
collider = UICollisionBehavior(items: arr)
collider.translatesReferenceBoundsIntoBoundary = true
animator.addBehavior(collider)
bouncingBehavior = UIDynamicItemBehavior(items: arr)
bouncingBehavior.elasticity = 0.05
animator.addBehavior(bouncingBehavior)
}
func addSubViews() {
let redView = createContainerView(with: .red)
let blueView = createContainerView(with: .blue)
let yellowView = createContainerView(with: .yellow)
let purpleView = createContainerView(with: .purple)
let greenView = createContainerView(with: .green)
view.addSubview(redView)
view.addSubview(blueView)
view.addSubview(yellowView)
view.addSubview(purpleView)
view.addSubview(greenView)
arr = [redView, blueView, yellowView, purpleView, greenView]
}
func createContainerView(with color: UIColor) -> UIView {
let containerView = CustomContainerView()
containerView.backgroundColor = .brown
let size = CGSize(width: 50, height: 50)
containerView.frame.size = size
containerView.center = view.center
let roundImageView = UIImageView()
roundImageView.translatesAutoresizingMaskIntoConstraints = false
roundImageView.backgroundColor = color
containerView.addSubview(roundImageView)
roundImageView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 10).isActive = true
roundImageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 10).isActive = true
roundImageView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -10).isActive = true
roundImageView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -10).isActive = true
roundImageView.layer.masksToBounds = true
roundImageView.layoutIfNeeded()
roundImageView.layer.cornerRadius = roundImageView.frame.height / 2
roundImageView.layer.borderWidth = 1
roundImageView.layer.borderColor = UIColor.white.cgColor
return containerView
}
Looks like collision behavior doesn't like .ellipse
type when the views are positioned exactly on top of each other.当视图完全位于彼此之上时,看起来碰撞行为不喜欢
.ellipse
类型。
Running your code a few times gives different results (as expected)... sometimes, all 5 views end up in a full vertical stack, other times it ends up with some overlap, and other times (after waiting a few seconds) the views settle with a couple visible and the others way below the bottom of the view - I've seen their y-positions get to > 40,000.运行您的代码几次会产生不同的结果(如预期的那样)......有时,所有 5 个视图最终都在一个完整的垂直堆栈中,有时它以一些重叠结束,而其他时候(等待几秒钟后)视图解决几个可见的问题,其他的在视图底部下方 - 我已经看到他们的 y 位置达到 > 40,000。
I made a few modifications to your code to see what's happening...我对你的代码做了一些修改,看看发生了什么......
I added more views and gave each one a shape layer showing the ellipse bounds.我添加了更多视图,并为每个视图提供了一个显示椭圆边界的形状层。
Then, instead of starting with them all at identical positions, I created a couple "rows" so it looks like this:然后,我没有从相同的位置开始,而是创建了几个“行”,所以它看起来像这样:
Then, on each tap, I reset the original positions and toggle the UIDynamicItemCollisionBoundsType
between ellipse and rectangle , and then call addAnimatorAndBehaviors()
again.然后,在每次点击时,我都会重置原始位置并在 ellipse 和 rectangle 之间切换
UIDynamicItemCollisionBoundsType
,然后再次调用addAnimatorAndBehaviors()
。
Here's how it looks on sample .ellipse
run:以下是它在示例
.ellipse
运行中的样子:
and on sample .rectangle
run:并在示例
.rectangle
上运行:
As we can see, the .ellipse
bounds are being used.正如我们所看到的,正在使用
.ellipse
边界。
Here's the code I used to play with this:这是我用来玩这个的代码:
class CustomContainerView: UIView {
var useEllipse: Bool = false
override public var collisionBoundsType: UIDynamicItemCollisionBoundsType {
return useEllipse ? .ellipse : .rectangle
}
}
class ViewController: UIViewController {
var arr = [CustomContainerView]()
var animator: UIDynamicAnimator!
var gravity: UIGravityBehavior!
var collider: UICollisionBehavior!
var bouncingBehavior : UIDynamicItemBehavior!
let infoLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
addSubViews()
// add info label
infoLabel.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(infoLabel)
infoLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
infoLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
// add a tap recognizer to start the Animator Behaviors
let t = UITapGestureRecognizer(target: self, action: #selector(gotTap(_:)))
view.addGestureRecognizer(t)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
positionViews()
}
func positionViews() -> Void {
// let's make rows of the views,
// instead of starting with them all on top of each other
// we'll do 3-views over 2-views
let w = arr[0].frame.width * 1.1
let h = arr[0].frame.height * 1.1
var x: CGFloat = 0
var y: CGFloat = 0
var idx: Int = 0
y = h
while idx < arr.count {
x = view.center.x - w
for _ in 1...3 {
if idx < arr.count {
arr[idx].center = CGPoint(x: x, y: y)
}
x += w
idx += 1
}
y += h
x = view.center.x - w * 0.5
for _ in 1...2 {
if idx < arr.count {
arr[idx].center = CGPoint(x: x, y: y)
}
x += w
idx += 1
}
y += h
}
}
@objc func gotTap(_ g: UIGestureRecognizer) -> Void {
positionViews()
arr.forEach { v in
v.useEllipse.toggle()
}
infoLabel.text = arr[0].useEllipse ? "Ellipse" : "Rectangle"
addAnimatorAndBehaviors()
}
func addAnimatorAndBehaviors() {
animator = UIDynamicAnimator(referenceView: self.view)
gravity = UIGravityBehavior(items: arr)
animator.addBehavior(gravity)
collider = UICollisionBehavior(items: arr)
collider.translatesReferenceBoundsIntoBoundary = true
animator.addBehavior(collider)
bouncingBehavior = UIDynamicItemBehavior(items: arr)
bouncingBehavior.elasticity = 0.05
animator.addBehavior(bouncingBehavior)
}
func addSubViews() {
let clrs: [UIColor] = [
.red, .green, .blue,
.purple, .orange,
.cyan, .yellow, .magenta,
.systemTeal, .systemGreen,
]
clrs.forEach { c in
let v = createContainerView(with: c)
view.addSubview(v)
arr.append(v)
}
}
func createContainerView(with color: UIColor) -> CustomContainerView {
let containerView = CustomContainerView()
containerView.backgroundColor = UIColor.brown.withAlphaComponent(0.2)
let size = CGSize(width: 50, height: 50)
containerView.frame.size = size
view.addSubview(containerView)
let roundImageView = UIImageView()
roundImageView.translatesAutoresizingMaskIntoConstraints = false
roundImageView.backgroundColor = color
containerView.addSubview(roundImageView)
roundImageView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 10).isActive = true
roundImageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 10).isActive = true
roundImageView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -10).isActive = true
roundImageView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -10).isActive = true
roundImageView.layer.masksToBounds = true
roundImageView.layoutIfNeeded()
roundImageView.layer.cornerRadius = roundImageView.frame.height / 2
roundImageView.layer.borderWidth = 1
roundImageView.layer.borderColor = UIColor.white.cgColor
// let's add a CAShapeLayer to show the ellipse bounds
let c = CAShapeLayer()
c.fillColor = UIColor.clear.cgColor
c.lineWidth = 1
c.strokeColor = UIColor.black.cgColor
c.path = UIBezierPath(ovalIn: CGRect(origin: .zero, size: size)).cgPath
containerView.layer.addSublayer(c)
return containerView
}
}
Edit编辑
Change the while
loop in positionViews()
to this... tap to reset and run the animation a number of times and see what happens when all the views start with the same frame:将
positionViews()
中的while
循环更改为此...点击以重置并多次运行 animation 并查看当所有视图以同一帧开始时会发生什么:
while idx < arr.count {
x = view.center.x - w
arr[idx].center = CGPoint(x: x, y: y)
idx += 1
}
Then, use this while loop, where we start the views at the same x-position, but increment the y-position for each view (just by 0.1
points):然后,使用这个 while 循环,我们从相同的 x 位置开始视图,但增加每个视图的 y 位置(仅增加
0.1
个点):
while idx < arr.count {
x = view.center.x - w
// increment the y position for each view -- just a tad
y += 0.1
arr[idx].center = CGPoint(x: x, y: y)
idx += 1
}
Another Edit另一个编辑
Worth noting, the fact that the ellipse collision bounds is round ( 1:1
ratio), also affects things.值得注意的是,椭圆碰撞边界是圆形的(
1:1
比例)这一事实也会影响事物。
If we change the size of the view frames just slightly , we get very different results.如果我们稍微改变视图框架的大小,我们会得到非常不同的结果。
Try it with:试试看:
let size = CGSize(width: 50.1, height: 50)
and start them all with the exact same center point:并以完全相同的中心点开始它们:
while idx < arr.count {
x = view.center.x - w
arr[idx].center = CGPoint(x: x, y: y)
idx += 1
}
and you'll see the views spread out immediately.您会立即看到视图散布开来。
One more Edit - to help visualize the differences再一次编辑-帮助可视化差异
Here's another example - this time, I've numbered the views and set a "every 1/10th second" timer to update a label with the current center of each view.这是另一个示例 - 这一次,我为视图编号并设置“每 1/10 秒”计时器以使用每个视图的当前中心更新 label。
Also added segmented controls to select collisionBoundsType
and overlaying the views exactly on top of each other or offsetting them slightly:还向 select
collisionBoundsType
添加了分段控件,并将视图完全重叠或稍微偏移:
class CustomContainerView: UIView {
var useEllipse: Bool = false
override public var collisionBoundsType: UIDynamicItemCollisionBoundsType {
return useEllipse ? .ellipse : .rectangle
}
}
// extension to left-pad a string up-to length
extension RangeReplaceableCollection where Self: StringProtocol {
func paddingToLeft(upTo length: Int, using element: Element = " ") -> SubSequence {
return repeatElement(element, count: Swift.max(0, length-count)) + suffix(Swift.max(count, count-length))
}
}
class CollisionVC: UIViewController {
var arr = [CustomContainerView]()
var animator: UIDynamicAnimator!
var gravity: UIGravityBehavior!
var collider: UICollisionBehavior!
var bouncingBehavior: UIDynamicItemBehavior!
let infoLabel = UILabel()
// add segmented controls for collisionBoundsType and "Spread Layout"
let seg1 = UISegmentedControl(items: ["Ellipse", "Rectangle"])
let seg2 = UISegmentedControl(items: ["Overlaid", "Offset"])
override func viewDidLoad() {
super.viewDidLoad()
addSubViews()
[seg1, seg2, infoLabel].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
infoLabel.numberOfLines = 0
infoLabel.font = .monospacedSystemFont(ofSize: 14.0, weight: .light)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
seg1.topAnchor.constraint(equalTo: g.topAnchor, constant: 4.0),
seg1.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0),
seg2.topAnchor.constraint(equalTo: g.topAnchor, constant: 4.0),
seg2.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0),
infoLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
infoLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
infoLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
])
seg1.selectedSegmentIndex = 0
seg2.selectedSegmentIndex = 0
// add a tap recognizer to start the Animator Behaviors
let t = UITapGestureRecognizer(target: self, action: #selector(gotTap(_:)))
view.addGestureRecognizer(t)
// run a Timer... every 1/10th second we'll fill the infoLabel with
// collisionBoundsType and a list of center points
// for all subviews
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
if self.animator != nil {
var s = ""
for i in 0..<self.arr.count {
let c = self.arr[i].center
let xs = String(format: "%0.2f", c.x)
let ys = String(format: "%0.2f", c.y)
s += "\n\(i) - x: \(String(xs.paddingToLeft(upTo: 7))) y: \(String(ys.paddingToLeft(upTo: 9)))"
}
s += "\nAnimator is running: " + (self.animator.isRunning ? "Yes" : "No")
self.infoLabel.text = s
}
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
positionViews()
}
func positionViews() -> Void {
var x: CGFloat = 0.0
var y: CGFloat = 0.0
arr.forEach { v in
v.center = CGPoint(x: view.center.x + x, y: view.safeAreaInsets.top + 100.0 + y)
// if seg2 == Overlaid, position all views exactly on top of each other
// else, Offset the x,y center of each one by 0.1 pts
// Offsetting them allows the animator to use
// "valid" collision adjustments on start
if seg2.selectedSegmentIndex == 1 {
x += 0.1
y += 0.1
}
// set collisionBoundsType
v.useEllipse = seg1.selectedSegmentIndex == 0
}
}
@objc func gotTap(_ g: UIGestureRecognizer) -> Void {
positionViews()
addAnimatorAndBehaviors()
}
func addAnimatorAndBehaviors() {
animator = UIDynamicAnimator(referenceView: self.view)
gravity = UIGravityBehavior(items: arr)
animator.addBehavior(gravity)
collider = UICollisionBehavior(items: arr)
collider.translatesReferenceBoundsIntoBoundary = true
animator.addBehavior(collider)
bouncingBehavior = UIDynamicItemBehavior(items: arr)
bouncingBehavior.elasticity = 0.05
animator.addBehavior(bouncingBehavior)
}
func addSubViews() {
let clrs: [UIColor] = [
.red, .green, UIColor(red: 1.0, green: 0.85, blue: 0.55, alpha: 1.0),
UIColor(red: 1.0, green: 0.5, blue: 1.0, alpha: 1.0), .orange,
.cyan, .yellow, .magenta,
.systemTeal, .systemGreen,
]
for (c, i) in zip(clrs, (0..<clrs.count)) {
let v = createContainerView(with: c, number: i)
view.addSubview(v)
arr.append(v)
}
}
func createContainerView(with color: UIColor, number: Int) -> CustomContainerView {
let containerView = CustomContainerView()
containerView.backgroundColor = UIColor.brown.withAlphaComponent(0.2)
let size = CGSize(width: 50, height: 50)
containerView.frame.size = size
view.addSubview(containerView)
let roundLabel = UILabel()
roundLabel.translatesAutoresizingMaskIntoConstraints = false
roundLabel.backgroundColor = color
roundLabel.text = "\(number)"
roundLabel.textAlignment = .center
containerView.addSubview(roundLabel)
roundLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 10).isActive = true
roundLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 10).isActive = true
roundLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -10).isActive = true
roundLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -10).isActive = true
roundLabel.layer.masksToBounds = true
roundLabel.layoutIfNeeded()
roundLabel.layer.cornerRadius = roundLabel.frame.height / 2
roundLabel.layer.borderWidth = 1
roundLabel.layer.borderColor = UIColor.white.cgColor
// let's add a CAShapeLayer to show the ellipse bounds
let c = CAShapeLayer()
c.fillColor = UIColor.clear.cgColor
c.lineWidth = 1
c.strokeColor = UIColor.black.cgColor
c.path = UIBezierPath(ovalIn: CGRect(origin: .zero, size: size)).cgPath
containerView.layer.addSublayer(c)
return containerView
}
}
Worth noting: when the collisionBoundsType ==.ellipse
and the views start exactly on top of each other, the collision algorithm can (and usually does) end up pushing a couple views off the bottom, which puts them outside the reference system's bounds .值得注意的是:当
collisionBoundsType ==.ellipse
和视图完全在彼此之上开始时,碰撞算法可以(并且通常确实)最终将几个视图推离底部,这将它们置于参考系统的边界之外。 At that point, the algorithm continues trying to collide those views, pushing them further and further down o the Y axis.那时,算法会继续尝试碰撞这些视图,将它们在 Y 轴上越来越远。
Here is the output after letting it run for a few seconds:这是 output 运行几秒钟后:
Views 5, 7 and 8 are way out of bounds, and the animator is still running.视图 5、7 和 8 超出范围,动画师仍在运行。 Those views will continue to be pushed further and further down, presumably until we get an invalid point crash (I haven't let it run long enough to find out).
这些观点将继续被推得越来越低,大概直到我们得到一个无效点崩溃(我没有让它运行足够长的时间来发现)。
Also, because the animator ends up doing so much processing on those out-of-bounds views, the collision detection on the remaining views suffers.此外,由于动画师最终对这些越界视图进行了如此多的处理,因此对剩余视图的碰撞检测会受到影响。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.