【问题标题】:Animating a NSStatusItem动画 NSStatusItem
【发布时间】:2025-11-30 05:50:01
【问题描述】:

我在 NSStatusItem 对象中有一个自定义视图。 此视图显示图标。它也可以显示进度,但您必须致电[self.statusitemview setProgressValue:theValue]; 我有一组图标,它使用这个值选择正确的。

这看起来很生涩,因为执行的进程不会一直发送更新。 所以我想动画这个。

我想像使用其他可可控件一样调用动画: [[self.statusItemView 动画师] setProgressValue:value];

如果可能的话

这样做的正确方法是什么? 我不想使用 NSTimer。

编辑

图像是使用 drawRect: 方法绘制的

代码如下:

- (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];
}

它必须以某种方式成为可能。 由于 Apple 的标准控件是使用 drawRect: 绘制的,但具有流畅的动画...

【问题讨论】:

  • 您的状态项视图是如何定义的?它有一层吗? setProgressValue: 有什么作用?
  • 很遗憾没有,我是用drawRect实现的:
  • 和 setProgressValue 只是设置了一个 ivar 并发送 setNeedsDisplay: 来更新它

标签: cocoa animation core-animation nsview nsstatusitem


【解决方案1】:

要为自定义属性设置动画,您需要使您的视图符合NSAnimatablePropertyContainer 协议。

然后,您可以将多个自定义属性设置为动画(除了 NSView 已经支持的属性),然后您可以简单地使用视图的 animator 代理为属性设置动画:

yourObject.animator.propertyName = finalPropertyValue;

除了让动画变得非常简单之外,它还允许您使用NSAnimationContext同时为多个对象设置动画:

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

您还可以设置持续时间并提供完成处理程序块:

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

对于一个标准的NSView对象,如果你想在你的视图中为一个属性添加动画支持,你只需要在你的视图中重写+defaultAnimationForKey:方法并为该属性返回一个动画:

//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];
    }
}

我创建了一个简单的sample project,它展示了如何使用NSAnimatablePropertyContainer 协议同时为视图的多个属性设置动画。

要成功更新您的视图所需要做的就是确保在修改任何可动画属性时调用setNeedsDisplay:YES。然后,您可以在 drawRect: 方法中获取这些属性的值,并根据这些值更新动画。

【讨论】:

  • 非常感谢!没想到这么简单
【解决方案2】:

我已经回答了类似的问题here

您不能使用 animator 为自定义属性设置动画,但如果需要,您可以编写自定义动画,但这不是最好的主意。

更新(自定义动画):

- (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];
}

【讨论】:

  • 你误解了我的问题。它与 NSProgressIndicator 无关。我有自己的属性,称为 property,它可以帮助我找到当前进度的正确图标。但不是设置 [self.statusItemView setProgressValue:0.5];看到它跳跃,我希望它动画到 0.5。
  • 您不能使用 animator 为自定义属性设置动画。您需要自定义动画 - 我已经用自定义动画示例(线性)更新了我的答案。
  • 这不是我想要的。是的,我知道我不能使用 animator 属性,但必须可以通过 core-animation 以某种方式做到这一点。如有必要,我可以使用图层。有很多方法可以做到这一点,但我只是在绘制它时才让它工作,而不是在将图像设置为内容时。
  • 我不喜欢使用计时器和递归方法,当你有很多动画时会出现问题,我会这样做。
  • 您绝对可以使用animator 代理为自定义属性设置动画。看我的回答。