Winforms 中的控件动画






4.95/5 (82投票s)
Winforms 动画。
引言
VisualEffects
库允许 Winforms 控件动画。它提供了许多现成的效果,包括控件的大小、位置、颜色和透明度效果,并支持缓动函数。您可以通过实现 IEffect
接口轻松创建新效果,并提供新的缓动函数。效果和缓动函数是独立的,可以根据需要组合。可以组合多种效果以无缝协同工作。一些示例
左侧固定宽度操作效果,带有弹性缓动
线性淡出效果
线性颜色渐变效果
组合之前的效果
背景
为了理解 VisualEffects
的工作原理,我需要定义动画器 (animators)、效果 (effects)、缓动函数 (easing functions)、动画 (animations) 和过渡 (transitions) 是什么。
动画器 (ANIMATORS)
动画器是实现动画效果的引擎。其核心是一个计时器和一个秒表。每次滴答,都会使用提供的缓动函数和实际经过的时间计算出一个新值,然后通过效果来操作控件属性。当经过时间达到预设时长时,计时器停止。
缓动函数 (EASING FUNCTIONS)
缓动函数是数学函数,用于在两个端点之间进行插值,通常产生非线性结果。它们决定了事物变化的方式;或者更准确地说,它们决定了效果如何应用。
给定预期的效果持续时间和动画器开始播放效果以来经过的时间量,缓动函数将计算出控件在给定时间点应具有的正确值。在 T(0)
时,值为初始值;在 T(Max)
时,值为我们想要达到的目标值。
我使用的缓动函数来自 http://gizma.com/easing/。在我的库中,缓动函数是一个委托函数,定义如下:
public delegate double EasingDelegate( double currentTime,
double minValue, double maxValue, double duration );
如果您是第一次听说缓动,您可能一直以来都在进行线性动画。线性缓动是一个有效的缓动函数,但它不够真实,因为现实生活中的物体不会立即启动和停止,并且几乎从不以恒定的速度变化。缓动函数(通常)以更自然、更真实的方式起作用。
此示例演示了线性缓动的实现方式
public static double Linear( double currentTime, double minHeight,
double maxHeight, double duration )
{
return maxHeight * currentTime / duration + minHeight;
}
效果 (EFFECTS)
效果是通过操作控件属性来实现的。在我的库中,效果是一个实现了 IEffect
接口的类,定义如下:
public interface IEffect
{
EffectInteractions Interaction { get; }
int GetCurrentValue( Control control );
void SetValue( Control control, int originalValue, int valueToReach, int newValue );
int GetMinimumValue( Control control );
int GetMaximumValue( Control control );
}
示例 1
一个简单、完美工作的示例,演示了如何实现 IEffect
来创建一个操作控件 Height
属性的效果,如下所示:
public class TopAnchoredHeightManipulationlEffect : IEffect
{
public EffectInteractions Interaction
{
get { return EffectInteractions.HEIGHT; }
}
public int GetCurrentValue( Control control )
{
return control.Height;
}
public void SetValue( Control control, int originalValue, int valueToReach, int newValue )
{
control.Height = newValue;
}
public int GetMinimumValue( Control control )
{
return control.MinimumSize.IsEmpty ? Int32.MinValue
: control.MinimumSize.Height;
}
public int GetMaximumValue( Control control )
{
return control.MaximumSize.IsEmpty ? Int32.MaxValue
: control.MaximumSize.Height;
}
}
上面的效果仅作用于 Height
属性。它在 GetCurrentValue
中获取控件当前的 Height
属性,并在 SetValue
中设置一个新的 Height
值。结果是,控件将从底部进行缩放。在这里,您可以看到它的实际效果,配合线性缓动:
示例 2
有几个属性决定了控件的大小或位置:Width、Height、Top、Bottom、Left、Right、Size、Location 和 Bounds。先设置 Width 再设置 Height 会产生双倍的工作量,而不是通过 Size 一次性设置两者。
作用于单个属性的效果是通用的,但它不能针对与其他效果协同工作进行优化。如果您同时应用许多效果,可能会出现闪烁或不平滑的动画。闪烁是否明显取决于您混合效果的方式以及效果的表现。
因此,有时最好编写一个自定义的、更专业的效果来优化绘制。假设我们希望控件底部固定,顶部高度随之改变;我们可以组合两个效果,一个作用于 Height
属性,一个作用于 Top
属性。这种方法可行,但不是最优的。要高效地实现我们的目标,我们需要同时操作 Height
和 Top
属性。
public class BottomAnchoredHeightManipulationEffect : IEffect
{
public int GetCurrentValue( Control control )
{
return control.Height;
}
public void SetValue( Control control, int originalValue, int valueToReach, int newValue )
{
//changing location and size independently can cause flickering:
//change bounds property instead.
var size = new Size( control.Width, newValue );
var location = new Point( control.Left, control.Top +
( control.Height - newValue ) );
control.Bounds = new Rectangle( location, size );
}
public int GetMinimumValue( Control control )
{
if( control.MinimumSize.IsEmpty )
return Int32.MinValue;
return control.MinimumSize.Height;
}
public int GetMaximumValue( Control control )
{
if( control.MaximumSize.IsEmpty )
return Int32.MaxValue;
return control.MaximumSize.Height;
}
public EffectInteractions Interaction
{
get { return EffectInteractions.BOUNDS; }
}
}
这是配合线性缓动时的效果:
示例 3
现在,我将展示一个更复杂的示例,我将尝试创建一个颜色渐变效果。由于我们需要处理 4 个通道(A、R、G、B),因此创建一个管理所有这些通道的效果会更方便:
public class ColorShiftEffect : IEffect
{
public EffectInteractions Interaction
{
get { return EffectInteractions.COLOR; }
}
public int GetCurrentValue( Control control )
{
return control.BackColor.ToArgb();
}
public void SetValue( Control control, int originalValue, int valueToReach, int newValue )
{
int actualValueChange = Math.Abs( originalValue - valueToReach );
int currentValue = this.GetCurrentValue( control );
double absoluteChangePerc =
( (double)( ( originalValue - newValue ) * 100 ) ) / actualValueChange;
absoluteChangePerc = Math.Abs( absoluteChangePerc );
if( absoluteChangePerc > 100.0f )
return;
Color originalColor = Color.FromArgb( originalValue );
Color newColor = Color.FromArgb( valueToReach );
int newA = (int)Interpolate( originalColor.A, newColor.A, absoluteChangePerc );
int newR = (int)Interpolate( originalColor.R, newColor.R, absoluteChangePerc );
int newG = (int)Interpolate( originalColor.G, newColor.G, absoluteChangePerc );
int newB = (int)Interpolate( originalColor.B, newColor.B, absoluteChangePerc );
control.BackColor = Color.FromArgb( newA, newR, newG, newB );
}
public int GetMinimumValue( Control control )
{
return Color.Black.ToArgb();
}
public int GetMaximumValue( Control control )
{
return Color.White.ToArgb();
}
private int Interpolate( int val1, int val2, double changePerc )
{
int difference = val2 - val1;
int distance = (int)( difference * ( changePerc / 100 ) );
int result = (int)( val1 + distance );
return result;
}
}
首先要注意的是,我必须找到一种方法将 Color
转换为整数,使其适合我的接口。如果您单独处理每个通道,则不需要这样做,因为每个通道都是一个整数。在这种情况下,ToArgb()
可以做到这一点。之后,在 SetValue
中,我们不能仅仅将 newValue
转换回 Color
并进行赋值来达到颜色渐变效果;事实上,我们的 newValue
更可能代表一个随机颜色,因为动画器不知道它正在操作的是“颜色的整数表示”,因此没有考虑相应地改变 ARGB 通道。
要解决这个问题,我们可以计算 newValue
相对于 originalValue
的百分比变化,并将该变化应用于我们原始颜色的每个通道。
以下是我们获得的效果(线性缓动):
动画和过渡 (ANIMATIONS AND TRANSITIONS)
动画由一个或多个效果在同一控件上协同工作组成。过渡定义了不同动画如何在不同控件上工作,以及不同动画如何相互作用。
目前我的库中没有动画或过渡的抽象,但由于它们基本上是对控件应用效果,所以可以轻松编写。
示例
大多数情况下,您希望动画能够展开和折叠、显示和隐藏,或者通常执行一个效果及其相反的效果。
public class FoldAnimation
{
public Control Control { get; private set; }
public Size MaxSize { get; set; }
public Size MinSize { get; set; }
public int Duration { get; set; }
public int Delay { get; set; }
public FoldAnimation(Control control)
{
this.Control = control;
this.MaxSize = control.Size;
this.MinSize = control.MinimumSize;
this.Duration = 1000;
this.Delay = 0;
}
public void Show()
{
this.Control.Animate(new HorizontalFoldEffect(),
EasingFunctions.CircEaseIn, this.MaxSize.Height, this.Duration, this.Delay);
this.Control.Animate(new VerticalFoldEffect(),
EasingFunctions.CircEaseOut, this.MaxSize.Width, this.Duration, this.Delay);
}
public void Hide()
{
this.Control.Animate(new HorizontalFoldEffect(),
EasingFunctions.CircEaseOut, this.MinSize.Height, this.Duration, this.Delay);
this.Control.Animate(new VerticalFoldEffect(),
EasingFunctions.CircEaseIn, this.MinSize.Width, this.Duration, this.Delay);
}
}
上面的实现存在两个缺点:一是它没有提供取消机制,以防在 Hide()
方法调用完成之前调用 Show()
方法,反之亦然;二是效果持续时间未调整,因此如果我在 Show()
动画进行到一半时调用 Hide()
方法,执行该动画将需要目标持续时间的一半。
幸运的是,Animate()
返回一个 AnimationStatus
对象,可用于解决这些问题。
public class FoldAnimation
{
private List<StatelessAnimator.AnimationStatus> _cancellationTokens;
public Control Control { get; private set; }
public Size MaxSize { get; set; }
public Size MinSize { get; set; }
public int Duration { get; set; }
public int Delay { get; set; }
public FoldAnimation( Control control )
{
_cancellationTokens = new List<StatelessAnimator.AnimationStatus>();
this.Control = control;
this.MaxSize = control.Size;
this.MinSize = control.MinimumSize;
this.Duration = 1000;
this.Delay = 0;
}
public void Show()
{
int duration = this.Duration;
if( _cancellationTokens.Any( aS => !aS.IsCompleted ) )
{
//residue time
var token = _cancellationTokens.First( aS => !aS.IsCompleted );
duration = (int)( token.ElapsedMilliseconds );
}
this.CancelAllPerformingEffects();
var cT1 = this.Control.Animate( new HorizontalFoldEffect(),
EasingFunctions.CircEaseIn, this.MaxSize.Height, duration, this.Delay );
var cT2 = this.Control.Animate( new VerticalFoldEffect(),
EasingFunctions.CircEaseOut, this.MaxSize.Width, duration, this.Delay );
_cancellationTokens.Add( cT1 );
_cancellationTokens.Add( cT2 );
}
public void Hide()
{
int duration = this.Duration;
if( _cancellationTokens.Any( aS => !aS.IsCompleted ) )
{
//residue time
var token = _cancellationTokens.First( aS => !aS.IsCompleted );
duration = (int)( token.ElapsedMilliseconds );
}
this.CancelAllPerformingEffects();
var cT1 = this.Control.Animate( new HorizontalFoldEffect(),
EasingFunctions.CircEaseOut, this.MinSize.Height, duration, this.Delay );
var cT2 = this.Control.Animate( new VerticalFoldEffect(),
EasingFunctions.CircEaseIn, this.MinSize.Width, duration, this.Delay );
_cancellationTokens.Add( cT1 );
_cancellationTokens.Add( cT2 );
}
public void Cancel()
{
this.CancelAllPerformingEffects();
}
private void CancelAllPerformingEffects()
{
foreach (var token in _cancellationTokens)
token.CancellationToken.Cancel();
_cancellationTokens.Clear();
}
}
Using the Code
要将效果应用于您的控件,只需像这样调用 Animator.Animate
方法,或在控件上调用 Animate
扩展方法:
yourControl.Animate
(
new XLocationEffect(), //effect to apply implementing IEffect
EasingFunctions.BounceEaseOut, //easing to apply
321, //value to reach
2000, //animation duration in milliseconds
0 //delayed start in milliseconds
);
更新 2014/09/12
我错过了反转动画和(如果启用了反转)执行指定次数的循环的能力。这些功能在 1.2 版本中可用。
Animator.Animate
方法还有两个可选参数:
Reverse
:如果设置为true
,动画将达到目标值,然后播放回初始值。默认情况下,此参数设置为false
。Loops
:如果您将 reverse 设置为true
,那么您可以定义动画需要执行的循环次数。默认情况下,此参数设置为1
。如果设置为0
或负数,动画将无限循环。您可以使用CancellationToken
像往常一样停止动画。
历史
- 2014/09/12:动画反转和循环次数
- 2014/10/16:项目已创建