效果图

好久没有写wpf了。
最近看到飞书的点赞动画非常有意思,决定试试,虽然不及飞书那样的绚丽,但是练手还是可以的,希望自己的手艺还在!
那么如何写一个这样的动画呢?
首先需要刨析这个动画的构成:
外圈圆
大拇指-1竖着
大拇指-2握着
颤动动画
中心旋转动画
展开中心旋转动画
当我们分析这些东西剩下的就好办了。
首先我们先办了这个最难的东西大拇指。

这个东西的构成,我们使用Path 直接去写。显然我们就会得到这样的代码
<Geometry x:Key="t1">
M 20 40
v 0 40
h 0 7
v 0 -40
z
M 30 42
v 0 38
h 40 0
l 15 -35
l -10 -5
h -25 0
l 2 -20
<!--小褶皱-->
q -10 -10, -20 22
z
</Geometry>
当我们在path 100*100的大小的时候使用脑补进行绘制就就可以了。
至于这个小褶皱我曾经不想要,但是看到了自己的猪爪...还是决定加上了。
这代码的原理非常简单,基本都是基本的直线绘制,最难的就是用了个贝塞尔来制造大拇指背部的弧度.
不管咋样还是弄出来个简单的赞。
剩下就是握着的状态了
那么我们只需要修改部分代码就可以达到了~
也就是
<Geometry x:Key="t2">
M 20 40
v 0 40
h 0 7
v 0 -40
z
M 30 42
v 0 38
h 40 0
l 15 -35
l -10 -5
h -25 0
l 2 0
<!--小褶皱-->
q -10 -10, -20 0
z
</Geometry>
我们修改了最后两行代码的 l 的y参数和q最后的end point参数的y的值都是降到0了 这样会形成一个简单的弧度

哈 这样子 我们就是得到了两个手掌的不同状态了。
剩下的事情就是来组装吧~~~~
首先是大拇指张开和大拇指握住的状态转换。
做到这事情最简单的动画就是使用eventtigger来做,我们使用简单的鼠标按下事件作为启动,当然了 想要丰富过程也是可以使用鼠标浮动事件作为启动事件之一。
<Path.Triggers>
<EventTrigger RoutedEvent="MouseLeftButtonDown">
<BeginStoryboard x:Name="Bs1">
<Storyboard>
<ObjectAnimationUsingKeyFrames BeginTime="0:0:0" Storyboard.TargetProperty="Data">
<DiscreteObjectKeyFrame KeyTime="0:0:0.01">
<DiscreteObjectKeyFrame.Value>
<StaticResource ResourceKey="t2"/>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Path.Triggers>
为了做件事 ,我们把geometry作为window的资源 所以子啊写动画的时候 用离弦值就非常方便了。
观察代码,我们仅仅只是在点下的时候让path转换为握住的data,因为我们需要在松开左键的时候才让拇指竖起来。
所以还需要补上一个MouseLeftButtonUp的动画
这最终的代码就是
<Path.Triggers>
<EventTrigger RoutedEvent="MouseLeftButtonDown">
<BeginStoryboard x:Name="Bs1">
<Storyboard>
<ObjectAnimationUsingKeyFrames BeginTime="0:0:0" Storyboard.TargetProperty="Data">
<DiscreteObjectKeyFrame KeyTime="0:0:0.01">
<DiscreteObjectKeyFrame.Value>
<StaticResource ResourceKey="t2"/>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="MouseLeftButtonUp">
<RemoveStoryboard BeginStoryboardName="Bs1"/>
</EventTrigger>
</Path.Triggers>
效果图

莫名的搞笑....
基础的东西我们构建好了,剩下就是补全了。
不全颤抖啊,补全中心旋转动画,也就是手指握住后有一个向下的动画。
首先这个颤动 我们可以简单的理解为位移,一个快速的上下左右的位移
正好WPF有这种动画 所以我们就可以得到如下的代码
<ThicknessAnimationUsingKeyFrames RepeatBehavior="Forever" Duration="0:0:0.4" Storyboard.TargetProperty="Margin" >
<SplineThicknessKeyFrame KeyTime="0:0:0.0" Value="4,3,0,0"/>
<SplineThicknessKeyFrame KeyTime="0:0:0.2" Value="3,4,0,0"/>
<SplineThicknessKeyFrame KeyTime="0:0:0.3" Value="0,0,4,0"/>
<SplineThicknessKeyFrame KeyTime="0:0:0.35" Value="0,0,4,3"/>
<SplineThicknessKeyFrame KeyTime="0:0:0.4" Value="4,3,0,0"/>
</ThicknessAnimationUsingKeyFrames>
我们可以直代码放到path的eventtriger中
看得出来 离散动画的值就是简单的marigin的位移,代码非常简单。

就是在这里颤抖...
虽然看上去不是很好看,但是我们结合下一个动画,也就是手掌向下就会好很多了
这个动画很明显是一个旋转动画,所以我们需要提前准备一个roteate的transofrom
代码如下
<Path.RenderTransform>
<RotateTransform x:Name="rote" Angle="0"/>
</Path.RenderTransform>
动画代码如下
<DoubleAnimation Duration="0:0:0.1" To="30" Storyboard.TargetName="rote" Storyboard.TargetProperty="Angle">
<DoubleAnimation.EasingFunction>
<CubicEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
我们简单的使用了一个函数,提升一下效果的动感...
但是感觉没啥用
效果图就是这样的了

虽然看上去已经非常不错了,但是还有些不做,想想 我们的手都朝下了 松开之后为啥没有一个向上的弹簧动作呢?
也就是

我们需要在抬起时加上一个角度的旋转。
也就是
<EventTrigger RoutedEvent="MouseLeftButtonUp">
<BeginStoryboard x:Name="Bs2">
<Storyboard>
<DoubleAnimation FillBehavior="Stop" Duration="0:0:0.5" To="-30" Storyboard.TargetName="rote" Storyboard.TargetProperty="Angle">
<DoubleAnimation.EasingFunction>
<CubicEase EasingMode="EaseOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
<RemoveStoryboard BeginStoryboardName="Bs1"/>
</EventTrigger>
代码可以说手掌向下的反向操作。
顺便播放完把bs1动画解除掉。
剩下的就是圈的构造和动画。
圆圈呢,我们可以是直接的圆圈,也可以是broder,看个人喜欢了。
我就不罗嗦直接上代码
<Border BorderThickness="2" Background="Transparent" BorderBrush="Transparent" CornerRadius="100" Width="200" Height="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Width}" Grid.Column="1" Grid.Row="1">
<Border x:Name="sor" Visibility="Hidden" BorderThickness="2" Background="Transparent" BorderBrush="Transparent" CornerRadius="100" Width="200" Height="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Width}" Grid.Column="1" Grid.Row="1"/>
</Border>
构造了两个嵌套的borderr,宽度其实可以随意,只是演示的时候放大的大小而已。
动画则是放到了path的启动动画之中
也就是
<DoubleAnimation RepeatBehavior="Forever" SpeedRatio="1.2" Duration="0:0:1.5" To="0" Storyboard.TargetName="sor" Storyboard.TargetProperty="Width">
<DoubleAnimation.EasingFunction>
<CubicEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<ObjectAnimationUsingKeyFrames BeginTime="0:0:0" Storyboard.TargetName="sor" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0:0:0.1">
<DiscreteObjectKeyFrame.Value>
<Visibility>
Visible
</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
代码非常简单,控制下内圈的大小,还有是否隐藏而已。
这样子我们就最终得到了头图的效果了

总的过程还是比较简单的。
下面是全部的代码
<Window.Resources>
<Geometry x:Key="t1">
M 20 40
v 0 40
h 0 7
v 0 -40
z
M 30 42
v 0 38
h 40 0
l 15 -35
l -10 -5
h -25 0
l 2 -20
<!--小褶皱-->
q -10 -10, -20 22
z
</Geometry>
<Geometry x:Key="t2">
M 20 40
v 0 40
h 0 7
v 0 -40
z
M 30 42
v 0 38
h 40 0
l 15 -35
l -10 -5
h -25 0
l 2 0
<!--小褶皱-->
q -10 -10, -20 0
z
</Geometry>
<PathGeometry Figures=" M 20 40 l 2 -5 v 0 5 h -2 0 z" x:Key="roue"/>
</Window.Resources>
<Grid>
<Border BorderThickness="2" Background="Transparent" BorderBrush="BlueViolet" CornerRadius="100" Width="200" Height="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Width}" Grid.Column="1" Grid.Row="1">
<Border x:Name="sor" Visibility="Hidden" BorderThickness="2" Background="Transparent" BorderBrush="Salmon" CornerRadius="100" Width="200" Height="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Width}" Grid.Column="1" Grid.Row="1"/>
</Border>
<Grid Width="300" Height="300" ShowGridLines="False">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition />
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Path StrokeThickness="2" Grid.Column="1" Grid.Row="1" VerticalAlignment="Bottom" Stretch="Uniform" Fill="Pink" Width="80" Height="80" Stroke="Blue" Data="{StaticResource t1}" RenderTransformOrigin="0.5,0.5">
<Path.RenderTransform>
<RotateTransform x:Name="rote" Angle="0"/>
</Path.RenderTransform>
<Path.Triggers>
<EventTrigger RoutedEvent="MouseLeftButtonDown">
<BeginStoryboard x:Name="Bs1">
<Storyboard>
<ObjectAnimationUsingKeyFrames BeginTime="0:0:0" Storyboard.TargetProperty="Data">
<DiscreteObjectKeyFrame KeyTime="0:0:0.01">
<DiscreteObjectKeyFrame.Value>
<StaticResource ResourceKey="t2"/>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ThicknessAnimationUsingKeyFrames RepeatBehavior="Forever" Duration="0:0:0.4" Storyboard.TargetProperty="Margin" >
<SplineThicknessKeyFrame KeyTime="0:0:0.0" Value="4,3,0,0"/>
<SplineThicknessKeyFrame KeyTime="0:0:0.2" Value="3,4,0,0"/>
<SplineThicknessKeyFrame KeyTime="0:0:0.3" Value="0,0,4,0"/>
<SplineThicknessKeyFrame KeyTime="0:0:0.35" Value="0,0,4,3"/>
<SplineThicknessKeyFrame KeyTime="0:0:0.4" Value="4,3,0,0"/>
</ThicknessAnimationUsingKeyFrames>
<DoubleAnimation Duration="0:0:0.1" To="30" Storyboard.TargetName="rote" Storyboard.TargetProperty="Angle">
<DoubleAnimation.EasingFunction>
<CubicEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation RepeatBehavior="Forever" SpeedRatio="1.2" Duration="0:0:1.5" To="0" Storyboard.TargetName="sor" Storyboard.TargetProperty="Width">
<DoubleAnimation.EasingFunction>
<CubicEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<ObjectAnimationUsingKeyFrames BeginTime="0:0:0" Storyboard.TargetName="sor" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0:0:0.1">
<DiscreteObjectKeyFrame.Value>
<Visibility>
Visible
</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="MouseLeftButtonUp">
<BeginStoryboard x:Name="Bs2">
<Storyboard>
<DoubleAnimation FillBehavior="Stop" Duration="0:0:0.5" To="-30" Storyboard.TargetName="rote" Storyboard.TargetProperty="Angle">
<DoubleAnimation.EasingFunction>
<CubicEase EasingMode="EaseOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
<RemoveStoryboard BeginStoryboardName="Bs1"/>
</EventTrigger>
</Path.Triggers>
</Path>
</Grid>
我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div
我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看rubyzip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d
类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc
很好奇,就使用rubyonrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提
假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于
我正在尝试使用ruby和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我
关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。
我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t
我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h
我想为Heroku构建一个Rails3应用程序。他们使用Postgres作为他们的数据库,所以我通过MacPorts安装了postgres9.0。现在我需要一个postgresgem并且共识是出于性能原因你想要pggem。但是我对我得到的错误感到非常困惑当我尝试在rvm下通过geminstall安装pg时。我已经非常明确地指定了所有postgres目录的位置可以找到但仍然无法完成安装:$envARCHFLAGS='-archx86_64'geminstallpg--\--with-pg-config=/opt/local/var/db/postgresql90/defaultdb/po