您好,欢迎访问一九零五行业门户网

用Shape做动画实例代码

相对于wpf/silverlight,uwp的动画系统可以说有大幅提高,不过本文无意深入讨论这些动画api,本文将介绍使用shape做一些进度、等待方面的动画,除此之外也会介绍一些相关技巧。
1. 使用strokedashoffset做等待提示动画圆形的等待提示动画十分容易做,只要让它旋转就可以了:
但是圆形以外的形状就不容易做了,例如三角形,总不能让它单纯地旋转吧:
要解决这个问题可以使用strokedashoffset。strokedashoffset用于控制虚线边框的第一个短线相对于shape开始点的位移,使用动画控制这个数值可以做出边框滚动的效果:
<page.resources><storyboard x:name="progressstoryboard"><doubleanimationusingkeyframes enabledependentanimation="true" storyboard.targetproperty="(shape.strokedashoffset)" storyboard.targetname="triangle"><easingdoublekeyframe keytime="0:1:0" value="-500" /></doubleanimationusingkeyframes></storyboard></page.resources><grid background="#ffcccccc"><grid height="100" horizontalalignment="center"><stackpanel orientation="horizontal" verticalalignment="center"><textblock text="l" fontsize="55" margin="0,0,5,4" /><local:triangle x:name="triangle" height="40" width="40" strokethickness="2" stroke="royalblue" strokedasharray="4.045 4.045" strokedashoffset="0.05" strokedashcap="round" /><textblock text="ading..." fontsize="55" margin="5,0,0,4" /></stackpanel></grid></grid>
需要注意的是shape的边长要正好能被strokedasharray中短线和缺口的和整除,即 满足边长 / strokethickness % sum( strokedasharray ) = 0,这是因为在strokedashoffset=0的地方会截断短线,如下图所示:
另外注意的是边长的计算,如rectangle,边长并不是(height + width) * 2,而是(height - strokethickness) * 2 + (width- strokethickness) * 2,如下图所示,边长应该从边框正中间开始计算:
有一些shape的边长计算还会受到stretch影响,如上一篇中自定义的triangle:
<stackpanel orientation="horizontal" horizontalalignment="center"><grid height="50" width="50"><local:triangle stretch="fill" strokethickness="5" stroke="royalblue" /></grid><grid height="50" width="50" margin="10,0,0,0"><local:triangle stretch="none" strokethickness="5" stroke="royalblue" /></grid></stackpanel>
2. 使用strokedasharray做进度提示动画strokedasharray用于将shape的边框变成虚线,strokedasharray的值是一个double类型的有序集合,里面的数值指定虚线中每一段以strokethickness为单位的长度。用strokedasharray做进度提示的基本做法就是将进度progress通过converter转换为分成两段的strokedasharray,第一段为实线,表示当前进度,第二段为空白。假设一个shape的边长是100,当前进度为50,则将strokedasharray设置成{50,double.maxvalue}两段。
做成动画如下图所示:
<page.resources><style targettype="textblock"><setter property="fontsize" value="12" /></style><local:progresstostrokedasharrayconverter x:key="progresstostrokedasharrayconverter" targetpath="{binding elementname=triangle}" /><local:progresstostrokedasharrayconverter2 x:key="progresstostrokedasharrayconverter2" targetpath="{binding elementname=triangle}" /> <toolkit:stringformatconverter x:key="stringformatconverter" /><local:progresswrapper x:name="progresswrapper" /><storyboard x:name="storyboard1"><doubleanimation duration="0:0:5" to="100" storyboard.targetproperty="progress" storyboard.targetname="progresswrapper" enabledependentanimation="true" /></storyboard></page.resources><grid background="{themeresource applicationpagebackgroundthemebrush}"><viewbox height="150"><stackpanel orientation="horizontal"><grid><local:triangle height="40" width="40" strokethickness="2" stroke="darkgray" /><local:triangle x:name="triangle" height="40" width="40" strokethickness="2" stroke="royalblue" strokedasharray="{binding progress,source={staticresource progresswrapper},converter={staticresource progresstostrokedasharrayconverter}}" /><textblock text="{binding progress,source={staticresource progresswrapper},converter={staticresource stringformatconverter},converterparameter='{}{0:0}'}" horizontalalignment="center" verticalalignment="center" margin="0,15,0,0" /></grid><grid margin="20,0,0,0"><local:triangle height="40" width="40" strokethickness="2" stroke="darkgray" /><local:triangle x:name="triangle2" height="40" width="40" strokethickness="2" stroke="royalblue" strokedasharray="{binding progress,source={staticresource progresswrapper},converter={staticresource progresstostrokedasharrayconverter2}}" /><textblock text="{binding progress,source={staticresource progresswrapper},converter={staticresource stringformatconverter},converterparameter='{}{0:0}'}" horizontalalignment="center" verticalalignment="center" margin="0,15,0,0" /></grid></stackpanel></viewbox></grid>
其中progresstostrokedasharrayconverter和progresstostrokedasharrayconverter2的代码如下:
public class progresstostrokedasharrayconverter : dependencyobject, ivalueconverter {/// <summary>/// 获取或设置targetpath的值/// </summary> public path targetpath { get { return (path)getvalue(targetpathproperty); } set { setvalue(targetpathproperty, value); } }/// <summary>/// 标识 targetpath 依赖属性。/// </summary>public static readonly dependencyproperty targetpathproperty = dependencyproperty.register("targetpath", typeof(path), typeof(progresstostrokedasharrayconverter), new propertymetadata(null));public virtual object convert(object value, type targettype, object parameter, string language) { if (value is double == false)return null; var progress = (double)value;if (targetpath == null)return null;var totallength = gettotallength(); var firstsection = progress * totallength / 100 / targetpath.strokethickness;if (progress == 100) firstsection = math.ceiling(firstsection);var result = new doublecollection { firstsection, double.maxvalue };return result; }public object convertback(object value, type targettype, object parameter, string language) {throw new notimplementedexception(); }protected double gettotallength() {var geometry = targetpath.data as pathgeometry; if (geometry == null) return 0; if (geometry.figures.any() == false)return 0; var figure = geometry.figures.firstordefault(); if (figure == null) return 0; var totallength = 0d; var point = figure.startpoint; foreach (var item in figure.segments) { var segment = item as linesegment; if (segment == null) return 0; totallength += math.sqrt(math.pow(point.x - segment.point.x, 2) + math.pow(point.y - segment.point.y, 2)); point = segment.point; } totallength += math.sqrt(math.pow(point.x - figure.startpoint.x, 2) + math.pow(point.y - figure.startpoint.y, 2)); return totallength; } } public class progresstostrokedasharrayconverter2 : progresstostrokedasharrayconverter { public override object convert(object value, type targettype, object parameter, string language) { if (value is double == false)return null; var progress = (double)value; if (targetpath == null) return null; var totallength = gettotallength(); totallength = totallength / targetpath.strokethickness; var thirdsection = progress * totallength / 100; if (progress == 100) thirdsection = math.ceiling(thirdsection); var secondsection = (totallength - thirdsection) / 2; var result = new doublecollection { 0, secondsection, thirdsection, double.maxvalue }; return result; } }
由于代码只是用于演示,protected double gettotallength()写得比较将就。可以看到这两个converter继承自dependencyobject,这是因为这里需要通过绑定为targetpath赋值。
这里还有另一个类progresswrapper:
public class progresswrapper : dependencyobject {/// <summary>/// 获取或设置progress的值/// </summary> public double progress {get { return (double)getvalue(progressproperty); }set { setvalue(progressproperty, value); } }/// <summary>/// 标识 progress 依赖属性。/// </summary>public static readonly dependencyproperty progressproperty = dependencyproperty.register("progress", typeof(double), typeof(progresswrapper), new propertymetadata(0d)); }
因为这里没有可供storyboard操作的double属性,所以用这个类充当storyboard和strokedasharray的桥梁。uwpcommunitytoolkit中也有一个差不多用法的类bindablevalueholder,这个类通用性比较强,可以参考它的用法。
3. 使用behavior改进进度提示动画代码只是做个动画而已,又是converter,又是wrapper,又是binding,看起来十分复杂,如果shape上面有progress属性就方便多了。这时候首先会考虑附加属性,在xaml用法如下:
<usercontrol.resources> <storyboard x:name="storyboard1"><doubleanimation duration="0:0:5" to="100" storyboard.targetproperty="(local:pathextention.progress)" storyboard.targetname="triangle" /> </storyboard></usercontrol.resources><grid x:name="layoutroot" background="white"><local:triangle x:name="triangle" height="40" local:pathextention.progress="0" width="40" strokethickness="2" stroke="royalblue" ></local:triangle></grid>
但其实这是行不通的,xaml有一个存在了很久的限制:however, an existing limitation of the windows runtime xaml implementation is that you cannot animate a custom attached property.。这个限制决定了xaml不能对自定义附加属性做动画。不过,这个限制只限制了不能对自定义附加属性本身做动画,但对附加属性中的类的属性则可以,例如以下这种写法应该是行得通的:
<usercontrol.resources> <storyboard x:name="storyboard1"><doubleanimation duration="0:0:5" to="100" storyboard.targetproperty="(local:pathextention.progress)" storyboard.targetname="trianglepathextention" /> </storyboard></usercontrol.resources><grid x:name="layoutroot" background="white"><local:triangle x:name="triangle" height="40" width="40" strokethickness="2" stroke="royalblue" > <local:pathhelper><local:pathextention x:name="trianglepathextention" progress="0" /> </local:pathhelper></local:triangle></grid>
更优雅的写法是利用xamlbehaviors,这篇文章很好地解释了xamlbehaviors的作用:
xaml behaviors非常重要,因为它们提供了一种方法,让开发人员能够以一种简洁、可重复的方式轻松地向ui对象添加功能。 他们无需创建控件的子类或重复编写逻辑代码,只要简单地增加一个xaml代码片段。
要使用behavior改进现有代码,只需实现一个pathprogressbehavior:
public class pathprogressbehavior : behavior<uielement> {protected override void onattached() {base.onattached();updatestrokedasharray(); }/// <summary>/// 获取或设置progress的值/// </summary> public double progress {get { return (double)getvalue(progressproperty); }set { setvalue(progressproperty, value); } }/*progress dependencyproperty*/protected virtual void onprogresschanged(double oldvalue, double newvalue) {updatestrokedasharray(); }protected virtual double gettotallength(path path) {/*some code*/}private void updatestrokedasharray() { var target = associatedobject as path;if (target == null)return;double progress = progress; //if (target.actualheight == 0 || target.actualwidth == 0)// return; if (target.strokethickness == 0) return; var totallength = gettotallength(target); var firstsection = progress * totallength / 100 / target.strokethickness; if (progress == 100) firstsection = math.ceiling(firstsection); var result = new doublecollection { firstsection, double.maxvalue }; target.strokedasharray = result; } }
xaml中如下使用:
<usercontrol.resources> <storyboard x:name="storyboard1"><doubleanimation duration="0:0:5" to="100" storyboard.targetproperty="progress" storyboard.targetname="pathprogressbehavior" enabledependentanimation="true"/> </storyboard></usercontrol.resources><grid x:name="layoutroot" background="white"> <local:triangle x:name="triangle" height="40" local:pathextention.progress="0" width="40" strokethickness="2" stroke="royalblue" ><interactivity:interaction.behaviors> <local:pathprogressbehavior x:name="pathprogressbehavior" /></interactivity:interaction.behaviors> </local:triangle></grid>
这样看起来就清爽多了。
4. 模仿背景填充动画先看看效果:
其实这篇文章里并不会讨论填充动画,不过首先声明做填充动画会更方便快捷,这一段只是深入学习过程中的产物,实用价值不高。
上图三角形的填充的效果只需要叠加两个同样大小的shape,前面那个设置stretch="uniform",再通过doubleanimation改变它的高度就可以了。文字也是相同的原理,叠加两个相同的textblock,将前面那个放在一个无边框的scrollviewer里再去改变scrollviewer的高度。
<page.resources><style targettype="textblock"><setter property="fontsize" value="12" /></style><local:progresstoheightconverter x:key="progresstoheightconverter" targetcontentcontrol="{binding elementname=contentcontrol}" /><local:reverseprogresstoheightconverter x:key="reverseprogresstoheightconverter" targetcontentcontrol="{binding elementname=contentcontrol2}" /><toolkit:stringformatconverter x:key="stringformatconverter" /><local:progresswrapper x:name="progresswrapper" /><storyboard x:name="storyboard1"><doubleanimation duration="0:0:5" to="100" storyboard.targetproperty="progress" storyboard.targetname="progresswrapper" enabledependentanimation="true" /></storyboard></page.resources><grid background="{themeresource applicationpagebackgroundthemebrush}"><grid><local:triangle height="40" width="40" strokethickness="2" fill="lightgray" /><local:triangle height="40" width="40" stretch="fill" strokethickness="2" stroke="royalblue" /><contentcontrol x:name="contentcontrol" verticalalignment="bottom" horizontalalignment="center" height="{binding progress,source={staticresource progresswrapper},converter={staticresource progresstoheightconverter}}"><local:triangle x:name="triangle3" height="40" width="40" strokethickness="2" fill="royalblue" stretch="uniform" verticalalignment="bottom" /></contentcontrol><textblock text="{binding progress,source={staticresource progresswrapper},converter={staticresource stringformatconverter},converterparameter='{}{0:0}'}" horizontalalignment="center" verticalalignment="center" margin="0,12,0,0" foreground="white" /><contentcontrol x:name="contentcontrol2" height="{binding progress,source={staticresource progresswrapper},converter={staticresource reverseprogresstoheightconverter}}" verticalalignment="top" horizontalalignment="center"><scrollviewer borderthickness="0" padding="0,0,0,0" verticalscrollbarvisibility="disabled" horizontalscrollbarvisibility="disabled" verticalalignment="top" height="40"><grid height="40"><textblock text="{binding progress,source={staticresource progresswrapper},converter={staticresource stringformatconverter},converterparameter='{}{0:0}'}" horizontalalignment="center" verticalalignment="center" margin="0,12,0,0" /></grid></scrollviewer></contentcontrol></grid></grid>
progresstoheightconverter和reverseprogresstoheightconverter的代码如下:
public class progresstoheightconverter : dependencyobject, ivalueconverter {/// <summary>/// 获取或设置targetcontentcontrol的值/// </summary> public contentcontrol targetcontentcontrol { get { return (contentcontrol)getvalue(targetcontentcontrolproperty); } set { setvalue(targetcontentcontrolproperty, value); } }/// <summary>/// 标识 targetcontentcontrol 依赖属性。/// </summary>public static readonly dependencyproperty targetcontentcontrolproperty = dependencyproperty.register("targetcontentcontrol", typeof(contentcontrol), typeof(progresstoheightconverter), new propertymetadata(null)); public object convert(object value, type targettype, object parameter, string language) { if (value is double == false) return 0d; var progress = (double)value; if (targetcontentcontrol == null) return 0d; var element = targetcontentcontrol.content as frameworkelement; if (element == null) return 0d;return element.height * progress / 100; }public object convertback(object value, type targettype, object parameter, string language) {throw new notimplementedexception(); } }public class reverseprogresstoheightconverter : dependencyobject, ivalueconverter {/// <summary>/// 获取或设置targetcontentcontrol的值/// </summary> public contentcontrol targetcontentcontrol { get { return (contentcontrol)getvalue(targetcontentcontrolproperty); } set { setvalue(targetcontentcontrolproperty, value); } }/// <summary>/// 标识 targetcontentcontrol 依赖属性。/// </summary>public static readonly dependencyproperty targetcontentcontrolproperty = dependencyproperty.register("targetcontentcontrol", typeof(contentcontrol), typeof(reverseprogresstoheightconverter), new propertymetadata(null)); public object convert(object value, type targettype, object parameter, string language) { if (value is double == false) return double.nan; var progress = (double)value;if (targetcontentcontrol == null)return double.nan; var element = targetcontentcontrol.content as frameworkelement; if (element == null)return double.nan; return element.height * (100 - progress) / 100; } public object convertback(object value, type targettype, object parameter, string language) { throw new notimplementedexception(); } }
再提醒一次,实际上老老实实做填充动画好像更方便些。
5. 将动画应用到button的controltemplate同样的技术,配合controltemplate可以制作很有趣的按钮:
pointerentered时,按钮的边框从进入点向反方向延伸。pointerexited时,边框从反方向向移出点消退。要做到这点需要在pointerentered时改变边框的方向,使用了changeangletoenterpointerbehavior:
public class changeangletoenterpointerbehavior : behavior<ellipse> {protected override void onattached() {base.onattached(); associatedobject.pointerentered += onassociatedobjectpointerentered; associatedobject.pointerexited += onassociatedobjectpointerexited; }protected override void ondetaching() {base.ondetaching(); associatedobject.pointerentered -= onassociatedobjectpointerentered; associatedobject.pointerexited -= onassociatedobjectpointerexited; }private void onassociatedobjectpointerexited(object sender, pointerroutedeventargs e) {updateangle(e); }private void onassociatedobjectpointerentered(object sender, pointerroutedeventargs e) {updateangle(e); }private void updateangle(pointerroutedeventargs e) {if (associatedobject == null || associatedobject.strokethickness == 0)return; associatedobject.rendertransformorigin = new point(0.5, 0.5);var rotatetransform = associatedobject.rendertransform as rotatetransform;if (rotatetransform == null) { rotatetransform = new rotatetransform(); associatedobject.rendertransform = rotatetransform; }var point = e.getcurrentpoint(associatedobject.parent as uielement).position;var centerpoint = new point(associatedobject.actualwidth / 2, associatedobject.actualheight / 2);var angleofline = math.atan2(point.y - centerpoint.y, point.x - centerpoint.x) * 180 / math.pi; rotatetransform.angle = angleofline + 180; } }
这个类命名不是很好,不过将就一下吧。
为了做出边框延伸的效果,另外需要一个类ellipseprogressbehavior:
public class ellipseprogressbehavior : behavior<ellipse> {/// <summary>/// 获取或设置progress的值/// </summary> public double progress { get { return (double)getvalue(progressproperty); } set { setvalue(progressproperty, value); } }/// <summary>/// 标识 progress 依赖属性。/// </summary> public static readonly dependencyproperty progressproperty = dependencyproperty.register("progress", typeof(double), typeof(ellipseprogressbehavior), new propertymetadata(0d, onprogresschanged)); private static void onprogresschanged(dependencyobject obj, dependencypropertychangedeventargs args) { var target = obj as ellipseprogressbehavior; double oldvalue = (double)args.oldvalue; double newvalue = (double)args.newvalue;if (oldvalue != newvalue) target.onprogresschanged(oldvalue, newvalue); } protected virtual void onprogresschanged(double oldvalue, double newvalue) {updatestrokedasharray(); }protected virtual double gettotallength() {if (associatedobject == null)return 0; return (associatedobject.actualheight - associatedobject.strokethickness) * math.pi; }private void updatestrokedasharray() {if (associatedobject == null || associatedobject.strokethickness == 0) return; //if (target.actualheight == 0 || target.actualwidth == 0)// return;var totallength = gettotallength(); totallength = totallength / associatedobject.strokethickness; var thirdsection = progress * totallength / 100; var secondsection = (totallength - thirdsection) / 2; var result = new doublecollection { 0, secondsection, thirdsection, double.maxvalue }; associatedobject.strokedasharray = result; } }
套用到controltemplate如下:
<controltemplate targettype="button"><grid x:name="rootgrid"><visualstatemanager.visualstategroups><visualstategroup x:name="commonstates"><visualstategroup.transitions><visualtransition generatedduration="0:0:1" to="normal"><storyboard><doubleanimationusingkeyframes enabledependentanimation="true" storyboard.targetproperty="(local:ellipseprogressbehavior.progress)" storyboard.targetname="ellipseprogressbehavior"><easingdoublekeyframe keytime="0:0:1" value="0"><easingdoublekeyframe.easingfunction><quinticease easingmode="easeout" /></easingdoublekeyframe.easingfunction></easingdoublekeyframe></doubleanimationusingkeyframes></storyboard></visualtransition><visualtransition generatedduration="0:0:1" to="pointerover"><storyboard><doubleanimationusingkeyframes enabledependentanimation="true" storyboard.targetproperty="(local:ellipseprogressbehavior.progress)" storyboard.targetname="ellipseprogressbehavior"><easingdoublekeyframe keytime="0:0:1" value="100"><easingdoublekeyframe.easingfunction><quinticease easingmode="easeout" /></easingdoublekeyframe.easingfunction></easingdoublekeyframe></doubleanimationusingkeyframes></storyboard></visualtransition></visualstategroup.transitions><visualstate x:name="normal"><storyboard><pointerupthemeanimation storyboard.targetname="rootgrid" /></storyboard></visualstate><visualstate x:name="pointerover"><storyboard><pointerupthemeanimation storyboard.targetname="rootgrid" /></storyboard><visualstate.setters><setter target="ellipseprogressbehavior.(local:ellipseprogressbehavior.progress)" value="100" /></visualstate.setters></visualstate><visualstate x:name="pressed"><storyboard><pointerdownthemeanimation storyboard.targetname="rootgrid" /></storyboard></visualstate><visualstate x:name="disabled" /></visualstategroup></visualstatemanager.visualstategroups><contentpresenter x:name="contentpresenter" automationproperties.accessibilityview="raw" contenttemplate="{templatebinding contenttemplate}" contenttransitions="{templatebinding contenttransitions}" content="{templatebinding content}" horizontalcontentalignment="{templatebinding horizontalcontentalignment}" padding="{templatebinding padding}" verticalcontentalignment="{templatebinding verticalcontentalignment}" /><ellipse fill="transparent" stroke="{templatebinding borderbrush}" strokethickness="2"><interactivity:interaction.behaviors><local:changeangletoenterpointerbehavior /><local:ellipseprogressbehavior x:name="ellipseprogressbehavior" /></interactivity:interaction.behaviors></ellipse></grid></controltemplate>
注意:我没有鼓励任何人自定义按钮外观的意思,能用系统自带的动画或样式就尽量用系统自带的,没有设计师的情况下 又想ui做得与众不同通常会做得很难看。想要ui好看,合理的布局、合理的颜色、合理的图片就足够了。
6. 结语在学习shape的过程中觉得好玩就做了很多尝试,因为以前工作中做过不少等待、进度的动画,所以这次就试着做出本文的动画。
xaml的传统动画并没有提供太多功能,主要是coloranimation、doubleanimation、pointanimation三种,不过靠binding和converter可以弥补这方面的不足,实现很多需要的功能。
本文的一些动画效果参考了svg的动画。话说回来,windows 10 1703新增了svgimagesource,不过看起来只是简单地将svg翻译成对应的shape,然后用shape呈现,不少高级特性都不支持(如下图阴影的滤镜),用法如下:
<image><image.source><svgimagesource urisource="feoffset_1.svg" /></image.source></image>
svgimagesource:
原本的svg:
以上就是用shape做动画实例代码的详细内容。
其它类似信息

推荐信息