如何解决方形父视图中圆形子视图之间的 UICollisionBehavior 检测
我有一个方形的 containerView,里面有一个 roundImageView。容器视图被添加到 UIDynamicAnimator。当 containerViews 的角相互碰撞时,我需要它们从 roundImageView 反弹,与此 question 相同。在 customContainerView I 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。
我对您的代码进行了一些修改以查看发生了什么...
我添加了更多视图,并为每个视图提供了一个显示椭圆边界的形状层。
然后,我没有从相同的位置开始,而是创建了几个“行”,看起来像这样:
然后,在每次点击时,我重置原始位置并在椭圆和矩形之间切换 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
}
}
编辑
将 while
中的 positionViews()
循环更改为...点击以重置并多次运行动画,看看当所有视图都以同一帧开始时会发生什么:
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 秒”计时器以使用每个视图的当前中心更新标签。
还添加了分段控件以选择 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,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,UIColor(red: 1.0,green: 0.85,blue: 0.55,alpha: 1.0),green: 0.5,blue: 1.0,]
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 轴上越往下推。
这是让它运行几秒钟后的输出:
视图 5、7 和 8 方式越界,动画师仍在运行。这些视图将继续被推得越来越低,大概直到我们遇到无效点崩溃(我还没有让它运行足够长的时间来找出答案)。
此外,由于动画师最终会对这些越界视图进行大量处理,因此其余视图的碰撞检测会受到影响。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。