Cocoa提供了在有限或不确定的时间内对某些类型的操作进行动画化的工具。NSAnimation提供的基本动画,其主要目的是提供一个动画时机和管理的源。尽管Animation
一词或许会让你想起卡通或其他电影,但是animation对象主要设计用于程序用户界面中的动画部分。比如,你可以使用NSViewAnimation类(NSAnimation的子类)对视图或窗口的大小、位置或者透明度创建平滑过渡。这个动画外观让您创建一个更加流畅的用户界面。
本文的组织结构:
本文包含下列文章:
NSAnimation
对象的使用。该部分描述动画对象的基本特征以及如何自定义它们。视图和窗口的动画。讲解视图动画对象的使用,该视图动画为平滑调整大小、重新定位和更改视图和窗口对象的不透明度提供高级接口。
NSAnimation 对象的使用
NSAnimation
类为在有限时间内发生的动画提供复杂的行为。OS X使用animation
对象对界面元素执行过渡效果。你可以自定义动画对象来为自己的代码实现动画。不同于NSTimer,动画的通知可以在不固定的时间间隔内发送,以方便我们去创建显示加速或减速的动画。
下面的部分介绍了创建自定义NSAnimation
对象并使用它管理动画内容的基本步骤。如果要给视图和窗口添加动画,NSViewAnimation`类或许能提供给你需要的功能。在下一节视图和窗口动画中描述的视图动画对视图缩放、随时间移动视图等提供了复杂的功能支持。
注意:动画对象在OS X10.4以后才能使用。
创建和配置Animation Timer
一个NSAnimation对象有如下几个重要的属性:
当前进度——介于0.0到1.0之间的值,该值表示动画完成的比例
帧率——每秒更新的次数
时长——动画发生的时长(秒为单位)
动画曲线——动画的相对速度;例如,动画可以在开始时缓慢加速,之后逐渐减速直到结束,或者保持匀速。
阻塞模式——根据应用程序对用户操作的响应性运行动画的模式。
当你去配置一个新的NSAnimation对象时,你至少要设置时长、动画曲线、帧率和阻塞模式这几个属性,也应当设置一个代理去监听动画的进度。当动画开始、结束、显式地停止、或者达到标记的进度时( Setting and Handling Progress Marks ),该动画对象就会发送消息给当前代理。如果不想使用代理,你必须继承NSAnimation类去接收进度信息,可查看Subclassing NSAnimation.
以下代码示范创建和配置一个标准的NSAnimation
对象的简单方法。创建动画的对象充当委托并处理任何进度消息。
Listing 1:初始化一个NSAnimation对象
- (id)init
{
self = [super init];
if (self)
{
// theAnim 是一个NSAnimation的实例对象.
theAnim = [[NSAnimation alloc] initWithDuration:10.0
animationCurve:NSAnimationEaseIn];
[theAnim setFrameRate:20.0];
[theAnim setAnimationBlockingMode:NSAnimationNonblocking];
[theAnim setDelegate:self];
}
return self;
}
initWithDuration:animationCurve:
方法是NSAnimation
类的指定初始化器,该方法允许设置动画的两个属性。对其他属性,你可以使用默认值或者使用正确的存取方法显式地设置属性值。默认属性如下:
- 默认的动画曲线为
NSAnimationEaseInOut
- 默认的阻塞模式为
NSAnimationBlocking
- 默认的帧率是一个合理值,平常为60Hz,但确切的值不确定
一旦你已经准备好了一个可使用的NSAnimation
对象,你可以通过向该对象发送startAnimation
消息运行该动画。如果需要在动画执行期间停止,则要想该对象发送一条stopAnimation
消息。NSAnimation对象的委托(如果存在的话)接收到通知它这两个事件的消息,以及通知它动画是否按计划完成的消息。
设置和处理进度标记
NSAnimation有进度标记的概念——表示动画完成进度的浮点数。当你开始一个动画并达到一个进度标记(特别地,当前进度值等于进度标记值时),动画对象会发送消息给它的代理,代理方法会去更新自定义的进度标记、播放声音、完成一些适合于动画的其他效果。
重要:
尽管您可以使用进度标记来“时间切片”对象的动画,但它不是实现一个流畅动画的理想方式。建议做法是继承NSAnimation并在每个frame改变时重新绘制该对象。在 Smooth Animations中查看更多信息。
通常在第一次创建和初始化对象时设置Animation对象的进度标记。以下代码展示设置20个等间距的进度标记。
Listing 2:设置一个NSAnimation对象的进度标记
- (void)awakeFromNib
{
NSAnimationProgress progMarks[] = {
0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5,
0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 1.0 };
int i, count = 20;
// theAnim 是一个NSAnimation实例变量
NSAnimation *theAnim = [[NSAnimation alloc] initWithDuration:10.0
animationCurve:NSAnimationEaseInOut];
[theAnim setFrameRate:20.0];
[theAnim setDelegate:self];
for (i=0; i<count; i++)
[theAnim addProgressMark:progMarks[i]];
}
不像上例那样在循环中添加progress-mark
值,您可以通过使用setProgressMarks
:方法在一次调用中设置它们,该方法需要封装浮点值的NSNumber对象数组为参数。
当一个运行中的动画对象达到进度标记值时,它会发送animation:didReachProgressMark:
消息给它的代理,委托应该以与传入的进度标记相适应的方式处理此消息。下面代码示例代理执行这个方法去定时播放一个音频。
Listing 3:代理方法animation:didReachProgressMark:的执行
- (void)animation:(NSAnimation *)animation
didReachProgressMark:(NSAnimationProgress)progress
{
if (animation == theAnim)
[[NSSound soundNamed:@"chug"] play];
}
自定义NSAnimation
尽管在很多时候可以使用NSAnimation对象,但是更多时候需要继承它实现我们的功能。自定义NSAnimation
有三个主要原因:
- 通过每帧间隔重新绘制来实现平滑的动画
- 当在主线程的非阻塞模式下运行动画时指定可用的run-loop模式
- 没有需要响应代理方法
animation:valueForProgress:
的开销就可以返回自定义的曲线值
完成前两个目标的程序将在下面的部分中描述。第三条中,不用遵守代理就可以返回自定义的curve值,但是必须要重写currentValue
方法。请在NSAnimation
类文档中查看更多信息。
平滑的动画
如上文中设置和处理进度标记中提到的,您可以给一个NSAnimation对象添加一系列的进度标记并且在每个进度值的地方执行代理方法animation:didReachProgressMark:
去重绘一个对象。然而,这不是对对象执行动画的最佳方式。除非你设置了一个很大的进度标记值(30每秒或更多),不然动画可能会非常不流畅。
更好的方式就是自定义NSAnimation
的子类,并重写setCurrentProgress:
方法,如下代码所示。NSAnimation对象在每一帧之后调用这个方法来改变进度值。通过拦截这个方法,你可以对那一帧执行其他任何重绘或你需要的更新。如果重写了该方法,请确保调用super的实现,以便它可以更新当前进度。
Listing 4:重写setCurrentProgress:方法
- (void)setCurrentProgress:(NSAnimationProgress)progress
{
//调用父类方法更新进度值.
[super setCurrentProgress:progress];
// 更新窗口位置.
NSRect theWinFrame = [[NSApp mainWindow] frame];
NSRect theScreenFrame = [[NSScreen mainScreen] visibleFrame];
theWinFrame.origin.x = progress *
(theScreenFrame.size.width - theWinFrame.size.width);
[[NSApp mainWindow] setFrame:theWinFrame display:YES animate:YES];
}
自定义Run-Loop模式集
具有NSAnimationNonblocking
阻塞模式的NSAnimation
对象以接受用户输入的运行循环模式在进程的主线程中运行。在运行动画之前,动画对象向自己发送一条runLoopModesForAnimation
消息,以获得当前有效的运行循环模式。默认情况下,这个方法返回nil,它告诉NSAnimation
使用默认模式(NSDefaultRunLoopMode
),模态面板模式(NSModalPanelRunLoopMode
)和事件跟踪运行循环模式(NSEventTrackingRunLoopMode
)。
您可以重写此方法以返回一组不同的运行循环模式,其中可以包含自定义模式。下面代码显示了一个返回默认模式数组减去事件跟踪模式(NSEventTrackingRunLoopMode
)的实现。
Listing 5: 从runLoopModesForAnimating返回一个run-loop模式
- (NSArray *)runLoopModesForAnimating
{
return [NSArray arrayWithObjects: NSDefaultRunLoopMode,
NSModalPanelRunLoopMode, nil];
}
链接动画
您可以链接两个动画对象,以便其中一个对象在另一个对象到达指定的动画标记时开始运行(或停止运行)。NSAnimation的这个特性对于协调不同的效果很有用。Listing 6演示了如何使用startWhenAnimation:reachesProgress:
方法在另一个动画到达中途点时启动动画。
Listing 6:链接两个动画对象
- (IBAction)startAnim:(id)sender
{
// theAnim 和 theOtherAnim 是NSAnimation类型的变量.
[theOtherAnim startWhenAnimation:theAnim reachesProgress:0.5];
[theAnim startAnimation];
}
如果您想在另一个动画达到进度标记时停止动画,请使用stopWhenAnimation:reachesProgess: method
。您可以无限地链接动画,一个接一个,然而,在任何时候,只能有一个“开始”和一个“停止”动画。
如果您有一个正在响应animation:didReachProgressMark: messages
的委托,它必须在多个动画之间进行区分,如Listing 7所示。
Listing 7:处理同时运行动画的进度标记
- (void)animation:(NSAnimation *)animation
didReachProgressMark:(NSAnimationProgress)progress
{
if (animation == theOtherAnim)
{
//执行一个适合于进度标记的效果
}else if (animation == theAnim)
{
// 执行一个适合于进度标记的效果
}
}
视图和窗口的动画
NSViewAnimation
是NSAnimation
类的子类,它为视图及窗口提供了一个方便的动画方式,主要有以下:
- 改变frame 的位置
- 改变frame 的大小
- 改变物体的不透明度,淡入或淡出。
视图动画过程
使用视图动画对象的方式与使用常规的NSAnimation
对象稍有不同。一个视图动画对象可以同时管理多个视图和窗口的动画过程。不是使用动画对象的方法设置属性,而是为要修改的每个视图或窗口创建一个动画属性字典。每个字典指定操作的目标(视图或窗口),以及您想要应用到该目标的效果。要设置其他因素,比如动画的持续时间和时间曲线,您需要继续使用NSAnimation
的方法。
一个动画属性字典只有一个必须值:目标对象,您可以通过NSViewAnimationTargetKey
键值添加此对象到字典。但是,这个键的存在不会改变视图或窗口。要进行更改,您必须包含一个或多个额外的键来指定所需的行为。
如果您愿意,您可以同时对单个目标对象执行多个操作。例如,您可以调整视图大小,改变它在屏幕上的位置,将它淡入或淡出。为了简单起见,以下部分将向您展示如何分别执行这些操作,要执行它们,只需将所有相关键添加到属性字典。
改变矩形frame
更改视图或窗口的框架矩形可以让您调整对象的大小并重新定位其相对于父对象的位置。对view来说,这意味着改变它在父视图中的位置和大小。对窗口来说,意味着改变它在桌面上的位置和大小。表1展示了改变框架矩形可能用到的部分键值对。
表1 视图或窗口调整所需要的键属性
Key | Value | 描述 |
---|---|---|
NSViewAnimationTargetKey | id | 需要调整大小或位置的视图或窗口的标识符,此键必要 |
NSViewAnimationStartFrameKey | NSValue | 包含目标对象的开始位置,NSValue对象应该包含一个编码过的NSRect数据类型。此值通常等于窗口或位置的当前结构。此值可选,默认值为目标对象的当前结构矩形。 |
NSViewAnimationEndFrameKey | NSValue | 包含目标对象的结束位置,NSValue对象应该包含一个编码过的NSRect数据类型。此键建议填写,如果没有给出,默认值为目标对象的当前结构矩形。 |
淡入淡出对象
如果你想隐藏一个视图或窗口,而不是让对象突然消失,你可以使用视图动画使该对象逐渐消失。同样地,可以使用类似的动画方式使视图再次可见,当视图渐入时,结束位置的frame必须为非零,若没有设置,该视图将仍然隐藏。表2列出了让一个对象淡入淡出时可设置在属性字典中的键值对。
Key | Value | 描述 |
---|---|---|
NSViewAnimationTargetKey | id | 要修改的NSView或NSWindow对象的标识符,此键为必须 |
NSViewAnimationEffectKey | NSString | 包含以下字符串常量之一:NSViewAnimationFadeInEffect 使对象可见,NSViewAnimationFadeOutEffect 隐藏它。这些效果在动画过程中改变了物体的不透明度。 |
一个视图动画的例子
Listing1说明了视图动画对象的基本用法。action方法为两个不同的视图对象设置属性字典,然后在发生动作时运行动画。对于第一个视图对象,动画对象沿着每个轴将视图的原点移动50个单位。对于第二个视图,动画对象将frame大小缩小到零,同时将视图淡出,直到完全隐藏。动画使用定制的时间曲线和持续时间,但使用默认的阻塞模式,这将阻塞用户在主线程上的输入,直到动画完成。
Listing 1 两个视图对象的动画
- (IBAction)startAnimations:(id)sender
{
// firstView, secondView 是通过outlets属性连接的xib文件中
NSViewAnimation *theAnim;
NSRect firstViewFrame;
NSRect newViewFrame;
NSMutableDictionary* firstViewDict;
NSMutableDictionary* secondViewDict;
{
// 对第一个视图创建属性字典.
firstViewDict = [NSMutableDictionary dictionaryWithCapacity:3];
firstViewFrame = [firstView frame];
// 指定要修改的视图对象.
[firstViewDict setObject:firstView forKey:NSViewAnimationTargetKey];
// 指定view的开始位置
[firstViewDict setObject:[NSValue valueWithRect:firstViewFrame]
forKey:NSViewAnimationStartFrameKey];
// 指定view的动画结束位置
newViewFrame = firstViewFrame;
newViewFrame.origin.x += 50;
newViewFrame.origin.y += 50;
[firstViewDict setObject:[NSValue valueWithRect:newViewFrame]
forKey:NSViewAnimationEndFrameKey];
}
{
// 为第二个视图创建属性字典.
secondViewDict = [NSMutableDictionary dictionaryWithCapacity:3];
[secondViewDict setObject:secondView forKey:NSViewAnimationTargetKey];
// 将view从当前位置缩放至看不见.
NSRect viewZeroSize = [secondView frame];
viewZeroSize.size.width = 0;
viewZeroSize.size.height = 0;
[secondViewDict setObject:[NSValue valueWithRect:viewZeroSize]
forKey:NSViewAnimationEndFrameKey];
// 让视图淡出
[secondViewDict setObject:NSViewAnimationFadeOutEffect
forKey:NSViewAnimationEffectKey];
}
// 创建view 的动画视图对象.
theAnim = [[NSViewAnimation alloc] initWithViewAnimations:[NSArray
arrayWithObjects:firstViewDict, secondViewDict, nil]];
//设置动画的其他属性
[theAnim setDuration:1.5];
[theAnim setAnimationCurve:NSAnimationEaseIn];
// 运行动画
[theAnim startAnimation];
}