65.9K
CodeProject 正在变化。 阅读更多。
Home

Winforms 中的控件动画

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (82投票s)

2014年10月21日

CPOL

6分钟阅读

viewsIcon

127718

downloadIcon

7068

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

引用:提升 Windows Forms 应用性能的实用技巧

有几个属性决定了控件的大小或位置:Width、Height、Top、Bottom、Left、Right、Size、Location 和 Bounds。先设置 Width 再设置 Height 会产生双倍的工作量,而不是通过 Size 一次性设置两者。

作用于单个属性的效果是通用的,但它不能针对与其他效果协同工作进行优化。如果您同时应用许多效果,可能会出现闪烁或不平滑的动画。闪烁是否明显取决于您混合效果的方式以及效果的表现。

因此,有时最好编写一个自定义的、更专业的效果来优化绘制。假设我们希望控件底部固定,顶部高度随之改变;我们可以组合两个效果,一个作用于 Height 属性,一个作用于 Top 属性。这种方法可行,但不是最优的。要高效地实现我们的目标,我们需要同时操作 HeightTop 属性。

    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:项目已创建
© . All rights reserved.