简体   繁体   English

方形父视图中圆形子视图之间的 UICollisionBehavior 检测

[英]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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM