睡眠艺术家一号 發表於 2025-8-24 19:03:00

一步一步学习使用FireMonkey动画(6) 用实例理解动画的运行状态

<p>虽然在过去的几节课中,已经详细的介绍了不少的属性和方法以及事件,本节来看一看动画的执行状态。</p>
<p>回顾一下前面的几节内容,可以看到,每一个动画都具有如下的属性和方法,请仔细阅读下面的属性或事件的作用,在接下来的案例中将会使用到它们:</p>
<h1 id="1-动画的属性和事件">1. 动画的属性和事件</h1>
<ul>
<li>
<p><strong>Duration</strong>:该属性表示动画的持续时间(以秒为单位)。它是一个浮点值,因此你可以指定动画播放的任何时间长度,而不仅仅是整数值。</p>
</li>
<li>
<p><strong>Enabled</strong>:严格来说,该属性决定动画是否可以启动。然而,该属性的一般理解与其在设计时使用相关。如果你希望动画在加载后自动启动,可以将 Enabled 设置为 True ;如果你在运行时将此属性从 True 更改为 False ,这也将导致动画在该时刻停止。</p>
</li>
</ul>
<p><em><strong>注意:请不要将 Enabled 与Running属性(Running属性只具有Public可见性,而 Enabled 是一个Publish属性,可以在属性编辑器上进行修改)混淆。Enabled决定了动画是否可以通过Start进行启动。</strong></em></p>
<ul>
<li>
<p><strong>Running</strong>: 这是Public的,并不是Published的属性,因此未列在对象检查器窗口中。这个只读属性可用于判断动画是否正在运行。当动画开始时,此属性的值将为 True ,并保持此值直到动画结束(这可能意味着整个持续时间,或者由于某种原因动画在完成前被停止)。完成的动画将设置 Running 为 False 。</p>
</li>
<li>
<p><strong>Delay</strong>: 此属性设置动画执行前等待的时间(以秒为单位)。如果你将此属性设置为 1 (秒)并调用 Start 方法,动画将在 1 秒后执行。</p>
</li>
<li>
<p><strong>Interpolation</strong>:此属性将允许你选择应用于此特定动画的插值修正函数。</p>
</li>
<li>
<p><strong>AnimationType</strong>:此属性与插值相关,你可以将其视为其修饰符。在大多数情况下,你可以将其视为在起始值和最终值之间选择插值强调的位置。此属性的可用值为 In 、 Out 和 InOut 。</p>
</li>
<li>
<p><strong>Inverse</strong>:你可以将此属性设置为 True 来反转动画方向;具体来说,你实际上反转的是动画中的时间流(你可以通过在动画的 OnProcess 事件处理程序中打印 NormalizedTime 值来轻松证明这一点)。这也意味着所有动画都是可逆的。</p>
</li>
<li>
<p><strong>Loop</strong>: 这个布尔属性决定动画是否应该在其开始后循环播放。将此属性设置为 True 将导致动画在从初始值到最终值的过渡完成后(或 Duration 到期时)不会结束。通常,设置此属性也会对列表中的下一个属性——即 AutoReverse ——产生一些影响,因为您可能不希望盲目地从相同的初始点重复到目标值的相同过渡。当将 Loop 设置为 True 时,默认情况下就会发生这种情况。</p>
</li>
<li>
<p><strong>AutoReverse</strong>:正如我们所见,所有动画都可以反转,并且可以配置为循环。 AutoReverse 属性将动画设置为在第一次运行完成后自动反转(通过切换 Inverse 属性)。因此,如果你正在将值从 A 过渡到 B,一旦达到 B,相同的动画(此时 Inverse 设置为 True )将反向运行;也就是说,从 B 过渡到 A。最终持续时间将遵循(最终的) Delay + Duration (A 到 B)+ Duration (B 到 A)的方案。请注意,在第二次迭代之前不会应用任何 Delay ;此功能通常与将 Loop 设置为 True 相关联(尽管它与它并不严格相关,但仍然可以独立工作)。</p>
</li>
<li>
<p><strong>CurrentTime</strong>:这是一个只读属性,它将提供动画的当前执行时间;如果动画未运行,它将是 0。你可以将其视为一个在实际过渡开始时启动的秒表(因此 Delay 永远不会包含在内)。当 Inverse 为 False 时,会看到这个值增加,如果它为 True ,这个值会减小(当这是由 AutoReverse 属性的值引起时,也会发生这种情况)。</p>
</li>
<li>
<p><strong>NormalizedTime</strong>:这是用于插值的时间, NormalizedTime 属性让您可以访问当前时间的值,包括插值。如果在动画运行时比较 CurrentTime 和 NormalizedTime ,将看到实际时间与用于插值的时间(使用线性插值时,将看到重合的值)之间的差异。</p>
</li>
<li>
<p><strong>Pause</strong>:Pause 属性可以在其值为 True 时暂停动画的执行;可以在动画运行时(或之前)设置此属性。</p>
</li>
<li>
<p><strong>OnProcess事件</strong>:可以设置 OnProcess 事件的处理器,以便每次动画将要计算新步骤时收到通知;在正常情况下,事件会频繁的触发。动画的默认帧率为 60,全局动画计时器的间隔也相应设置,因此可以预期每秒调用此事件 60 次。这是一个监控动画的 CurrentTime 或 NormalizedTime 属性值并使用它们进行某些附加目的(同步、日志记录等)的好地方。</p>
</li>
<li>
<p><strong>OnFinish事件</strong>:当动画完成所有中间步骤,并且当前过渡值与动画设置的最终期望值一致时, OnFinish 事件就会被触发。</p>
</li>
</ul>
<p><em><strong>请记住, OnFinish 事件只会发生一次。这意味着如果在同一执行过程中暂停动画——即使多次暂停—— OnFinish 事件也不会被触发。同样适用于 AutoInverse 属性的使用,当其设置为 True 时,基本上会导致动画运行两次,但不会导致 OnFinish 事件触发两次。</strong></em></p>
<h1 id="2-使用示例观察动画的执行状态">2. 使用示例观察动画的执行状态</h1>
<p>在这一节中,创建了一个非常简单的例子,它包含一个使小球旋转的TFloatAnimation组件,它的属性设置如下所示:</p>

<p><img src="https://img2024.cnblogs.com/blog/22554/202508/22554-20250824150201099-68596507.png"></p>
<ul>
<li>AutoReverse为True,表示动画会反向进行一次。</li>
<li>Enabled为False,表示动画不会在启动后立即执行。</li>
<li>Deploy为1,表示延迟1秒,Duration为5表示5秒跑完。</li>
</ul>
<p>整个用户界面布局如下所示:</p>

<p><img src="https://img2024.cnblogs.com/blog/22554/202508/22554-20250824151034425-846109778.png"></p>
<p>在主窗体上添加了5个按钮,一个TMemo和一个TProgressBar控件,添加了一个TTimer控件用来计时。</p>
<p>我们分别为TFloatAnimation的OnProcess和OnFinish添加了事件,并且在TTimer的OnTimer事件中进行全局的计时,代码如下:</p>
<pre><code class="language-Pascal">// FloatAnimation1动画完成事件处理程序
procedure TfrmMain.FloatAnimation1Finish(Sender: TObject);
begin
// 在Memo1控件中添加一行日志,记录动画完成的时间
Memo1.Lines.Add(TimeToStr(Now) + ': ' + '动画已完成');

// 重置开始时间标记,0通常表示未开始或已结束
FStartedAt:=0;
end;

// FloatAnimation1动画处理过程事件(每帧触发)
procedure TfrmMain.FloatAnimation1Process(Sender: TObject);
// 声明局部变量
var
// t: 用于存储标准化时间进度(0.0到1.0)
t,
// Scale: 声明但未使用的变量,可能用于缩放计算
Scale: Single;
begin
// 在Label3上显示动画的当前时间和标准化时间
// FormatFloat将浮点数格式化为指定格式的字符串(保留3位小数)
Label3.Text :=
    // 显示动画从开始到现在经过的时间(秒)
    '当前时间: ' + FormatFloat('0.000', FloatAnimation1.CurrentTime) +
    // 显示动画的标准化进度(0.0到1.0)
    '插值时间: ' + FormatFloat('0.000', FloatAnimation1.NormalizedTime);

// 获取当前的标准化时间进度(0.0表示开始,1.0表示结束)
t := FloatAnimation1.NormalizedTime;

// 在Label1上显示动画进度百分比
// Format函数将浮点数格式化为百分比字符串(保留1位小数)
Label1.Text := Format('进度: %.1f%%', );

// 设置进度条的值,将标准化时间转换为百分比(0-100)
ProgressBar1.Value:=t*100;

end;

// 定时器Timer1的定时事件处理程序(按Interval间隔定期触发)
procedure TfrmMain.Timer1Timer(Sender: TObject);
begin
// 声明并初始化一个空字符串,用于构建状态信息
var LStr := '';

// 检查动画是否启用
if FloatAnimation1.Enabled then
    // 如果启用,在状态字符串后添加"-Enabled"标记
    LStr := LStr + '-Enabled';

// 检查动画是否正在运行
if FloatAnimation1.Running then
    // 如果正在运行,在状态字符串后添加"-Running"标记
    LStr := LStr + '-Running';

// 检查动画是否处于反向播放模式
if FloatAnimation1.Inverse then
    // 如果处于反向模式,在状态字符串后添加"-Inverse"标记
    LStr := LStr + '-Inverse';

// 检查动画是否有有效的开始时间(FStartedAt &gt; 0)
if FStartedAt&gt;0 then
    // 在Label2上显示动画状态和从开始到现在经过的毫秒数
    // MilliSecondsBetween计算两个时间点之间的毫秒数差
    // ToString将数值转换为字符串
    Label2.Text := LStr + ' ' + MilliSecondsBetween(FStartedAt, Now).ToString + 'ms';
end;

</code></pre>
<p>接下来测试按下不同的按钮,会发生什么。</p>
<h3 id="21-当单击启动动画按钮">2.1 当单击“启动动画”按钮,</h3>
<p>它会调用FloatAnimation.Start,并给FStartedAt赋当前时间值:</p>
<pre><code class="language-Pascal">procedure TfrmMain.btnStartAnimationClick(Sender: TObject);
begin
// 启动FloatAnimation1动画,开始播放
FloatAnimation1.Start;
// 记录动画开始的时间点,Now函数返回当前日期和时间
FStartedAt := Now;
end;
</code></pre>
<p>运行时可以很清楚的看到运行状态,直到出现“动画已完成”的提示:</p>

<p><img src="https://img2024.cnblogs.com/blog/22554/202508/22554-20250824152210667-815369237.gif"></p>
<p>当调用FloatAnimation.Start后,TTimer开始显示FloatAnimation的状态,可以看到Enabled、Running和Inverse三个状态分别为True,并且在延时1秒之后,CurrentTime和NormalizedTime才开始显示值,进度条开始动起来。</p>
<p>FloatAnimation使用的是Quadratic插值算法,可以看到CurrentTime和NormalizedTime的值其实并不重合。</p>
<p>FloatAnimation的OnFinish事件直到反向动画完成,实际上经历了2个5秒,再加上延迟的1秒,因此最终经历了差不多11秒时长。</p>
<h3 id="22如果按下暂停启动动画按钮会出现什么效果">2.2 如果按下"暂停/启动动画"按钮,会出现什么效果?</h3>

<p><img src="https://img2024.cnblogs.com/blog/22554/202508/22554-20250824153507453-652914402.gif"><br>
在启动动画后,按下暂停键,pause属性设置为True后,动画仍然处于Running状态,所以无法单击“启动动画”按钮让动画重新运作。但是可以单击“停止动画”按钮,让动画处于停止状态,停止之后,CurrentTime和NormalizedTime归0。</p>
<p><em><strong>虽然动画已经停止,但是Pause仍然为False状态,所以在下次启动动画时,需要将Pause设置为True,动画才能重新启动,这跟很多播放器完全不相同</strong></em></p>
<p><em><strong>特别注意:暂停并不会触发OnFinish事件,无论暂停多少次。</strong></em></p>
<p>按钮的事件处理代码如下所示:</p>
<pre><code class="language-Pascal">// 暂停/继续动画按钮的点击事件处理程序
procedure TfrmMain.btnPauseAnimationClick(Sender: TObject);
begin
// 切换动画的暂停状态:如果当前是暂停则继续,如果是播放则暂停
FloatAnimation1.Pause := not FloatAnimation1.Pause;

// 在Memo1控件中添加一行日志,记录当前时间和暂停状态
// TimeToStr(Now)将当前时间转换为字符串格式
// BoolToStr将布尔值转换为字符串,True参数表示返回'True'/'False'而不是'-1'/'0'
Memo1.Lines.Add(TimeToStr(Now) + ': ' + '暂停状态 = ' + BoolToStr(FloatAnimation1.Pause, True));
end;
</code></pre>
<h3 id="23如果按下启用禁用动画按钮会出现什么效果">2.3 如果按下“启用/禁用动画”按钮,会出现什么效果?</h3>
<p>“启用/禁用动画”按钮会设置动画的Enable属性,如果设置Enable属性为True,则如同调用Start方法,动画立即开始运行。如果Enable为False,则动画立即停止,并触发OnFinish事件。</p>
<p>代码如下所示:</p>
<pre><code class="language-Pascal">// 启用/禁用动画按钮的点击事件处理程序
procedure TfrmMain.btnEnableDisableAnimationClick(Sender: TObject);
begin
// 切换动画的启用状态:如果当前启用则禁用,如果禁用则启用
FloatAnimation1.Enabled := not FloatAnimation1.Enabled;

// 在Memo1控件中添加一行日志,记录当前时间和启用状态
Memo1.Lines.Add(TimeToStr(Now) + ': ' + 'Enabled 状态 = ' + BoolToStr(FloatAnimation1.Enabled, True));

// 如果动画被启用
if FloatAnimation1.Enabled then
      // 重新记录开始时间
      FStartedAt := Now;
end;
</code></pre>
<p>效果如下所示:</p>

<p><img src="https://img2024.cnblogs.com/blog/22554/202508/22554-20250824155151873-1704887428.gif"></p>
<p>可以看到,禁用时,OnFinish被触发,CurrentTime和NormalizedTime的时间被重置。</p>
<h3 id="24如果按下停止动画按钮会出现什么效果">2.4 如果按下“停止动画”按钮,会出现什么效果?</h3>
<p>“停止动画”按钮调用Stop方法,代码如下:</p>
<pre><code class="language-Pascal">// 停止动画按钮的点击事件处理程序
procedure TfrmMain.btnStopAnimationClick(Sender: TObject);
begin
// 检查动画是否正在运行中
if FloatAnimation1.Running then
   // 如果正在运行,则停止动画
   FloatAnimation1.Stop;
end;
</code></pre>
<p>运行效果如下所示:</p>

<p><img src="https://img2024.cnblogs.com/blog/22554/202508/22554-20250824155622656-1823595541.gif"></p>
<p>它与调用Enable=False相似,动画立即停止,并重置了显示时间。</p>
<h3 id="25如果按下在当下位置停止按钮会出现什么效果">2.5 如果按下“在当下位置停止”按钮,会出现什么效果?</h3>
<p>这个按钮的事件处理代码如下:</p>
<pre><code class="language-Pascal">// 停止在当前状态按钮的点击事件处理程序
procedure TfrmMain.btnStopAtAnimationClick(Sender: TObject);
begin
// 检查动画是否正在运行中
if FloatAnimation1.Running then
   // 如果正在运行,则停止动画并保持当前属性值
   FloatAnimation1.StopAtCurrent;
end;
</code></pre>
<p>效果如下所示:</p>

<p><img src="https://img2024.cnblogs.com/blog/22554/202508/22554-20250824155903567-210874234.gif"></p>
<p>可以看到,StopAtCurrent方法,并不会重置时间,它保留了CurrentTime和NormalizedTime的值,但动画确实已经停止,OnFinish已经触发。</p>
<h1 id="总结">总结</h1>
<p>当设计多个动画联合时,有时候要获取或设置动画的状态,或者是要获取动画的执行时间来进行下一步的处理,这个时候了解动画的执行状态就特别有必要。<br>
这节通过一个案例,详细介绍了不同的动画属性在不同的状态下的值,相信对于初学者,会有一定的启发作用。</p><br><br>
来源:https://www.cnblogs.com/lincats/p/19055742
頁: [1]
查看完整版本: 一步一步学习使用FireMonkey动画(6) 用实例理解动画的运行状态