Jellyのアプリと同様のUIKitDynamicsの実装を理解しようとしています(具体的には、下にスワイプしてビューを画面外にドラッグします)。
アニメーションを参照してください: http://vimeo.com/83478484 (@ 1:17)
UIKit Dynamicsがどのように機能するかは理解していますが、物理学のバックグラウンドがあまりないため、さまざまな動作を組み合わせて目的の結果を得るのに問題があります。
この種のドラッグは、UIAttachmentBehavior
でアタッチメント動作を作成し、UIGestureRecognizerStateBegan
でアンカーを変更するUIGestureRecognizerStateChanged
で実行できます。これにより、ユーザーがパンジェスチャを実行するときに、回転によるドラッグが実現します。
UIGestureRecognizerStateEnded
で、UIAttachmentBehavior
を削除できますが、次にUIDynamicItemBehavior
を適用して、アニメーションを同じ線形速度とangular速度)でシームレスに続行します。ユーザーが手放したときにドラッグしていました(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://stackoverflow.com/a/2051861/1271826
return atan2(view.transform.b, view.transform.a);
}
これにより、次のようになります(下にドラッグしない場合のスナップ動作と、正常に下にドラッグした場合の動的動作の両方が表示されます)。
これはデモンストレーションのシェルにすぎませんが、パンジェスチャ中にUIAttachmentBehavior
を使用し、ジェスチャのアニメーションを元に戻したいと結論付けた場合にスナップバックする場合はUISnapBehavior
を使用する方法を示しています。 、ただし、UIDynamicItemBehavior
を使用して、画面から下にドラッグするアニメーションを終了しますが、UIAttachmentBehavior
から最終的なアニメーションへの移行を可能な限りスムーズにします。また、最後のUIDynamicItemBehavior
と同時に少し重力を追加して、画面からスムーズに加速するようにしました(それほど時間はかかりません)。
必要に応じてこれをカスタマイズします。特に、そのパンジェスチャハンドラは扱いにくいため、そのコードをクリーンアップするためのカスタムレコグナイザーを作成することを検討するかもしれません。しかし、うまくいけば、これはUIKitDynamicsを使用してビューを画面の下部からドラッグする際の基本的な概念を示しています。
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)
}
}
@Robの答えは素晴らしいです(賛成です!)が、手動のangular速度計算を削除し、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];
}
}