[英]UICollisionBehavior detection between round subViews in square parentViews
我有一個方形的 containerView,里面有一個 roundImageView。 containerView 被添加到 UIDynamicAnimator。 當 containerViews 的角相互碰撞時,我需要它們從 roundImageView 反彈,就像這個問題一樣。 在 customContainerView 內部,我override collisionBoundsType... return.ellipse
但碰撞仍然是從正方形而不是圓形發生的,並且視圖相互重疊。
自定義視圖:
class CustomContainerView: UIView {
override public var collisionBoundsType: UIDynamicItemCollisionBoundsType {
return .ellipse
}
}
代碼:
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
}
當視圖完全位於彼此之上時,看起來碰撞行為不喜歡.ellipse
類型。
運行您的代碼幾次會產生不同的結果(如預期的那樣)......有時,所有 5 個視圖最終都在一個完整的垂直堆棧中,有時它以一些重疊結束,而其他時候(等待幾秒鍾后)視圖解決幾個可見的問題,其他的在視圖底部下方 - 我已經看到他們的 y 位置達到 > 40,000。
我對你的代碼做了一些修改,看看發生了什么......
我添加了更多視圖,並為每個視圖提供了一個顯示橢圓邊界的形狀層。
然后,我沒有從相同的位置開始,而是創建了幾個“行”,所以它看起來像這樣:
然后,在每次點擊時,我都會重置原始位置並在 ellipse 和 rectangle 之間切換UIDynamicItemCollisionBoundsType
,然后再次調用addAnimatorAndBehaviors()
。
以下是它在示例.ellipse
運行中的樣子:
並在示例.rectangle
上運行:
正如我們所看到的,正在使用.ellipse
邊界。
這是我用來玩這個的代碼:
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
}
}
編輯
將positionViews()
中的while
循環更改為此...點擊以重置並多次運行 animation 並查看當所有視圖以同一幀開始時會發生什么:
while idx < arr.count {
x = view.center.x - w
arr[idx].center = CGPoint(x: x, y: y)
idx += 1
}
然后,使用這個 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
}
另一個編輯
值得注意的是,橢圓碰撞邊界是圓形的( 1:1
比例)這一事實也會影響事物。
如果我們稍微改變視圖框架的大小,我們會得到非常不同的結果。
試試看:
let size = CGSize(width: 50.1, height: 50)
並以完全相同的中心點開始它們:
while idx < arr.count {
x = view.center.x - w
arr[idx].center = CGPoint(x: x, y: y)
idx += 1
}
您會立即看到視圖散布開來。
再一次編輯-幫助可視化差異
這是另一個示例 - 這一次,我為視圖編號並設置“每 1/10 秒”計時器以使用每個視圖的當前中心更新 label。
還向 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
}
}
值得注意的是:當collisionBoundsType ==.ellipse
和視圖完全在彼此之上開始時,碰撞算法可以(並且通常確實)最終將幾個視圖推離底部,這將它們置於參考系統的邊界之外。 那時,算法會繼續嘗試碰撞這些視圖,將它們在 Y 軸上越來越遠。
這是 output 運行幾秒鍾后:
視圖 5、7 和 8 超出范圍,動畫師仍在運行。 這些觀點將繼續被推得越來越低,大概直到我們得到一個無效點崩潰(我沒有讓它運行足夠長的時間來發現)。
此外,由於動畫師最終對這些越界視圖進行了如此多的處理,因此對剩余視圖的碰撞檢測會受到影響。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.