上一篇几乎都在说doubleanimation的应用,这篇说说pointanimation。
1. 使用pointanimation使用pointanimation可以让shape变形,但实际上没看到多少人会这么用,毕竟wpf做的软件多数不需要这么花俏。
1.1 在xaml上使用pointanimation<storyboard x:name="storyboard2" repeatbehavior="forever" autoreverse="true" duration="0:0:4"><pointanimation storyboard.targetproperty="(path.data).(pathgeometry.figures)[0].(pathfigure.startpoint)" storyboard.targetname="path2" to="0,0" enabledependentanimation="true" /><pointanimation storyboard.targetproperty="(path.data).(pathgeometry.figures)[0].(pathfigure.segments)[0].(linesegment.point)" storyboard.targetname="path2" to="100,0" enabledependentanimation="true" /><coloranimation to="#ff85c82e" storyboard.targetproperty="(shape.fill).(solidcolorbrush.color)" storyboard.targetname="path2" /></storyboard>…<path margin="0,20,0,0" x:name="path2" fill="greenyellow"><path.data><pathgeometry><pathfigure startpoint="50,0"><linesegment point="50,0" /><linesegment point="0,100" /><linesegment point="0,100" /><linesegment point="100,100" /><linesegment point="100,100" /></pathfigure></pathgeometry></path.data></path>
在这个例子里最头痛的地方是property-path 语法,如果不能熟记的话最好依赖blend生成。
1.2 在代码中使用pointanimation如果point数量很多,例如图表,通常会在c#代码中使用pointanimation:
_storyboard = new storyboard();
random random = new random();for (int i = 0; i < _pathfigure.segments.count; i++)
{var animation = new pointanimation { duration = timespan.fromseconds(3) };
storyboard.settarget(animation, _pathfigure.segments[i]);
storyboard.settargetproperty(animation, "(linesegment.point)");
animation.enabledependentanimation = true;
animation.easingfunction = new quarticease { easingmode = easingmode.easeout };
animation.to = new point((_pathfigure.segments[i] as linesegment).point.x, (i % 2 == 0 ? 1 : -1) * i * 1.2 + 60);
_storyboard.children.add(animation);
}
_storyboard.begin();
因为可以直接settarget,所以property-path语法就可以很简单。
2. 扩展pointanimation上面两个例子的动画都还算简单,如果更复杂些,xaml或c#代码都需要写到很复杂。我参考了这个网页 想做出类似的动画,但发现需要写很多xaml所以放弃用pointanimation实现。这个页面的动画核心是这段html:
<polygon fill="#ffd41d" points="97.3,0 127.4,60.9 194.6,70.7 145.9,118.1 157.4,185.1 97.3,153.5 37.2,185.1 48.6,118.1 0,70.7 67.2,60.9">
<animate id="animation-to-check" begin="indefinite" fill="freeze" attributename="points" dur="500ms" to="110,58.2 147.3,0 192.1,29 141.7,105.1 118.7,139.8 88.8,185.1 46.1,156.5 0,125 23.5,86.6 71.1,116.7"/>
<animate id="animation-to-star" begin="indefinite" fill="freeze" attributename="points" dur="500ms" to="97.3,0 127.4,60.9 194.6,70.7 145.9,118.1 157.4,185.1 97.3,153.5 37.2,185.1 48.6,118.1 0,70.7 67.2,60.9"/> </polygon>
只需一组point的集合就可以控制所有point的动画,确实比pointanimation高效很多。 在wpf中可以通过继承timeline实现一个pointcollectionanimamtion,具体可以参考这个项目。可惜的是虽然uwp的timeline类并不封闭,但完全不知道如何继承并派生一个自定义的animation。
这时候需要稍微变通一下思维。可以将doubleanimation理解成这样:storyboard将timespan传递给doubleanimation,doubleanimation通过这个timespan(有时还需要结合easingfunction)计算出目标属性的当前值最后传递给目标属性,如下图所示:
既然这样,也可以接收到这个计算出来的double,再通过converter计算出目标的pointcollection值:
假设告诉这个converter当传入的double值(命名为progress)为0的时候,pointcollection是{0,0 1,1 …},progress为100时pointcollection是{1,1 2,2 …},当progress处于其中任何值时的计算方法则是:
private pointcollection getcurrentpoints(pointcollection frompoints, pointcollection topoints, double percentage)
{var result = new pointcollection();for (var i = 0;
i < math.min(frompoints.count, topoints.count);
i++)
{
var x = (1 - percentage / 100d) * frompoints[i].x + percentage / 100d * topoints[i].x;
var y = (1 - percentage / 100d) * frompoints[i].y + percentage / 100d * topoints[i].y;
result.add(new point(x, y));
}return result;
}
这样就完成了从timespan到pointcollection的转换过程。然后就是定义在xaml上的使用方式。参考上面pointcollectionanimation,虽然多了个converter,但xaml也应该足够简洁:
<local:progresstopointcollectionbridge x:name="progresstopointcollectionbridge"><pointcollection>97.3,0 127.4,60.9 194.6,70.7 145.9,118.1 157.4,185.1 97.3,153.5 37.2,185.1 48.6,118.1 0,70.7 67.2,60.9</pointcollection><pointcollection>110,58.2 147.3,0 192.1,29 141.7,105.1 118.7,139.8 88.8,185.1 46.1,156.5 0,125 23.5,86.6 71.1,116.7</pointcollection></local:progresstopointcollectionbridge><storyboard x:name="storyboard1" fillbehavior="holdend"><doubleanimation duration="0:0:2" to="100" fillbehavior="holdend" storyboard.targetproperty="(local:progresstopointcollectionbridge.progress)" storyboard.targetname="progresstopointcollectionbridge" enabledependentanimation="true"/></storyboard>…<polygon x:name="polygon" points="{binding source={staticresource progresstopointcollectionbridge},path=points}" stroke="darkolivegreen" strokethickness="2" height="250" width="250" stretch="fill" />
最终我选择了将这个converter命名为progresstopointcollectionbridge。可以看出polygon 将points绑定到progresstopointcollectionbridge,doubleanimation 改变progresstopointcollectionbridge.progress,从而改变points。xaml的简洁程度还算令人满意,如果需要操作多个点的话相对于pointanimation的优势就很大。
运行结果如下:
完整的xaml:
<usercontrol.resources><local:progresstopointcollectionbridge x:name="progresstopointcollectionbridge"><pointcollection>97.3,0 127.4,60.9 194.6,70.7 145.9,118.1 157.4,185.1 97.3,153.5 37.2,185.1 48.6,118.1 0,70.7 67.2,60.9</pointcollection><pointcollection>110,58.2 147.3,0 192.1,29 141.7,105.1 118.7,139.8 88.8,185.1 46.1,156.5 0,125 23.5,86.6 71.1,116.7</pointcollection></local:progresstopointcollectionbridge><storyboard x:name="storyboard1" fillbehavior="holdend"><doubleanimation duration="0:0:2" to="100" fillbehavior="holdend" storyboard.targetproperty="(local:progresstopointcollectionbridge.progress)" storyboard.targetname="progresstopointcollectionbridge" enabledependentanimation="true"><doubleanimation.easingfunction><elasticease easingmode="easeinout" /></doubleanimation.easingfunction></doubleanimation><coloranimation duration="0:0:2" to="#ff48f412" storyboard.targetproperty="(shape.fill).(solidcolorbrush.color)" storyboard.targetname="polygon" d:isoptimized="true"><coloranimation.easingfunction><elasticease easingmode="easeinout" /></coloranimation.easingfunction></coloranimation></storyboard></usercontrol.resources><grid x:name="layoutroot" background="white"><polygon x:name="polygon" points="{binding source={staticresource progresstopointcollectionbridge},path=points}" stroke="darkolivegreen" strokethickness="2" height="250" width="250" stretch="fill" fill="#ffebf412" /></grid>
progresstopointcollectionbridge:
[contentproperty(name = nameof(children))]public class progresstopointcollectionbridge : dependencyobject
{public progresstopointcollectionbridge()
{
children = new observablecollection<pointcollection>();
}/// <summary>/// 获取或设置points的值/// </summary>public pointcollection points
{get { return (pointcollection) getvalue(pointsproperty); }set { setvalue(pointsproperty, value); }
}/// <summary>/// 获取或设置progress的值/// </summary>public double progress
{get { return (double) getvalue(progressproperty); }set { setvalue(progressproperty, value); }
}/// <summary>/// 获取或设置children的值/// </summary>public collection<pointcollection> children
{get { return (collection<pointcollection>) getvalue(childrenproperty); }set { setvalue(childrenproperty, value); }
}protected virtual void onprogresschanged(double oldvalue, double newvalue)
{updatepoints();
}protected virtual void onchildrenchanged(collection<pointcollection> oldvalue, collection<pointcollection> newvalue)
{var oldcollection = oldvalue as inotifycollectionchanged;if (oldcollection != null)
oldcollection.collectionchanged -= onchildrencollectionchanged;var newcollection = newvalue as inotifycollectionchanged;if (newcollection != null)
newcollection.collectionchanged += onchildrencollectionchanged;updatepoints();
}private void onchildrencollectionchanged(object sender, notifycollectionchangedeventargs e)
{updatepoints();
}private void updatepoints()
{if (children == null || children.any() == false)
{
points = null;
}else if (children.count == 1)
{var frompoints = new pointcollection();for (var i = 0; i < children[0].count; i++)
frompoints.add(new point(0, 0));var topoints = children[0];
points = getcurrentpoints(frompoints, topoints, progress);
}else{var rangepersection = 100d / (children.count - 1);var fromindex = math.min(children.count - 2, convert.toint32(math.floor(progress / rangepersection)));
fromindex = math.max(fromindex, 0);var toindex = fromindex + 1;
pointcollection frompoints;if (fromindex == toindex)
{
frompoints = new pointcollection();for (var i = 0; i < children[0].count; i++)
frompoints.add(new point(0, 0));
}else{
frompoints = children.elementat(fromindex);
}var topoints = children.elementat(toindex);
var percentage = (progress / rangepersection - fromindex) * 100;
points = getcurrentpoints(frompoints, topoints, percentage);
}
}private pointcollection getcurrentpoints(pointcollection frompoints, pointcollection topoints, double percentage)
{var result = new pointcollection();for (var i = 0;
i < math.min(frompoints.count, topoints.count);
i++)
{
var x = (1 - percentage / 100d) * frompoints[i].x + percentage / 100d * topoints[i].x;
var y = (1 - percentage / 100d) * frompoints[i].y + percentage / 100d * topoints[i].y;
result.add(new point(x, y));
}return result;
}#region dependencyproperties#endregion}
3. 结语如果将doubleanimation说成“对目标的double属性做动画”,那pointanimation可以说成“对目标的point.x和point.y两个double属性同时做动画”,coloranimation则是“对目标的color.a、r、g、b四个int属性同时做动画”。这样理解的话pointanimation和coloranimation只不过是doubleanimation的延伸而已,进一步的说,通过doubleanimation应该可以延伸出所有类型属性的动画。不过我并不清楚怎么在uwp上自定义动画,只能通过本文的折衷方式扩展。虽然xaml需要写复杂些,但这样也有它的好处:
不需要了解太多animation相关类的知识,只需要有依赖属性、绑定等基础知识就够了。
不会因为动画api的改变而更改,可以兼容wpf、silverlight和uwp(大概吧,我没有真的在wpf上测试这些代码)。
代码足够简单,省去了计算timespan及easingfunction的步骤。 稍微修改下还可以做成泛型的animationbridge 808ed36a4929ba137db2b9ee76c79186,提供pointcollection以外数据类型的支持。
结合上一篇文章再发散一下,总觉得将来遇到什么uwp没有提供的功能都可以通过变通的方法实现,binding和dependencyproperty真是uwp开发者最好的朋友。
4. 参考how svg shape morphing works
gadal metasyllabus
以上就是用shape做动画的实例详解的详细内容。