Реализация UIKitDynamics для перетаскивания вида с экрана
Я пытаюсь выяснить, как реализовать UIKit Dynamics, которые похожи на те, которые есть в приложении Jelly (в частности, проведя пальцем вниз, чтобы перетащить представление за пределы экрана).
Смотрите анимацию: http://vimeo.com/83478484 (@ 1: 17)
Я понимаю, как работает UIKit Dynamics, но у меня нет хороших физических знаний, и поэтому я испытываю затруднения, комбинируя различные варианты поведения, чтобы получить желаемый результат!
3 ответа
Этот вид перетаскивания может быть выполнен с 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://stackru.com/a/2051861/1271826
return atan2(view.transform.b, view.transform.a);
}
Это дает (показывает как поведение привязки, если вы не перетащите вниз, так и динамическое поведение, если вы успешно перетащите его вниз):
Это всего лишь оболочка демонстрации, но она иллюстрирует использование UIAttachmentBehavior
во время жеста панорамирования, используя UISnapBehavior
если вы хотите вернуть его обратно, если вы решили, что хотите изменить анимацию жеста, но с помощью UIDynamicItemBehavior
чтобы завершить анимацию перетаскивания его вниз с экрана, но переходя от UIAttachmentBehavior
чтобы финальная анимация была как можно более плавной. Я также добавил немного гравитации в то же время, что и финал UIDynamicItemBehavior
чтобы он плавно ускорялся за пределами экрана (чтобы это не занимало много времени).
Настройте это по своему усмотрению. Примечательно, что этот обработчик жестов панорамирования достаточно громоздкий, чтобы я мог подумать о создании пользовательского распознавателя для очистки этого кода. Но, надеюсь, это иллюстрирует основные концепции использования UIKit Dynamics для перетаскивания вида из нижней части экрана.
@ Ответ Роба отличный (обоснуйте его!), Но я бы удалил ручные вычисления угловой скорости и позволил UIDynamics сделать работу с UIPushBehavior
, Просто установите целевое смещение UIPushBehavior
и UIDynamics выполнит за вас ротационные вычисления.
Начните с той же настройки @ Роба:
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];
}
}
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)
}
}