实现 UIKitDynamics 以将视图拖出屏幕
Posted
技术标签:
【中文标题】实现 UIKitDynamics 以将视图拖出屏幕【英文标题】:Implement UIKitDynamics for dragging view off screen 【发布时间】:2014-01-24 05:23:36 【问题描述】:我正在尝试实现类似于 Jelly 应用中的 UIKit Dynamics(特别是向下滑动以将视图拖出屏幕)。
看动画:http://vimeo.com/83478484 (@1:17)
我了解 UIKit Dynamics 的工作原理,但没有很好的物理背景,因此无法组合不同的行为以获得所需的结果!
【问题讨论】:
【参考方案1】:这种拖动可以通过UIAttachmentBehavior
来完成,您可以在UIGestureRecognizerStateBegan
上创建附件行为,在UIGestureRecognizerStateChanged
上更改锚点。这实现了在用户进行平移手势时进行旋转拖动。
在UIGestureRecognizerStateEnded
上,您可以删除UIAttachmentBehavior
,然后应用UIDynamicItemBehavior
以使动画以用户放开它时拖动它时的相同线性和角速度无缝继续(不要忘记使用action
块来确定视图何时不再与父视图相交,因此您可以删除动态行为,也可能删除视图)。或者,如果您的逻辑确定要将其返回到原始位置,您可以使用 UISnapBehavior
来执行此操作。
坦率地说,根据这个短片,很难准确地确定他们在做什么,但这些是基本的构建块。
例如,假设您创建了一些要拖出屏幕的视图:
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://***.com/a/2051861/1271826
return atan2(view.transform.b, view.transform.a);
这会产生(如果您不向下拖动,则会显示捕捉行为,如果您成功将其向下拖动,则会显示动态行为):
这只是一个演示的外壳,但它说明了在平移手势期间使用UIAttachmentBehavior
,如果您想将其快速返回,如果您断定要反转手势的动画,则使用UISnapBehavior
,但使用UIDynamicItemBehavior
完成将其向下拖动的动画,离开屏幕,但使从 UIAttachmentBehavior
到最终动画的过渡尽可能平滑。我还在最后一个UIDynamicItemBehavior
的同时添加了一点重力,以便它平稳地加速离开屏幕(所以它不会花费太长时间)。
根据您的需要进行自定义。值得注意的是,平移手势处理程序非常笨拙,我可能会考虑创建一个自定义识别器来清理该代码。但希望这能说明使用 UIKit Dynamics 将视图拖离屏幕底部的基本概念。
【讨论】:
这太棒了!非常感谢:) 感谢您的解决方案,这正是我所需要的。但它包含保留循环,当您将 self 传递到块中时。要修复它,只需传递对 self 的弱引用:__weak typeof(self) wself = self;
好收获。谢谢。固定。
在我的测试中,大约 10 次中,附件动作块计算的角速度为 0。然而,这不应该是这种情况,因为我的平移手势识别器在平移结束时具有速度(速度 = 1158.742554)并且我从应该产生旋转的一侧拖动(你必须相信我的话那个)。 我认为应该有一种方法可以根据手势识别器的最后一个触摸点、手势识别器的速度向量和视图的中心点来计算角速度。【参考方案2】:
@Rob 的回答很棒(点赞!),但我会删除手动角速度计算,让 UIDynamics 使用UIPushBehavior
完成工作。只需设置UIPushBehavior
的目标偏移量,UIDynamics 就会为您完成旋转计算工作。
从@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];
【讨论】:
【参考方案3】: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)
【讨论】:
以上是关于实现 UIKitDynamics 以将视图拖出屏幕的主要内容,如果未能解决你的问题,请参考以下文章
为啥我无法检测到我使用 UIKit Dynamics 推离屏幕的 UIView 何时不再可见?