Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UIDynamicAnimator refuses to reach equilibrium when a UIGravityBehavior is active

I'm working on code for an expandable tray view that uses UIDynamicAnimator to achieve a nice expand/contract animation.

To achieve a realistic acceleration I use UIGravityBehavior to make my tray fall, until the "tab" of the tray hits the bottom of the screen.

This works well, but even though all items in the scene have stopped moving, UIDynamicAnimatorDelegate dynamicAnimatorDidPause: is never called. This means that the animator continues using CPU cycles to animate the scene ( the delegate is set, and fires for UIDynamicAnimatorDelegate dynamicAnimatorDidPause: ).

I tried removing the UIGravityBehavior from the scene, which did indeed cause the animator to stop in the end. I can't time the removal of the gravity behavior right though, since I need to remove it from the scene once everything has stopped moving.

I understand that gravity is a constant force, but I still assumed it would stop the animator once everything has 0 velocity and 0 acceleration.

Is this last assumption false?

Anyone having similar problems?

like image 239
Nailer Avatar asked Oct 15 '13 15:10

Nailer


3 Answers

You are correct that the animator should pause once everything comes to rest.

Check what items are attached to your gravity behavior, and make sure that there aren't other items still falling. For example, it is easy to accidentally create the following bug:

  • Add a view to gravity and collision
  • Remove view from superview and from collision
  • Fail to remove view from gravity

In this situation, the "ghost item" will fall forever.

Another possible problem (though less likely given your description) is if your items are attached to other behaviors that are causing infinite but small "bounce." I would check the full list of behaviors on your animator (remember to check child behaviors, too). In particular I'd be interested in any UIDynamicItemBehavior that adds elasticity.


EDIT:

You may also want to go the other way. Start with a very basic dynamics system and add components from yours until you can reproduce the problem. For instance, the following does converge quite quickly (logging "pause"):

@interface PTLViewController () <UIDynamicAnimatorDelegate>
@property (nonatomic, strong) UIDynamicAnimator *animator;
@end

@implementation PTLViewController

- (void)viewDidLoad {
  [super viewDidLoad];

  UIView *view = [[UIView alloc] initWithFrame:CGRectMake(100,100,100,100)];
  view.backgroundColor = [UIColor lightGrayColor];
  [self.view addSubview:view];

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

  UICollisionBehavior *collisionBehavior = [[UICollisionBehavior alloc] initWithItems:@[view]];
  collisionBehavior.translatesReferenceBoundsIntoBoundary = YES;
  [self.animator addBehavior:collisionBehavior];

  UIGravityBehavior *gravityBehavior = [[UIGravityBehavior alloc] initWithItems:@[view]];
  [self.animator addBehavior:gravityBehavior];
}

- (void)dynamicAnimatorDidPause:(UIDynamicAnimator *)animator {
  NSLog(@"pause");
}
@end

To your question about getting all item velocities, I don't know of an easy way to do that. Unfortunately, UIDynamicAnimator doesn't directly know all of its items. This is indirectly because UIDyanamicBehavior doesn't include an items property. If this bothers you as much as it does me, consider duping radar://15054405.

But there is a solution if you just want to know the current linear velocity of specific items. Just add a UIDynamicItemBehavior with a custom action to log it:

UIDynamicItemBehavior *dynamicItemBehavior = [[UIDynamicItemBehavior alloc] initWithItems:@[view]];
__weak UIDynamicItemBehavior *weakBehavior = dynamicItemBehavior;
dynamicItemBehavior.action = ^{
  NSLog(@"Velocity: %@", NSStringFromCGPoint([weakBehavior linearVelocityForItem:view]));
};
[self.animator addBehavior:dynamicItemBehavior];
like image 67
Rob Napier Avatar answered Nov 02 '22 07:11

Rob Napier


I had a similar issue recently. In the end I used a UICollisionBehavior with boundaries instead of items (because otherwise the moving items were bumping the others...) and implement the delegate method collisionBehavior:beganContactForItem:withBoundaryIdentifier:atPoint: to know when I should remove the gravity

UICollisionBehavior *collide = [[[UICollisionBehavior alloc] initWithItems:borders] retain];
[collide addItem:movingItem];

[collide setCollisionMode:UICollisionBehaviorModeBoundaries];
[collide setTranslatesReferenceBoundsIntoBoundary:YES];

If you find a better solution, let me know :) !

like image 22
KIDdAe Avatar answered Nov 02 '22 09:11

KIDdAe


My problem is the same. My animator never comes to rest so once started, my app consumes 3 to 4% CPU forever. My views all appear to stop moving within 1/2 second. So rather than figure out why I'm not reaching equilibrium, I just hit it with a hammer and kill the animator with a timer. I give it 2 seconds.

- (void)createAnimator {
    if (_timer) {
        [_timer invalidate];
    }

    if (_animator) {
        [_animator removeAllBehaviors];
    }

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

    // create all behaviors

    // kill the animator in 2 seconds
    _timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(killAnimator:) userInfo:nil repeats:NO];
}

- (void)killAnimator:(NSTimer *)timer {
    [_animator removeAllBehaviors];
    _animator = nil;
    _timer = nil;
}
like image 1
Dan Loughney Avatar answered Nov 02 '22 09:11

Dan Loughney