Question

I have a custom view in an NSStatusItem object. This view displays icons. It's also able to show a progress, but you have to call [self.statusitemview setProgressValue:theValue]; I have a set of icons, and it chooses the right one using this value.

This seems very jerky, because the executed process doesn't send updates all the time. So I would like to animate this.

I would like to call the animation like you can with other cocoa-controls: [[self.statusItemView animator] setProgressValue:value];

If that's at all possible

What is the proper way to do this? I wouldn't want to use an NSTimer.

EDIT

The images are drawn using the drawRect: method

Here's the code:

- (void)drawRect:(NSRect)dirtyRect
{
    if (self.isHighlighted) {
        [self.statusItem drawStatusBarBackgroundInRect:self.bounds withHighlight:YES];
    }

    [self drawIcon];
}

- (void)drawIcon {
    if (!self.showsProgress) {
        [self drawIconWithName:@"statusItem"];
    } else {
        [self drawProgressIcon];
    }
}

- (void)drawProgressIcon {
    NSString *pushed = (self.isHighlighted)?@"-pushed":@"";
    int iconValue = ((self.progressValue / (float)kStatusItemViewMaxValue) * kStatusItemViewProgressStates);
    [self drawIconWithName:[NSString stringWithFormat:@"statusItem%@-%d", pushed, iconValue]];
}

- (void)drawIconWithName:(NSString *)iconName {
    if (self.isHighlighted && !self.showsProgress) iconName = [iconName stringByAppendingString:@"-pushed"];
    NSImage *icon = [NSImage imageNamed:iconName];
    NSRect drawingRect = NSCenterRect(self.bounds, icon);

    [icon drawInRect:drawingRect fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0 respectFlipped:YES hints:nil];
}


- (void)setProgressValue:(int)progressValue {
    if (progressValue > kStatusItemViewMaxValue || progressValue < 0) {
        @throw [NSException exceptionWithName:@"Invalid Progress Value"
                                       reason:[NSString stringWithFormat:@"The value %d id invalid. Range {0 - %d}", progressValue, kStatusItemViewMaxValue]
                                     userInfo:nil];
    }

    _progressValue = progressValue;
    [self setNeedsDisplay:YES];
}

- (void)setShowsProgress:(BOOL)showsProgress {
    if (!showsProgress) self.progressValue = 0;
    _showsProgress = showsProgress;

    [self setNeedsDisplay:YES];
}

It has to be possible somehow. Since standard controls from Apple are drawn using the drawRect:, but have smooth animations...

Was it helpful?

Solution

To animate custom properties, you need to make your view conform to the NSAnimatablePropertyContainer protocol.

You can then set up multiple custom properties as animatable (in addition to the properties already supported by NSView), and then you can simply use your views' animator proxy to animate the properties:

yourObject.animator.propertyName = finalPropertyValue;

Apart from making animation very simple, it also allows you to animate multiple objects simultaneously using an NSAnimationContext:

[NSAnimationContext beginGrouping];
firstObject.animator.propertyName = finalPropertyValue1;
secondObject.animator.propertyName = finalPropertyValue2;
[NSAnimationContext endGrouping];

You can also set the duration and supply a completion handler block:

[NSAnimationContext beginGrouping];
[[NSAnimationContext currentContext] setDuration:0.5];
[[NSAnimationContext currentContext] setCompletionHandler:^{
    NSLog(@"animation finished");
}];
firstObject.animator.propertyName = finalPropertyValue1;
secondObject.animator.propertyName = finalPropertyValue2;
[NSAnimationContext endGrouping];

For a standard NSView object, if you want to add animation support to a property in your view, you just need to override the +defaultAnimationForKey: method in your view and return an animation for the property:

//declare the default animations for any keys we want to animate
+ (id)defaultAnimationForKey:(NSString *)key
{
    //in this case, we want to add animation for our x and y keys
    if ([key isEqualToString:@"x"] || [key isEqualToString:@"y"]) {
        return [CABasicAnimation animation];
    } else {
        // Defer to super's implementation for any keys we don't specifically handle.
        return [super defaultAnimationForKey:key];
    }
}

I've created a simple sample project that shows how to animate multiple properties of a view simultaneously using the NSAnimatablePropertyContainer protocol.

All your view needs to do to update successfully is make sure that setNeedsDisplay:YES is called when any of the animatable properties are modified. You can then get the values of those properties in your drawRect: method and update the animation based on those values.

OTHER TIPS

I have answered similar question here

You can't animate custom properties with animator, but you can write custom animation if you need, though it's not best idea.

Updated (Custom Animation):

- (void) scrollTick:(NSDictionary*) params
{
  static NSTimeInterval timeStart = 0;
  if(!timeStart)
    timeStart = [NSDate timeIntervalSinceReferenceDate];

  NSTimeInterval stTime = timeStart;
  NSTimeInterval currentTime = [NSDate timeIntervalSinceReferenceDate];
  NSTimeInterval totalTime = [[params valueForKey:@"duration"] doubleValue];

  if(currentTime > timeStart + totalTime)
  {
      currentTime = timeStart + totalTime;
      timeStart = 0;
  }

  double progress = (currentTime - stTime)/totalTime;
  progress = (sin(progress*3.14-3.14/2.0)+1.0)/2.0;

  NSClipView* clip = [params valueForKey:@"target"];
  float startValue = [[params valueForKey:@"from"] floatValue];
  float endValue = [[params valueForKey:@"to"] floatValue];

  float newValue = startValue + (endValue - startValue)*progress;
  [self setProperty:newValue];

  if(timeStart)
    [self performSelectorOnMainThread:@selector(scrollTick:) withObject:params waitUntilDone:NO];
}

- (void) setAnimatedProperty:(float)newValue
{
  NSDictionary* params = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithFloat:self.property], @"from",
   [NSNumber numberWithFloat:newValue],@"to",
   [NSNumber numberWithFloat:1.0],@"duration",
            nil];

  [self performSelectorOnMainThread:@selector(scrollTick:) withObject:params waitUntilDone:NO];
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top