UIKit动力学:识别圆形形状和边界

3
我是一款使用UIKit Dynamics模拟不同圆形之间交互的应用程序开发者。
我使用以下代码创建我的圆形:
self = [super initWithFrame:CGRectMake(location.x - radius/2.0, location.y - radius/2, radius, radius)];
if (self) {
    [self.layer setCornerRadius: radius /2.0f];
    self.clipsToBounds = YES;
    self.layer.masksToBounds = YES;
    self.backgroundColor = color;
    self.userInteractionEnabled = NO;
}
return self;

其中,location表示所需圆的位置,radius表示其半径。

我会通过以下方式将这些圆添加到不同的UIBehaviours中:

[_collision addItem:circle];
[_gravity addItem:circle];
[_itemBehaviour addItem:circle];

项目行为被定义如下:
_itemBehaviour = [[UIDynamicItemBehavior alloc] initWithItems:@[square]];
_itemBehaviour.elasticity = 1;
_itemBehaviour.friction = 0;
_itemBehaviour.resistance = 0;
_itemBehaviour.angularResistance = 0;
_itemBehaviour.allowsRotation = NO;

我遇到的问题是我的圆圈行为像正方形。在特定方式被撞击时,它们会获得角动量并失去速度。如果再次碰撞,有时角动量会重新转化为速度。这对于正方形来说是正常的,但当视图是圆形时,就会显得奇怪和不自然。
通过打开一些调试选项,我制作了这张截图: small circle is actually a square 如您所见,此圆形明显是一个正方形。
因此,我的问题是,如何创建一个真正的圆形UIVIew,并在UIKit Dynamics中表现出相应的行为?

方块是截图中较大的正方形,只是另一个UIView。如果这是问题,我会非常惊讶。 - daniel f.
是的,我意识到了。你的q代码顺序颠倒了,所以我没有意识到你在添加圆形。不过,请看我的答案。 - Fogmeister
3
为了方便未来的读者,在iOS 9中,使用UIDynamicItem属性collisionBoundsTypecollisionBoundingPath现在是可能的。 - Rob
感谢@Rob,很高兴知道这个信息。未来的读者应该研究一下,在我写这篇帖子的时候,iOS7是当前版本,如果没记错的话。 - daniel f.
@Rob,你应该将其发布为答案,因为这是原始问题的实际正确解决方案。 - Iulian Onofrei
1
@IulianOnofrei 完成。 - Rob
2个回答

8
我知道这个问题早于iOS 9,但是为了未来的读者,现在你可以定义一个视图,并将collisionBoundsType设置为UIDynamicItemCollisionBoundsTypePath,并使用一个圆形的collisionBoundingPath。因此,虽然你不能“创建一个真正的圆形UIView”,但是你可以定义一个路径,该路径定义了在视图内呈现的形状以及动画器的碰撞边界,从而产生一个圆形视图的效果(尽管视图本身显然仍然是矩形的,因为所有视图都是矩形的):
@interface CircleView: UIView

@property (nonatomic) CGFloat lineWidth;
@property (nonatomic, strong) CAShapeLayer *shapeLayer;

@end

@implementation CircleView

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        [self configure];
    }
    return self;
}

- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        [self configure];
    }
    return self;
}

- (instancetype)init {
    return [self initWithFrame:CGRectZero];
}

- (void)configure {
    self.translatesAutoresizingMaskIntoConstraints = false;

    // create shape layer for circle

    self.shapeLayer = [CAShapeLayer layer];
    self.shapeLayer.strokeColor = [[UIColor blueColor] CGColor];
    self.shapeLayer.fillColor = [[[UIColor blueColor] colorWithAlphaComponent:0.5] CGColor];
    self.lineWidth = 3;
    [self.layer addSublayer:self.shapeLayer];
}

- (void)layoutSubviews {
    [super layoutSubviews];

    // path of shape layer is with respect to center of the `bounds`

    CGPoint center = CGPointMake(self.bounds.origin.x + self.bounds.size.width / 2, self.bounds.origin.y + self.bounds.size.height / 2);
    self.shapeLayer.path = [[self circularPathWithLineWidth:self.lineWidth center:center] CGPath];
}

- (UIDynamicItemCollisionBoundsType)collisionBoundsType {
    return UIDynamicItemCollisionBoundsTypePath;
}

- (UIBezierPath *)collisionBoundingPath {
    // path of collision bounding path is with respect to center of the dynamic item, so center of this path will be CGPointZero

    return [self circularPathWithLineWidth:0 center:CGPointZero];
}

- (UIBezierPath *)circularPathWithLineWidth:(CGFloat)lineWidth center:(CGPoint)center {
    CGFloat radius = (MIN(self.bounds.size.width, self.bounds.size.height) - self.lineWidth) / 2;
    return [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:0 endAngle:M_PI * 2 clockwise:true];
}

@end

然后,当您进行碰撞时,它将尊重collisionBoundingPath的值:

self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];

// create circle views

CircleView *circle1 = [[CircleView alloc] initWithFrame:CGRectMake(60, 100, 80, 80)];
[self.view addSubview:circle1];

CircleView *circle2 = [[CircleView alloc] initWithFrame:CGRectMake(250, 150, 120, 120)];
[self.view addSubview:circle2];

// have them collide with each other

UICollisionBehavior *collision = [[UICollisionBehavior alloc] initWithItems:@[circle1, circle2]];
[self.animator addBehavior:collision];

// with perfect elasticity

UIDynamicItemBehavior *behavior = [[UIDynamicItemBehavior alloc] initWithItems:@[circle1, circle2]];
behavior.elasticity = 1;
[self.animator addBehavior:behavior];

// and push one of the circles

UIPushBehavior *push = [[UIPushBehavior alloc] initWithItems:@[circle1] mode:UIPushBehaviorModeInstantaneous];
[push setAngle:0 magnitude:1];

[self.animator addBehavior:push];

这将产生:

enter image description here

顺便提一下,文档概述了路径的一些限制:

您创建的路径对象必须表示具有逆时针或顺时针绕组的凸多边形,并且路径不能与自身相交。路径的(0, 0)点必须位于相应动态项的中心点处。如果中心点与路径的原点不匹配,则碰撞行为可能无法按预期工作。

但是一个简单的圆形路径很容易满足这些标准。


或者,对于Swift用户:

class CircleView: UIView {
    var lineWidth: CGFloat = 3

    var shapeLayer: CAShapeLayer = {
        let _shapeLayer = CAShapeLayer()
        _shapeLayer.strokeColor = UIColor.blue.cgColor
        _shapeLayer.fillColor = UIColor.blue.withAlphaComponent(0.5).cgColor
        return _shapeLayer
    }()

    override func layoutSubviews() {
        super.layoutSubviews()

        layer.addSublayer(shapeLayer)
        shapeLayer.lineWidth = lineWidth
        let center = CGPoint(x: bounds.midX, y: bounds.midY)
        shapeLayer.path = circularPath(lineWidth: lineWidth, center: center).cgPath
    }

    private func circularPath(lineWidth: CGFloat = 0, center: CGPoint = .zero) -> UIBezierPath {
        let radius = (min(bounds.width, bounds.height) - lineWidth) / 2
        return UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: .pi * 2, clockwise: true)
    }

    override var collisionBoundsType: UIDynamicItemCollisionBoundsType { return .path }

    override var collisionBoundingPath: UIBezierPath { return circularPath() }
}

class ViewController: UIViewController {

    let animator = UIDynamicAnimator()

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        let circle1 = CircleView(frame: CGRect(x: 60, y: 100, width: 80, height: 80))
        view.addSubview(circle1)

        let circle2 = CircleView(frame: CGRect(x: 250, y: 150, width: 120, height: 120))
        view.addSubview(circle2)

        animator.addBehavior(UICollisionBehavior(items: [circle1, circle2]))

        let behavior = UIDynamicItemBehavior(items: [circle1, circle2])
        behavior.elasticity = 1
        animator.addBehavior(behavior)

        let push = UIPushBehavior(items: [circle1], mode: .instantaneous)
        push.setAngle(0, magnitude: 1)
        animator.addBehavior(push)
    }

}

1
现在应该是被接受的答案了。谢谢@Rob! - Tulleb

1

好的,首先。

你启用的调试选项显示了透明单元格的区域。看起来是圆形的视图实际上是一个带有圆角的正方形。

所有视图都是矩形的。它们呈现为圆形的方式是通过使角落透明(因此拥有圆角)。

其次,您想要使用UIKit Dynamics做什么?屏幕上显示的东西看起来像是您正在创建某种游戏。

Dynamics旨在用于更自然和真实的UI动画。它不是一个完整的物理引擎。

如果您想要类似的东西,那么最好使用Sprite Kit。


谢谢你的回答,它解决了我一些疑惑。我并不打算做什么复杂的事情,这只是为了完成作业而已,我想呈现出一些好看的东西。我认为我不会在比那更复杂的地方使用 uikit 动力学。你提到这个工具包是为了创建更好的动画效果而设计的,这很有道理,并解释了一些它的缺点。我之后会链接到精灵工具包中!感谢你的帮助。 - daniel f.
没问题。在一定的限制条件下,您可以通过动态效果获得一些非常好的效果。我建议您查看Ray Wenderlich网站上的Sprite Kit。在那里很容易实现高级物理效果。 - Fogmeister
1
此问题与“Sprite Kit”无关。 - Iulian Onofrei
1
@IulianOnofrei 这个问题已经三年了,我的回答也是。你的意思是什么?你让一个已经过时的问题复活了。是的,Rob可以把他的评论发表为答案,这可能会有好处。但这是否意味着我的回答是错误的?不是的。原帖作者试图使用UIKitDynamics来做一些它并不适合的事情。所以我建议他们最好使用SpriteKit。如果我试图用不理想的方式做某事,我也希望别人能给我同样的建议。也许原帖作者不知道SpriteKit?你有考虑过这个吗?还是你只是想无礼地回应? - Fogmeister
我认为你应该将你的评论作为编辑添加 @Fogmeister。人们可能不会在你的答案之后再看到它,因为你的答案已被接受,但他们实际上应该看到 Rob 的答案。 - Tulleb

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接