Implement UIKitDynamics for dragging view off screen

后端 未结 3 1295
感动是毒
感动是毒 2020-12-22 16:36

I\'m trying to figure out implement UIKit Dynamics that are similar to those in Jelly\'s app (specifically swiping down to drag view off-screen).

See the animation:

相关标签:
3条回答
  • 2020-12-22 17:01

    This sort of dragging can be accomplished with an UIAttachmentBehavior where you create the attachment behavior upon UIGestureRecognizerStateBegan, change the anchor upon UIGestureRecognizerStateChanged. This achieves the dragging with rotation as the user conducts the pan gesture.

    Upon UIGestureRecognizerStateEnded you can remove the UIAttachmentBehavior, but then apply a UIDynamicItemBehavior to have the animation seamlessly continue with the same linear and angular velocities the user was dragging it when they let go of it (don't forget to use an action block to determine when the view no longer intersects the superview, so you can remove the dynamic behavior and probably the view, too). Or, if your logic determines that you want to return it back to the original location, you can use a UISnapBehavior to do so.

    Frankly, on the basis of this short clip, it's a little tough to determine precisely what they're doing, but these are the basic building blocks.


    For example, let's assume you create some view you want to drag off the screen:

    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];
    

    You can then create a gesture recognizer to drag it off the screen:

    - (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);
    }
    

    That yields (showing both the snap behavior if you don't drag down, as well as the dynamic behavior if you successfully drag it down):

    UIDynamics demo

    This is only a shell of a demonstration, but it illustrates using a UIAttachmentBehavior during the pan gesture, using a UISnapBehavior if you want to snap it back if you conclude you want to reverse the gesture's animation, but using UIDynamicItemBehavior to finish the animation of dragging it down, off the screen, but making the transition from the the UIAttachmentBehavior to the final animation as smooth as possible. I also added a little gravity at the same time as that final UIDynamicItemBehavior so that it smoothly accelerates off the screen (so it doesn't take too long).

    Customize this as you see fit. Notably, that pan gesture handler is unwieldy enough that I might contemplate creating a custom recognizer to clean up that code. But hopefully this illustrates the basic concepts in using UIKit Dynamics to drag a view off the bottom of the screen.

    0 讨论(0)
  • 2020-12-22 17:05

    @Rob's answer is great (upvote it!), but I'd remove the manual angular velocity calculations and let UIDynamics do the work with a UIPushBehavior. Just set the target offset of the UIPushBehavior and UIDynamics will do the rotational calculation work for you.

    Start with @Rob's same setup:

    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];
    

    But tweak the gesture recognizer handler to use a 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];
        }
    }
    
    0 讨论(0)
  • 2020-12-22 17:22

    SWIFT 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)
    }
    }
    
    0 讨论(0)
提交回复
热议问题