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

探索 UIElement 的 Clip 属性以在 WPF 中创建动画

starIconstarIconstarIconstarIconstarIcon

5.00/5 (28投票s)

2009年12月16日

CPOL

12分钟阅读

viewsIcon

72774

downloadIcon

1

基于 UIElement 的 Clip 属性在 WPF 中创建动画的技术

引言

动画由一系列帧组成。要执行动画,这些帧会以固定的时间间隔一个接一个地显示。在这里,我将展示几个基于WPF中UIelementClip属性的动画。每个UIelement类型对象都有一个clip属性。这里所有的动画都是使用UIelementclip属性完成的。通过这种方式,你可以做很多动画。我认为这是一种强大的动画制作方式。可能性是无限的。你可以使用这种技术在2D中完成大多数几何类型的动画。本文将展示以下动画,这些动画都是通过这种方式实现的:

  • 圆形动画
  • 径向动画
  • 隔行扫描动画
  • 块状动画
  • 瀑布动画

几何图形如何定义UIelement(如Image)的可见区域?

每个UIelement都有一个名为clipProperty,它是Geometry类型。你可以设置任何用于定义元素内容的轮廓的几何图形。只有在几何图形区域内的部分才会可见。UIelement超出几何图形的部分将在渲染的布局中被视觉上剪裁。在下图中,一个Image元素在没有定义clip区域和定义了clip区域时显示。

smallimage.jpg

图:没有Clip区域的Image

smallclip.jpg

图:带有椭圆Clip区域的Image

所有动画的基本思想

所有动画的主要主题都基于在固定时间间隔内改变UIelement的clip区域。单个或复合几何图形定义了UIelement的clip区域。因此,在固定时间间隔内改变几何图形或将新几何图形组合到现有几何图形中将产生动画的错觉。在这里,对于所有动画,几何图形定义了像Image这样的UIelement的可见区域。如果我们改变clip几何图形,我们将得到Image不同的可见区域。通过在固定时间间隔内改变clip几何图形,我们将得到一系列帧,看起来像动画。因此,这里所有的动画都基于单个或复合几何图形对象,这些对象在固定时间间隔内改变,以创造运动的错觉。

为什么WPF的基于属性的动画有时不起作用?

WPF有一个新的基于依赖属性的动画系统。每个基于属性的动画作用于一个单一的依赖属性,并根据时间线改变其值。根据MSDN,一个属性要具有动画能力,它必须满足以下三个要求:

  • 它必须是一个依赖属性。
  • 它必须属于一个继承自DependencyObject并实现IAnimatable接口的类。
  • 必须有一个兼容的动画类型可用。

然而,有些动画,仅改变依赖属性的值无法达到所需的动画效果。例如,使用基于属性的动画无法添加或删除任何新的几何对象/UI对象。在这些情况下,你必须使用dispatcher timer来制作动画,它在UI线程上运行。在本文中,只有圆形动画使用了基于属性的动画,而其余动画则使用了Dispatcher timer。

圆形动画

这种动画类似于Microsoft PowerPoint的圆形动画。你可以设置任何几何图形,如椭圆几何图形,来定义WPF UIelement类型的可见区域。为了实现这一点,每个WPF UIelement类型都有一个clip属性,你可以设置任何几何图形来进行裁剪。然而,对于圆形动画,我们需要圆形几何图形,但WPF没有内置的圆形几何图形。

circle1.gif

如何在WPF中从椭圆几何图形制作圆形几何图形?

椭圆几何图形由一个中心点、一个x半径和一个y半径定义。众所周知,椭圆几何图形有两个半径:radiousxradiousy。如果我们保持这两个半径值相同,它就会表现得像一个圆。我们只需一直保持Radiousx = Radiousy

这个动画效果是如何实现的?

这个技巧适用于任何UIelement类型。这里我以图像作为UIelement类型来实现圆形动画。我计算了图像的高度和宽度。然后将圆心设置为(width/2, height/2),以便从中心点开始动画。首先,我将圆的半径设置为0。然后按比例随时间递增半径。对于动画,我使用了双精度动画。代码如下:

class CircleAnimation
{
    public void MakeCircleAnimation(FrameworkElement animatedElement, 
    	double width, double height, TimeSpan timeSpan)
    {   
        EllipseGeometry ellipseGeometry = new EllipseGeometry();
        ellipseGeometry.RadiusX = 0;
        ellipseGeometry.RadiusY = 0;
        double centrex = width / 2;
        double centrey = height / 2;
        ellipseGeometry.Center = new Point(centrex, centrey);
        animatedElement.Clip = ellipseGeometry; //The most important line           
        double halfWidth = width / 2;
        double halfheight = height / 2;
        DoubleAnimation a = new DoubleAnimation();
        a.From = 0;
        a.To = Math.Sqrt(halfWidth * halfWidth + halfheight * halfheight); 
        a.Duration = new Duration(timeSpan);
        ellipseGeometry.BeginAnimation(EllipseGeometry.RadiusXProperty, a);
        ellipseGeometry.BeginAnimation(EllipseGeometry.RadiusYProperty, a);
    }
}

对于圆形动画,我使用了WPF的基于属性的动画,因为我不需要在运行时添加或删除任何对象。这里DoubleAnimation同时应用于EllipseGeometry.RadiusXPropertyEllipseGeometry.RadiusYProperty,并且x半径和y半径的值相等地增加,使其表现为圆形。接下来是如何增加半径。

裁剪圆的半径是如何计算的?

由于我正在对图像进行动画处理,它是矩形形状,如果一个圆想填满这个矩形,它的直径必须等于矩形的对角线。圆的半径将是矩形对角线的一半。因此,我应用了勾股定理来计算裁剪圆的半径,如下图所示。

py_update.jpg

图:用勾股定理计算裁剪圆的半径

径向动画(顺时针轮子,1辐条,Microsoft PowerPoint动画)

这种动画类似于Microsoft PowerPoint的“顺时针轮子,1辐条”动画。有些人也称这种动画为径向动画。这种动画从一条指向从中心到顶部中间的线开始,然后它顺时针旋转360度,使下方的对象可见。我知道一张图片比写很多字更能说明动画的样子。动画看起来像这样:

radial1.gif

这个动画效果是如何实现的?

我们知道,每个WPF UIelement类型都有一个clip属性,你可以设置任何几何图形来进行裁剪。我使用了这个clip属性来制作这个动画。我随时间改变了几何图形对象,使其看起来像一个轮子动画。用这种方式制作动画的所有挑战都在于随时间制作精确的几何图形对象。

我展示的是针对Image类型的动画,它是一个UIelement类型。然而,这个技巧适用于任何UIelement类型。这里使用了PathGeometry作为裁剪图像的几何图形。线段被动态地添加到路径几何图形中,以便随时间使可见区域变大。我计算了图像的高度和宽度。然后将第一条线段从(width/2, height/2)设置为(Width/2, 0)。然后,当需要时添加其他线段,并改变它们的点。对于这个动画,我使用了DispatcherTimer,它在UI线程上运行,而不是在工作线程上。代码如下:

class RadialAnimation
    {
        DispatcherTimer _timer = null;
        Ellipse _grdMain = null;
        ContentPresenter _circleContentPresenter = null;
        LineSegment LineSegment2 = null;
        PathFigure PathFigure1 = null;
        bool ISIncrementdirectionX = true;
        bool IsIncrementX = true;
        bool IsIncrementY = true;
        LineSegment LineSegmentFirstcorner = null;
        LineSegment LineSegmentseconcorner = null;
        LineSegment LineSegmentThirdcorner = null;
        LineSegment LineSegmentFourthcorner = null;
        double _ofsetOfAnimation = 1;
        double _height = 0;
        double _width = 0;
        public void MakeRadiaAnimation(FrameworkElement animatedElement, 
        	double width, double height, TimeSpan timeSpan)
        {
            _height = height;
            _width = width;
            double steps = 2 * (_height + _width);
            double tickTime = timeSpan.TotalSeconds / steps;
            string topCentre = Convert.ToString(_width / 2) + ",0";
            string centre = Convert.ToString(_width / 2) + 
            	"," + Convert.ToString(_height / 2);       
            PathGeometry PathGeometry1 = new PathGeometry();         
            PathFigure1 = new PathFigure();
            PathFigure1.StartPoint = 
		((Point)new PointConverter().ConvertFromString(centre));
            PathGeometry1.Figures.Add(PathFigure1);
            LineSegment LineSegmentdummy = new LineSegment();
            LineSegmentdummy.Point = 
		((Point)new PointConverter().ConvertFromString(topCentre));
            PathFigure1.Segments.Add(LineSegmentdummy);
            LineSegmentseconcorner = new LineSegment();
            LineSegmentseconcorner.Point = 
		((Point)new PointConverter().ConvertFromString(topCentre));
            PathFigure1.Segments.Add(LineSegmentseconcorner);
            LineSegmentThirdcorner = new LineSegment();
            LineSegmentThirdcorner.Point = 
		((Point)new PointConverter().ConvertFromString(topCentre));
            PathFigure1.Segments.Add(LineSegmentThirdcorner);
            LineSegmentFourthcorner = new LineSegment();
            LineSegmentFourthcorner.Point = 
		((Point)new PointConverter().ConvertFromString(topCentre));
            PathFigure1.Segments.Add(LineSegmentFourthcorner);
            LineSegmentFirstcorner = new LineSegment();
            LineSegmentFirstcorner.Point = 
		((Point)new PointConverter().ConvertFromString(topCentre));
            PathFigure1.Segments.Add(LineSegmentFirstcorner);
            LineSegment2 = new LineSegment();
            LineSegment2.Point = 
		((Point)new PointConverter().ConvertFromString(topCentre));
            PathFigure1.Segments.Add(LineSegment2);
            animatedElement.Clip = PathGeometry1;
            PointAnimationUsingKeyFrames pointAnimationUsingKeyFrames = 
            				new PointAnimationUsingKeyFrames();
            DiscretePointKeyFrame discretePointKeyFrame = new DiscretePointKeyFrame();
            discretePointKeyFrame.Value = new Point(0, 0);
            TimeSpan keyTime = new TimeSpan(0, 0, 1);
            discretePointKeyFrame.KeyTime = keyTime;
            _timer = new DispatcherTimer();
            _timer.Interval = TimeSpan.FromSeconds(tickTime);
            _timer.Tick += new EventHandler(_timer_Tick);
            _timer.IsEnabled = true;
        }

        void _timer_Tick(object sender, EventArgs e)
        {
            if ((LineSegment2.Point.X <= 0) && (LineSegment2.Point.Y <= 0))
            {
                LineSegmentFirstcorner.Point = 
                	new Point(LineSegment2.Point.X, LineSegment2.Point.Y);
                ISIncrementdirectionX = true;
                IsIncrementX = true;
            }
            else if (((LineSegment2.Point.X >= _width) 
            	&& (LineSegment2.Point.Y <= 0)))
            {
                LineSegmentseconcorner.Point = 
                	new Point(LineSegment2.Point.X, LineSegment2.Point.Y);
                LineSegmentThirdcorner.Point = 
                	new Point(LineSegment2.Point.X, LineSegment2.Point.Y);
                LineSegmentFourthcorner.Point = 
                	new Point(LineSegment2.Point.X, LineSegment2.Point.Y);
                LineSegmentFirstcorner.Point = 
                	new Point(LineSegment2.Point.X, LineSegment2.Point.Y);
                ISIncrementdirectionX = false;
                IsIncrementY = true;
            }

            else if ((LineSegment2.Point.X >= _width) 
            	&& (LineSegment2.Point.Y >= _height))
            {
                LineSegmentThirdcorner.Point = 
                	new Point(LineSegment2.Point.X, LineSegment2.Point.Y);
                LineSegmentFourthcorner.Point = 
                	new Point(LineSegment2.Point.X, LineSegment2.Point.Y);
                LineSegmentFirstcorner.Point = 
                	new Point(LineSegment2.Point.X, LineSegment2.Point.Y);
                ISIncrementdirectionX = true;
                IsIncrementX = false;
            }

            else if ((LineSegment2.Point.X <= 0) 
            	&& (LineSegment2.Point.Y >= _height))
            {
                LineSegmentFourthcorner.Point = 
                	new Point(LineSegment2.Point.X, LineSegment2.Point.Y);
                LineSegmentFirstcorner.Point = 
                	new Point(LineSegment2.Point.X, LineSegment2.Point.Y);
                ISIncrementdirectionX = false;
                IsIncrementY = false;
            }

            double x = 0, y = 0;

            if (ISIncrementdirectionX == true)
            {
                if (IsIncrementX)
                {
                    x = LineSegment2.Point.X + _ofsetOfAnimation;
                    y = LineSegment2.Point.Y;
                }
                else
                {
                    x = LineSegment2.Point.X - _ofsetOfAnimation;
                    y = LineSegment2.Point.Y;
                }
            }
            else
            {
                if (IsIncrementY)
                {
                    x = LineSegment2.Point.X;
                    y = LineSegment2.Point.Y + _ofsetOfAnimation;
                }
                else
                {
                    x = LineSegment2.Point.X;
                    y = LineSegment2.Point.Y - _ofsetOfAnimation;
                }
            }
            LineSegment2.Point = new Point(x, y);
            if ((LineSegment2.Point.X == _width / 2) 
            	&& (LineSegment2.Point.Y == 0))
            {
                _timer.IsEnabled = false;
            }
        }
    }

块状动画

block1.gif

这里使用路径几何图形作为UIelementImage)的clip几何图形。最初,路径几何图形是空的,所以在显示器上什么也看不到。如果你看块状动画,它看起来像使用了一组矩形来创建动画。这些矩形在固定时间间隔内动态添加到路径几何图形中。这里一次添加一个矩形,并且随机选择矩形的位置。因此,我将UIelement分割成了固定数量的单元格。

cell.jpg

我选择下一个要可见的单元格是随机的。然后使用以下公式将该单元格号转换为行号和列号:

Row number = (cell Number-1) / number of cells in a row +1 Column Number = 
	(cell Number-1) % number of cells in a row +1

这里每个正方形(矩形)都有相等的显式高度和宽度。然后,我使用以下公式制作了矩形:

new Rect( row number * Size, column number * Size, Size, Size); 

然后,我使用以下行执行了新创建的矩形与路径几何图形之间的并集组合操作:

pathGeometry = Geometry.Combine(pathGeometry, myRectGeometry2, 
				GeometryCombineMode.Union, null); 

最后,我将图像的clip几何图形重置为修改后的路径几何图形。我在dispatcher timer的每个tick处理程序中都执行了上述操作。因此,在每个tick中我都得到了不同的路径几何图形,并获得了动画。使用的代码如下:

class BlockAnimation
    {
        DispatcherTimer _timer = null;
        PathGeometry pathGeometry = null;
        double _rectangleSize = 80;
        double _numberofrectangles = 0;
        RandomNumberFromAGivenSetOfNumbers rdm = null;
        int _numberOfalreadyDrawnRectangles = 0;
        double _height = 0;
        double _width = 0;
        FrameworkElement _animatedElement = null;

        public void MakeBlockAnimation(FrameworkElement animatedElement, 
		double width, double height, TimeSpan timeSpan)
        {
            _animatedElement = animatedElement;
            _height = width;
            _width = height;
            if (_height > _width)
            {
                _numberofrectangles = Math.Ceiling(_height / _rectangleSize);
            }
            else
            {
                _numberofrectangles = Math.Ceiling(_width / _rectangleSize);
            }
            rdm = new RandomNumberFromAGivenSetOfNumbers(0, 
		Convert.ToInt32(_numberofrectangles * _numberofrectangles - 1));
            double steps = _numberofrectangles * _numberofrectangles;
            double tickTime = timeSpan.TotalSeconds / steps;
            pathGeometry = new PathGeometry();
            _animatedElement.Clip = pathGeometry;
            _timer = new DispatcherTimer(DispatcherPriority.Input);
            _timer.Interval = TimeSpan.FromSeconds(tickTime);
            _timer.Tick += new EventHandler(_timer_Tick);
            _timer.IsEnabled = true;
        }

        void _timer_Tick(object sender, EventArgs e)
        {
            int random = rdm.Next();
            int i = random / (int)_numberofrectangles;
            int j = random % (int)_numberofrectangles;
            RectangleGeometry myRectGeometry2 = new RectangleGeometry();
            myRectGeometry2.Rect = new Rect(j * _rectangleSize, 
		i * _rectangleSize, _rectangleSize, _rectangleSize);
            pathGeometry = Geometry.Combine(pathGeometry, 
		myRectGeometry2, GeometryCombineMode.Union, null);
            _animatedElement.Clip = pathGeometry;

            if (_numberOfalreadyDrawnRectangles == 
		_numberofrectangles * _numberofrectangles)
            {
                _timer.IsEnabled = false;
            }
            _numberOfalreadyDrawnRectangles++;
        }
    }

为了从特定范围内获取随机数,我创建了一个类并在这里使用它。 .NET Framework中定义的Random类提供了生成伪随机数的功能。根据MSDN,Random类的当前实现基于Donald E. Knuth的减法随机数生成器算法。随机数生成从种子值开始。如果反复使用相同的种子,将生成相同的数字序列。产生不同序列的一种方法是使种子值依赖于时间,从而在每次新创建Random实例时产生不同的序列。然而,.NET Framework当前的Random类实现不提供以下功能:从一系列数字中选取随机数而不重复:当前实现返回范围内的随机数,但可能包含重复数字。例如,你想要从范围(4到21)中选取随机数。你希望一次随机地从该范围内选取一个数字,而不重复或重复。然而,通过使用.NET Framework的Random类,这种功能很容易实现。我需要这个功能来实现这个动画。使用的代码如下:

class RandomNumberFromAGivenSetOfNumbers
    {
        List<int> _setOfNumbers = new List<int>();
        Random _random = new Random();
        public RandomNumberFromAGivenSetOfNumbers(int min, int max)
        {
            for (int i = min; i <= max; i++)
            {
                _setOfNumbers.Add(i);
            }
        }

        public int Next()
        {
            if (_setOfNumbers.Count > 0)
            {
                int nextNumberIndex = _random.Next(_setOfNumbers.Count);
                int val = _setOfNumbers[nextNumberIndex];
                _setOfNumbers.RemoveAt(nextNumberIndex);
                return val;
            }
            return -1;
        }
    }

隔行扫描动画

当隔行扫描动画应用于图像或任何UIelement时,图像/UIelement将分多次绘制,而不是一次全部绘制。第一次扫描跳过一定数量的行,然后在连续的扫描中填充之前跳过的行。我知道一张图片比写很多字更能说明动画的样子。动画看起来像这样:

in1.gif

现在我将讨论我如何用WPF实现隔行扫描动画。这个动画与显示器隔行扫描无关。这里我是在WPF中模拟显示器隔行扫描过程。为了实现隔行扫描动画,我使用了一组WPF的矩形几何图形。我们知道,在隔行扫描动画的第一遍中,我从顶部和底部绘制了一定数量的短高矩形来填充图像,每个矩形之间跳过一行的高度。然后在隔行扫描动画的第二遍中,我从中心开始,以向顶部和底部方向再次绘制一定数量的短矩形。这次,代码在两个连续的小矩形之间没有留下任何空白行。这里使用路径几何图形作为clip几何图形。最初,这个路径几何图形是空的。在第一遍中,我通过组合联合操作,在固定时间间隔内,从上到下向路径几何图形添加了一定数量的小矩形,每个矩形之间跳过一行高度来填充图像。在第二遍中,我通过组合联合操作,在从中心开始的顶部和底部方向上,在固定时间间隔内向路径几何图形添加了另一组小矩形。代码如下:

class InterlacedAnimation
    {
        DispatcherTimer _timer = null;
        PathGeometry pathGeometry = null;
        double _rectangleSize = 3;
        double _startingCoordinate = 0;
        double _endcoordinate = 0;
        FrameworkElement _animatedElement = null;
        double _width = 0;
        double _height = 0;
        double _offset = 0.5;
        bool IsTimerShouldBeStoppedForUp = false;
        bool IsTimerShouldBeStoppedForDown = false;

        public void MakeInterlacedAnimation(FrameworkElement animatedElement, 
		double width, double height, TimeSpan timeSpan)
        {
            double steps =  (height / (_rectangleSize + _offset));
            double tickTime =  timeSpan.TotalSeconds / steps;
            _animatedElement = animatedElement;
            _width = width;
            _height = height;
            _endcoordinate = height;
            pathGeometry = new PathGeometry();
            animatedElement.Clip = pathGeometry;
            _timer = new DispatcherTimer();
            _timer.Interval = TimeSpan.FromSeconds(tickTime);
            _timer.Tick += new EventHandler(_timer_Tick);
            _timer.IsEnabled = true;
        }
        void _timer_Tick(object sender, EventArgs e)
        {
            if (IsTimerShouldBeStoppedForUp == true && 
		IsTimerShouldBeStoppedForDown == true)
            {
                _timer.IsEnabled = false;
            }
            if (_startingCoordinate == _endcoordinate)
            {
                _offset = 0;
            }
            if (_startingCoordinate >= (_height / 2 - 5))
                _offset = 0;
            if (_endcoordinate <= (_height / 2 - 5))
                _offset = 0;
            if (_startingCoordinate < _height)
            {
                RectangleGeometry myRectGeometry2 = new RectangleGeometry();
                myRectGeometry2.Rect = new Rect(0, _startingCoordinate, 
					_width, _rectangleSize);
                pathGeometry = Geometry.Combine(pathGeometry, 
			myRectGeometry2, GeometryCombineMode.Union, null);
            }
            else
            {
                IsTimerShouldBeStoppedForUp = true;
            }
            if (_endcoordinate > 0)
            {
                RectangleGeometry myRectGeometry3 = new RectangleGeometry();
                myRectGeometry3.Rect = 
		new Rect(0, _endcoordinate, _width, _rectangleSize);
                pathGeometry = Geometry.Combine(pathGeometry, 
			myRectGeometry3, GeometryCombineMode.Union, null);
            }
            else
            {
                IsTimerShouldBeStoppedForDown = true;
            }
            _startingCoordinate = _startingCoordinate + _rectangleSize + _offset;
            _endcoordinate = _endcoordinate - _rectangleSize - _offset;
            _animatedElement.Clip = pathGeometry;
        }
    }

由于我是在运行时动态添加一定数量的小矩形,所以基于属性的动画在这里不起作用。因此,我使用了基于dispatcher timer的动画。我在这里使用的是dispatcher timer而不是传统的Timer,因为dispatcher timer在UI线程上运行,而我不需要编写线程切换代码。这个技巧适用于任何UIelement类型。这里我以图像作为UIelement类型来实现隔行扫描动画。我计算了图像的高度和宽度。使用Geometry类的Combine方法,我们可以使用指定的geometryCombineMode组合两个几何图形。GeometryCombineMode可以是UnionIntersectExcludeXOR。我通过执行联合操作将所有小矩形添加到了路径几何图形中。

瀑布动画

我知道一张图片比写很多字更能说明瀑布动画的样子。动画看起来像这样:

waterfall.gif

我们知道,每个WPF UIelement类型都有一个clip属性,你可以设置任何几何图形来进行裁剪。我使用了这个clip属性来制作这个动画。我随时间改变了几何图形对象,使其看起来像瀑布动画。用这种方式制作动画的所有挑战都在于随时间制作精确的几何图形对象。在瀑布动画中,绘制一定数量的小矩形来填充宽度,并留有一定的间隙,这些矩形垂直向下移动。矩形的高度随机取值在0到25之间,并且特定矩形高度的随机值随时间改变。实现此动画的主要主题是clip几何图形。由于我是在运行时更改clip几何图形,所以基于属性的动画在这里不起作用。因此,我使用了基于dispatcher timer的动画。我在这里使用的是dispatcher timer而不是传统的Timer,因为dispatcher timer在UI线程上运行,而我不需要编写线程切换代码。下面的C#代码用于实现滚动动画:

 class WaterFallAnimation
    {
        DispatcherTimer _timer = null;
        PathGeometry pathGeometry = null;
        double _rectangleSize = 10 + 50;
        double _offset = 5;
        int _waterFallHeight = 50;
        double _width = 0;
        double _height = 0;
        Random random = new Random();
        FrameworkElement _animatedElement = null;
        RectangleGeometry myRectGeometry2 = null;
        PathGeometry pathGeometry2 = null;
        RectangleGeometry myRectGeometry5 = null;        

        public void MakeWaterFallAnimation(FrameworkElement animatedElement, 
			double width, double height, TimeSpan timeSpan)
        {
            _animatedElement = animatedElement;
            _height = height;
            _width = width;
            myRectGeometry2 = new RectangleGeometry();
            pathGeometry2 = new PathGeometry();
            myRectGeometry5 = new RectangleGeometry();       
            TranslateTransform translateTransform = new TranslateTransform();
            translateTransform.X = 0;
            translateTransform.Y = 10;
            double steps = (_height / _offset);
            double tickTime = timeSpan.TotalSeconds / steps;
            pathGeometry = new PathGeometry();
            animatedElement.Clip = pathGeometry;
            _timer = new DispatcherTimer(DispatcherPriority.Input);
            _timer.Interval = TimeSpan.FromSeconds(tickTime);
            _timer.Tick += new EventHandler(_timer_Tick);
            _timer.IsEnabled = true;
        }

        void _timer_Tick(object sender, EventArgs e)
        {
            myRectGeometry2.Rect = new Rect(0, 0, _width, _rectangleSize);
            pathGeometry2 = Geometry.Combine
		(pathGeometry2, myRectGeometry2, GeometryCombineMode.Union, null);
            for (int i = 1; i <= _width; i = i + 2)
            {
                myRectGeometry5.Rect = new Rect(new Point(i, _rectangleSize), 
		new Point(i + 2, _rectangleSize - random.Next(0, _waterFallHeight)));
                pathGeometry2 = Geometry.Combine(pathGeometry2, 
		myRectGeometry5, GeometryCombineMode.Exclude, null);
            }
            _animatedElement.Clip = pathGeometry2;
            if (_rectangleSize == _height + _waterFallHeight)
            {
                _timer.IsEnabled = false;
            }
            _rectangleSize += _offset;
        }
    }

这里我使用路径几何图形作为Image的clip几何图形。这个路径几何图形是这个动画的基础。myRectGeometry2用于在没有雨水的情况下显示图像的特定部分。然后,在myRectGeometry2和路径几何图形之间执行联合操作。在每次timer tick时,我都会增加myRectGeometry2的高度。然后,我使用一个for循环绘制一定数量的宽度为2、高度随机的小矩形。这些矩形是用两个点绘制的。在for循环中,我创建了Rectangle Geometry myRectGeometry5并执行了与路径几何图形的联合操作。然后,将图像的clip属性重置为修改后的路径几何图形。通过这种方式,我在dispatcher timer的每次tick中都获得了修改后的路径几何图形,并使用该路径几何图形获得了这个动画。

已知问题

这里大多数动画都是使用dispatcher timer完成的,它不能保证在精确的时间触发,并且在UI线程上运行。要使动画在时间上可控,你可以使用dispatcher timer来调整每一步的变化(偏移量)。另一方面,要使动画时间可控,你可以使用System.Timer而不是dispatcher timer,它保证在精确的时间执行,并且在不同的线程而不是UI线程上运行。如果你使用System.Timer,你必须执行跨线程操作来更新UI。为了保持代码简单,我在这里没有实现System.Timer

结论

感谢阅读本文。希望这能为您节省一些时间。如果您有任何问题,我很乐意回答。我一直很欣赏评论。如果您发现任何问题,请告知我。我将尽我所能解决这些问题。

历史

  • 初始发布 – 2009/12/30
© . All rights reserved.