28

我试图弄清楚实现类似于果冻应用程序中的 UIKit 动力学(特别是向下滑动以将视图拖出屏幕)。

看动画: http: //vimeo.com/83478484 (@1:17)

我了解 UIKit Dynamics 的工作原理,但没有很好的物理背景,因此无法组合不同的行为以获得所需的结果!

4

3 回答 3

87

这种拖动可以通过在UIAttachmentBehavior其中创建附件行为UIGestureRecognizerStateBegan、更改锚点来完成UIGestureRecognizerStateChanged。当用户进行平移手势时,这实现了旋转拖动。

UIGestureRecognizerStateEnded可以删除UIAttachmentBehavior,然后应用 aUIDynamicItemBehavior以使动画以用户放开它时拖动它时的相同线性和角速度无缝继续(不要忘记使用action块来确定视图何时不再与超级视图相交,因此您可以删除动态行为,也可能删除视图)。或者,如果您的逻辑确定要将其返回到原始位置,则可以使用 aUISnapBehavior来执行此操作。

坦率地说,根据这个短片,很难准确地确定他们在做什么,但这些是基本的构建块。


例如,假设您创建了一些要拖出屏幕的视图:

UIView *viewToDrag = [[UIView alloc] initWithFrame:...];
viewToDrag.backgroundColor = [UIColor lightGrayColor];
[self.view addSubview:viewToDrag];

UIGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
[viewToDrag addGestureRecognizer:pan];

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

然后,您可以创建一个手势识别器将其拖出屏幕:

- (void)handlePan:(UIPanGestureRecognizer *)gesture {
    static UIAttachmentBehavior *attachment;
    static CGPoint               startCenter;

    // variables for calculating angular velocity

    static CFAbsoluteTime        lastTime;
    static CGFloat               lastAngle;
    static CGFloat               angularVelocity;

    if (gesture.state == UIGestureRecognizerStateBegan) {
        [self.animator removeAllBehaviors];

        startCenter = gesture.view.center;

        // calculate the center offset and anchor point

        CGPoint pointWithinAnimatedView = [gesture locationInView:gesture.view];

        UIOffset offset = UIOffsetMake(pointWithinAnimatedView.x - gesture.view.bounds.size.width / 2.0,
                                       pointWithinAnimatedView.y - gesture.view.bounds.size.height / 2.0);

        CGPoint anchor = [gesture locationInView:gesture.view.superview];

        // create attachment behavior

        attachment = [[UIAttachmentBehavior alloc] initWithItem:gesture.view
                                               offsetFromCenter:offset
                                               attachedToAnchor:anchor];

        // code to calculate angular velocity (seems curious that I have to calculate this myself, but I can if I have to)

        lastTime = CFAbsoluteTimeGetCurrent();
        lastAngle = [self angleOfView:gesture.view];

        typeof(self) __weak weakSelf = self;

        attachment.action = ^{
            CFAbsoluteTime time = CFAbsoluteTimeGetCurrent();
            CGFloat angle = [weakSelf angleOfView:gesture.view];
            if (time > lastTime) {
                angularVelocity = (angle - lastAngle) / (time - lastTime);
                lastTime = time;
                lastAngle = angle;
            }
        };

        // add attachment behavior

        [self.animator addBehavior:attachment];
    } else if (gesture.state == UIGestureRecognizerStateChanged) {
        // as user makes gesture, update attachment behavior's anchor point, achieving drag 'n' rotate

        CGPoint anchor = [gesture locationInView:gesture.view.superview];
        attachment.anchorPoint = anchor;
    } else if (gesture.state == UIGestureRecognizerStateEnded) {
        [self.animator removeAllBehaviors];

        CGPoint velocity = [gesture velocityInView:gesture.view.superview];

        // if we aren't dragging it down, just snap it back and quit

        if (fabs(atan2(velocity.y, velocity.x) - M_PI_2) > M_PI_4) {
            UISnapBehavior *snap = [[UISnapBehavior alloc] initWithItem:gesture.view snapToPoint:startCenter];
            [self.animator addBehavior:snap];

            return;
        }

        // otherwise, create UIDynamicItemBehavior that carries on animation from where the gesture left off (notably linear and angular velocity)

        UIDynamicItemBehavior *dynamic = [[UIDynamicItemBehavior alloc] initWithItems:@[gesture.view]];
        [dynamic addLinearVelocity:velocity forItem:gesture.view];
        [dynamic addAngularVelocity:angularVelocity forItem:gesture.view];
        [dynamic setAngularResistance:1.25];

        // when the view no longer intersects with its superview, go ahead and remove it

        typeof(self) __weak weakSelf = self;

        dynamic.action = ^{
            if (!CGRectIntersectsRect(gesture.view.superview.bounds, gesture.view.frame)) {
                [weakSelf.animator removeAllBehaviors];
                [gesture.view removeFromSuperview];

                [[[UIAlertView alloc] initWithTitle:nil message:@"View is gone!" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
            }
        };
        [self.animator addBehavior:dynamic];

        // add a little gravity so it accelerates off the screen (in case user gesture was slow)

        UIGravityBehavior *gravity = [[UIGravityBehavior alloc] initWithItems:@[gesture.view]];
        gravity.magnitude = 0.7;
        [self.animator addBehavior:gravity];
    }
}

- (CGFloat)angleOfView:(UIView *)view
{
    // http://stackoverflow.com/a/2051861/1271826

    return atan2(view.transform.b, view.transform.a);
}

这会产生(如果您不向下拖动,则会显示捕捉行为,如果您成功将其向下拖动,则会显示动态行为):

UIDynamics 演示

这只是一个演示的外壳,但它说明了UIAttachmentBehavior在平移手势期间使用 a ,UISnapBehavior如果您确定要反转手势的动画,则使用 a 如果您想将其捕捉回来,但使用UIDynamicItemBehavior完成向下拖动它的动画,离开屏幕,但使从 theUIAttachmentBehavior到最终动画的过渡尽可能平滑。我还在决赛的同时添加了一点重力,UIDynamicItemBehavior以便它平稳地加速离开屏幕(所以它不会花费太长时间)。

按照您认为合适的方式自定义它。值得注意的是,该平移手势处理程序非常笨拙,我可能会考虑创建一个自定义识别器来清理该代码。但希望这能说明使用 UIKit Dynamics 将视图拖离屏幕底部的基本概念。

于 2014-01-25T04:56:40.553 回答
5

@Rob 的答案很棒(赞成!),但我会删除手动角速度计算并让 UIDynamics 使用UIPushBehavior. 只需设置目标偏移量,UIPushBehaviorUIDynamics 就会为您完成旋转计算工作。

从@Rob 的相同设置开始:

UIView *viewToDrag = [[UIView alloc] initWithFrame:...];
viewToDrag.backgroundColor = [UIColor lightGrayColor];
[self.view addSubview:viewToDrag];

UIGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
[viewToDrag addGestureRecognizer:pan];

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

但是调整手势识别器处理程序以使用UIPushBehavior

- (void)handlePan:(UIPanGestureRecognizer *)gesture {
    static UIAttachmentBehavior *attachment;
    static CGPoint startCenter;

    if (gesture.state == UIGestureRecognizerStateBegan) {
        [self.animator removeAllBehaviors];

        startCenter = gesture.view.center;

        // calculate the center offset and anchor point

        CGPoint pointWithinAnimatedView = [gesture locationInView:gesture.view];

        UIOffset offset = UIOffsetMake(pointWithinAnimatedView.x - gesture.view.bounds.size.width / 2.0,
                                       pointWithinAnimatedView.y - gesture.view.bounds.size.height / 2.0);

        CGPoint anchor = [gesture locationInView:gesture.view.superview];

        // create attachment behavior

        attachment = [[UIAttachmentBehavior alloc] initWithItem:gesture.view
                                               offsetFromCenter:offset
                                               attachedToAnchor:anchor];

        // add attachment behavior

        [self.animator addBehavior:attachment];
    } else if (gesture.state == UIGestureRecognizerStateChanged) {
        // as user makes gesture, update attachment behavior's anchor point, achieving drag 'n' rotate

        CGPoint anchor = [gesture locationInView:gesture.view.superview];
        attachment.anchorPoint = anchor;
    } else if (gesture.state == UIGestureRecognizerStateEnded) {
        [self.animator removeAllBehaviors];

        CGPoint velocity = [gesture velocityInView:gesture.view.superview];

        // if we aren't dragging it down, just snap it back and quit

        if (fabs(atan2(velocity.y, velocity.x) - M_PI_2) > M_PI_4) {
            UISnapBehavior *snap = [[UISnapBehavior alloc] initWithItem:gesture.view snapToPoint:startCenter];
            [self.animator addBehavior:snap];

            return;
        }

        // otherwise, create UIPushBehavior that carries on animation from where the gesture left off

        CGFloat velocityMagnitude = sqrtf((velocity.x * velocity.x) + (velocity.y * velocity.y));
        UIPushBehavior *pushBehavior = [[UIPushBehavior alloc] initWithItems:@[gesture.view] mode:UIPushBehaviorModeInstantaneous];
        pushBehavior.pushDirection = CGVectorMake((velocity.x / 10) , (velocity.y / 10));
        // some constant to limit the speed of the animation
        pushBehavior.magnitude = velocityMagnitude / 35.0;
        CGPoint finalPoint = [gesture locationInView:gesture.view.superview];
        CGPoint center = gesture.view.center;
        [pushBehavior setTargetOffsetFromCenter:UIOffsetMake(finalPoint.x - center.x, finalPoint.y - center.y) forItem:gesture.view];

        // when the view no longer intersects with its superview, go ahead and remove it

        typeof(self) __weak weakSelf = self;

        pushBehavior.action = ^{
            if (!CGRectIntersectsRect(gesture.view.superview.bounds, gesture.view.frame)) {
                [weakSelf.animator removeAllBehaviors];
                [gesture.view removeFromSuperview];

                [[[UIAlertView alloc] initWithTitle:nil message:@"View is gone!" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
            }
        };
        [self.animator addBehavior:pushBehavior];

        // add a little gravity so it accelerates off the screen (in case user gesture was slow)

        UIGravityBehavior *gravity = [[UIGravityBehavior alloc] initWithItems:@[gesture.view]];
        gravity.magnitude = 0.7;
        [self.animator addBehavior:gravity];
    }
}
于 2015-08-05T21:55:28.937 回答
4

斯威夫特 3.0:

import UIKit

class SwipeToDisMissView: UIView {

var animator : UIDynamicAnimator?

func initSwipeToDismissView(_ parentView:UIView)  {
    let panGesture = UIPanGestureRecognizer(target: self, action: #selector(SwipeToDisMissView.panGesture))
    self.addGestureRecognizer(panGesture)
    animator = UIDynamicAnimator(referenceView: parentView)
}

func panGesture(_ gesture:UIPanGestureRecognizer)  {
    var attachment : UIAttachmentBehavior?
    var lastTime = CFAbsoluteTime()
    var lastAngle: CGFloat = 0.0
    var angularVelocity: CGFloat = 0.0

    if gesture.state == .began {
        self.animator?.removeAllBehaviors()
        if let gestureView = gesture.view {
            let pointWithinAnimatedView = gesture.location(in: gestureView)
            let offset = UIOffsetMake(pointWithinAnimatedView.x - gestureView.bounds.size.width / 2.0, pointWithinAnimatedView.y - gestureView.bounds.size.height / 2.0)
            let anchor = gesture.location(in: gestureView.superview!)
            // create attachment behavior
            attachment = UIAttachmentBehavior(item: gestureView, offsetFromCenter: offset, attachedToAnchor: anchor)
            // code to calculate angular velocity (seems curious that I have to calculate this myself, but I can if I have to)
            lastTime = CFAbsoluteTimeGetCurrent()
            lastAngle = self.angleOf(gestureView)
            weak var weakSelf = self
            attachment?.action = {() -> Void in
                let time = CFAbsoluteTimeGetCurrent()
                let angle: CGFloat = weakSelf!.angleOf(gestureView)
                if time > lastTime {
                    angularVelocity = (angle - lastAngle) / CGFloat(time - lastTime)
                    lastTime = time
                    lastAngle = angle
                }
            }
            self.animator?.addBehavior(attachment!)
        }
    }
    else if gesture.state == .changed {
        if let gestureView = gesture.view {
            if let superView = gestureView.superview {
                let anchor = gesture.location(in: superView)
                if let attachment = attachment {
                    attachment.anchorPoint = anchor
                }
            }
        }
    }
    else if gesture.state == .ended {
        if let gestureView = gesture.view {
            let anchor = gesture.location(in: gestureView.superview!)
            attachment?.anchorPoint = anchor
            self.animator?.removeAllBehaviors()
            let velocity = gesture.velocity(in: gestureView.superview!)
            let dynamic = UIDynamicItemBehavior(items: [gestureView])
            dynamic.addLinearVelocity(velocity, for: gestureView)
            dynamic.addAngularVelocity(angularVelocity, for: gestureView)
            dynamic.angularResistance = 1.25
            // when the view no longer intersects with its superview, go ahead and remove it
            weak var weakSelf = self
            dynamic.action = {() -> Void in
                if !gestureView.superview!.bounds.intersects(gestureView.frame) {
                    weakSelf?.animator?.removeAllBehaviors()
                    gesture.view?.removeFromSuperview()
                }
            }
            self.animator?.addBehavior(dynamic)

            let gravity = UIGravityBehavior(items: [gestureView])
            gravity.magnitude = 0.7
            self.animator?.addBehavior(gravity)
        }
    }
}

func angleOf(_ view: UIView) -> CGFloat {
    return atan2(view.transform.b, view.transform.a)
}
}
于 2016-12-22T12:06:58.737 回答