9

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?

Nailer
  • 2,446
  • 1
  • 24
  • 34
  • To anyone reading this: I might have found the solution. I haven't tested it yet, but I ran into the same problem in a different situation: I was using the UIDynamicAnimator updateItemUsingCurrentState inside an animation block. This was not the case in my TrayView class, but I figured this realisation might help people in the future. If I have time I will try to inspect my use of the updateItemUsingCurrentState and see if I can stop the problem from happening in my TrayView class. – Nailer Nov 05 '13 at 10:23

3 Answers3

6

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];
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • I am also suspecting the infinitely small bounce you're mentioning. I've tried removing any items with elasticity. Is it possible to somehow print the velocity/acceleration of items in the animator? – Nailer Oct 16 '13 at 07:44
  • Thanks for your detailed edit. I will definitely revisit the bouncing tray in the future. Currently I've just replaced it with my old pre-UIDynamics implementation. Bouncy trays aren't highest on my priority list right now ;) I will accept your answer though. – Nailer Oct 18 '13 at 08:17
1

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 :) !

KIDdAe
  • 2,714
  • 2
  • 22
  • 29
  • I am using boundries to keep my tray in place. The problem with adding anything in beganContactForItem is that my tray bounces once it hits the bottom of the screen. Thus the first bounce will de-activate the gravity, and it will never hit the bottom again since it will just bounce up zero gravity style. – Nailer Oct 16 '13 at 07:43
  • And if you remove completely the items from the animator when they hit the bottom to finish the animation by yourself ? Or if you have only one item, maybe you can replace it correctly with an animation when `dynamicAnimatorDidPause:` is triggered ? – KIDdAe Oct 16 '13 at 08:04
  • `dynamicAnimatorDidPause:` Is never triggered, but one solution might be to finish the animation manually after the first collision. – Nailer Oct 16 '13 at 08:08
1

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;
}
Dan Loughney
  • 4,647
  • 3
  • 25
  • 40