2年前我在玩Flex的时候就一直有一个疑问,就是如何来实现一个蚊香慢慢烧完的Loading动画呢?
刚经历了某甲方高强度一个月的洗礼后,这几天刚好闲下来,这个问题又浮现在我脑海里。于是经过几番思索纠结后,我发现了一个更好玩的效果,如下:
1.总体思路
下面我就来分析一下实现思路。
仔细观察下,
1:遍历出所有的Path。
2:把各个Path分解成各种点。
3:依次对各个点执行PointAnimation动画(直线执行PointAnimation,曲线执行PointAnimationUsingPath)。
2.详细设计
首先,我们得画一个路径作为动画的样本路径。
1、准备萌妹子一枚
用打开后,转成路径,如下:
参数:
确定后得到
可以手动删除一些无用的路径。
然后导出路径
打开导出的Xaml将Path都Copy出来用。
2、遍历出萌妹子的各种轮廓
这里我之前写过一个遍历一个对象的可视树下所有的某类型的子对象的方法(wpf移植过来的为了适应,临时小改了一下)。
List<Path> list = new List<Path>(); /// <summary> /// 遍历某对象下某类型的所有子元素 /// </summary> /// <param name="myVisual">遍历的对象</param> /// <param name="type">子元素的类型</param> public void EnumVisual(DependencyObject myVisual, Type type) { for (int i = 0; i < VisualTreeHelper.GetChildrenCount(myVisual); i++) { DependencyObject childVisual = (DependencyObject)VisualTreeHelper.GetChild(myVisual, i); if (childVisual != null) { if (childVisual.GetType() == type) { list.Add(childVisual as Path); } EnumVisual(childVisual, type); } } return; }
然后
EnumVisual(Group, typeof(Path));
其中Group为包含所有Path的容器,这样list里就充满了萌妹子的轮廓。
3、把每一条Path都分割成各种点的表示方式
这里我们先来了解一下Path的路径标记语法。其实,Path的线段是有两种表示方法的,如下:
第一种:
<Path Data="M96,200 L176,288 C176,288 272,232 248,208" Fill="#FFF8F8F8" Stroke="Black"/>
第二种:
<Path x:Name="path" Fill="#FFF8F8F8" Stroke="Black"> <Path.Data> <PathGeometry> <PathFigure StartPoint="96,200"> <LineSegment Point="176,288"/> <BezierSegment Point3="248,208" Point2="272,232" Point1="176,288"/> </PathFigure> </PathGeometry> </Path.Data> </Path>
这两段代码表示的是同样的路径。其中M=StartPoint,L=LineSegment,C=BezierSegment,LineSegment的Point属性指的是线段的终结点,BezierSegment的Point3指的是曲线的终结点,Point1和Point2分别指的是起始点控制点和结束点控制点的位置,就是用钢笔工具画曲线时拖动曲度的那两个点。当然了,每个线段的起始点为上一个线段的终结点。迷你表示法还有大小写分别表示绝对位置与相对位置,更多说明请参考下面链接。
详情请参考:http://msdn.microsoft.com/zh-cn/library/ms752293.aspx
我们遍历出来的Path其实都是用第一种表示方法表示的,这样不方便我们后台来分解解析。我们得把第一种表示方法装换为第二种。这里我们得借用国外大侠分享的类:
http://stringtopathgeometry.codeplex.com/
这个东西可以很方便的实现这两种格式的转换,如下:
StringToPathGeometryConverter _s = new StringToPathGeometryConverter(); PathGeometry PG = _s.Convert(path.Data.ToString());
这样转换出来的PG就是第二种表示方法了。
通过遍历第二种表示方法里面PathFigure的子项就可以知道点与点之间的关系了,依次在点与点之间执行PointAnimation和PointAnimationUsingPath让终结点从起始点运动到结束位置,就实现划线的效果了。
曲线的话必须得用PointAnimationUsingPath来执行才显得自然一些,当然了,silverlight里是没有PointAnimationUsingPath动画的,所以得借助某外国达人写的PointAnimationUsingPath类,用法和WPF里差不多,不过个人觉得可以在Animation里直接来设置target和targetProperty还有Begin方法,这一点比微软写的更好用。
详情请参看:http://www.codeproject.com/Articles/30819/Animation-Along-a-Path-for-Silverlight
好了,直接上代码:
/// <summary> /// 存储遍历到的Path样本 /// </summary> List<Path> list = new List<Path>(); /// <summary> /// Path样本索引 /// </summary> int pathNum = 0; /// <summary> /// 新绘制的路径的集合 /// </summary> List<Path> drawPathList = new List<Path>(); /// <summary> /// 点集合索引 /// </summary> int num = 0; /// <summary> /// 当前绘制路径的样本Path /// </summary> PathGeometry PG; /// <summary> /// 当前绘制路径中的点集合 /// </summary> PathFigure PF; /// <summary> /// 当前绘制路径中的结束点 /// </summary> Point endPoint;
/// <summary> /// 开始绘制一条路径 /// </summary> /// <param name="path">路径样本</param> private void PathPlay(Path path) { Path ph = new Path();//当前绘制的路径 drawPathList.Add(ph); ph.Stroke = new SolidColorBrush(Colors.Black); drawGrid.Children.Add(ph);//添加进画布 PathGeometry thisPG = new PathGeometry();//动画路径的数据 ph.Data = thisPG; StringToPathGeometryConverter _s = new StringToPathGeometryConverter(); PG = _s.Convert(path.Data.ToString());//读取样本路径,分解段 PF = new PathFigure();//创建集合 endPoint = PF.StartPoint = PG.Figures[0].StartPoint;//设置起始点,一开始的终结点为起始点 thisPG.Figures.Add(PF); Play();//开始绘制分段的路径 } /// <summary> /// 绘制路径的一段 /// </summary> private void Play() { try { if (pathNum >= list.Count)//画完所有的路径 { FillColor();//开始填充颜色 Group.Visibility = Visibility.Visible; return; } else if (num >= PG.Figures[0].Segments.Count)//画完一条线 { if (pathNum < list.Count) { num = 0; PathPlay(list[pathNum++] as Path);//播放完毕就播放下一条线 return; } } PathSegment item = PG.Figures[0].Segments[num++];//读取下一个点 if (item.ToString().Contains("Line"))//如果这个点是直线 { LineSegment _ls = new LineSegment(); _ls.Point = endPoint; PathSegment PS = _ls;//创建一条直线的初始状态点 PointAnimation PA = new PointAnimation();//动画到读取的点的位置 PA.To=(item as LineSegment).Point; PA.Duration = new Duration(TimeSpan.FromMilliseconds(50)); PA.Completed += new EventHandler((sender1, e1) =>//播放完毕后进行递归,绘制下一条线 { Play(); }); PF.Segments.Add(PS);//添加点 //PS.BeginAnimation(LineSegment.PointProperty, PA); Storyboard sb = new Storyboard(); sb.Children.Add(PA); Storyboard.SetTarget(PA, PS); Storyboard.SetTargetProperty(PA, new PropertyPath("Point")); sb.Begin();//开始动画 endPoint = (item as LineSegment).Point;//记录终结点 } else if (item.ToString().Contains("Bezier"))//如果这个点是贝尔曲线 { BezierSegment _bs = new BezierSegment(); _bs.Point1 = _bs.Point2 = _bs.Point3 = endPoint; PathSegment PS = _bs; PointAnimationUsingPath PA = new PointAnimationUsingPath();//创建终结点的路径动画,曲线要严格按照路径来运动 PA.Target = PS; PA.TargetProperty = new PropertyPath("Point3"); PA.Duration = TimeSpan.FromMilliseconds(50); //生成动画的路径形状 PathGeometry newPG = new PathGeometry(); PathFigure newPF = new PathFigure();//创建集合 newPF.StartPoint = endPoint;//s设置起始点和每次动画的种植点 newPG.Figures.Add(newPF); BezierSegment _bs1 = new BezierSegment(); _bs1.Point1 = (item as BezierSegment).Point1; _bs1.Point2 = (item as BezierSegment).Point2; _bs1.Point3 = (item as BezierSegment).Point3; newPF.Segments.Add(_bs1); PA.PathGeometry = newPG; PA.Completed += new EventHandler((sender1, e1) => { Play(); }); PA.Begin(); //同样对控制点也要进行一般的动画 PointAnimation PA1 = new PointAnimation(); PA1.To=(item as BezierSegment).Point1; PA1.Duration=new Duration(TimeSpan.FromMilliseconds(500)); PointAnimation PA2 = new PointAnimation(); PA2.To=(item as BezierSegment).Point2; PA2.Duration=new Duration(TimeSpan.FromMilliseconds(500)); PF.Segments.Add(PS); //PS.BeginAnimation(BezierSegment.Point3Property, PA); //PS.BeginAnimation(BezierSegment.Point1Property, PA1); //PS.BeginAnimation(BezierSegment.Point2Property, PA2); Storyboard sb = new Storyboard(); //sb.Children.Add(PA); //Storyboard.SetTarget(PA, PS); //Storyboard.SetTargetProperty(PA, new PropertyPath("Point")); //sb.Begin();//开始动画 endPoint = (item as BezierSegment).Point3; } } catch { } }
方法里用了各种递归是因为处理完一条Path的所有动画后执行下一条Path的动画,而每一条Path里的每一小段也得依次处理, 要让一序列的动画依次播放,得在动画播放完毕后再播放下一段动画,各位大虾有没有更好的方法来依次播放一序列动画呢??
后记
原版是Wpf的,wpf自带了PointAnimationUsingPath动画,所以实现起来代码少得多了。接下来我打算优化后把它封成一个行为,方便以后使用。
文中出现了这么多外国牛人的文章,当然了以小弟的强烈爱国情怀是无法完全理解,所以特别谢http://www.cnblogs.com/beniao/archive/2010/05/26/1736446.html。
觉得本文还可以的话要点击下面的推荐哦喵~