【问题标题】:UICollisionBehavior detection between round subViews in square parentViews方形父视图中圆形子视图之间的 UICollisionBehavior 检测
【发布时间】:2021-04-09 04:48:09
【问题描述】:

我有一个方形 containerView,里面有一个 roundImageView。 containerView 被添加到 UIDynamicAnimator。当 containerViews 的角相互碰撞时,我需要它们从 roundImageView 反弹,就像 question 一样。在 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
}

【问题讨论】:

    标签: ios swift collision-detection uidynamicanimator uicollisionbehavior


    【解决方案1】:

    看起来碰撞行为不像.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
        }
        
    }
    

    编辑

    positionViews() 中的while 循环更改为此...点击重置并多次运行动画,看看当所有视图从同一帧开始时会发生什么:

            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 ratio)这一事实也会影响事物。

    如果我们稍微改变一下视图框架的大小,我们会得到非常不同的结果。

    试试看:

    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, 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 轴上越来越远的位置。

    这是让它运行几秒钟后的输出:

    视图 5、7 和 8 方式超出范围,动画师仍在运行。这些视图将继续被推得越来越低,大概直到我们得到一个无效点崩溃(我没有让它运行足够长的时间来发现)。

    此外,由于动画师最终对这些越界视图进行了大量处理,因此对剩余视图的碰撞检测会受到影响。

    【讨论】:

    • 嘿伙计,怎么了,再次感谢您的帮助:)。这是我在创建 roundImageView 时缺少的 c.path = UIBezierPath。我没有在问题中添加它,因为我希望 UIBezierPath 自行将 imageView 舍入,所以我注释掉了 roundImageView.layer.cornerRadius = ... 并且它一直显示为正方形,所以我放弃了。我看到您添加了一个 UIBezierPath 以便 ContainerView 内的 override public var collisionBoundsType ... 可以识别它。无论如何感谢您的帮助!!!
    • 结果非常好。感谢您添加了一种同时使用.ellipse.rectangle 的方法。真的很好!
    • “这是我在创建 roundImageView 时缺少的 c.path = UIBezierPath。” -- .ellipse 设置在没有额外的形状层的情况下有效。我只是添加到以便更容易看到“碰撞点”。
    • 这很奇怪,我想知道为什么没有那个它对我不起作用。除了你定位视图和点击的方式之外,为什么它不能像我最初的方式那样工作?
    • 您知道可能是什么问题,您将视图定位在viewDidAppear,然后点击以创建动画效果。我在viewDidLoad 中做了所有事情。我记得在 Ray Wenderlich 的书中读到,所有动画都需要出现在 viewDidAppear 中,因为其他两个太早了。
    猜你喜欢
    • 2021-03-17
    • 1970-01-01
    • 2019-03-12
    • 1970-01-01
    • 2019-11-24
    • 1970-01-01
    • 2012-05-20
    相关资源
    最近更新 更多