你自己的缓和功能
您可以轻松制作自己的缓动功能。所需要的只是一个类型为Func 的方法,它是一个带有double参数和double返回值的函数。这是一个传递函数:它应该为0的参数返回0,并且对于1的参数应该返回1.但是在这两个值之间,任何事情都会发生。
通常,您将自定义缓动函数定义为Easing构造函数的参数。这是Easing定义的唯一构造函数,但Easing类还定义了一个隐式转换
Func 到Easing。
Xamarin.Forms动画函数调用Easing对象的Ease方法。该Ease方法还有一个double参数和一个double返回值,它基本上提供了对在Easing构造函数中指定的缓动函数的公共访问。 (本章前面的图表显示了各种预定义的缓动函数是由访问各种预定义Easing对象的Ease方法的程序生成的。)
这是一个程序,它包含两个自定义缓动函数来控制Button的缩放。这些函数有点与“ease”这个词的含义相矛盾,这就是为什么程序被称为UneasyScale。 这两个缓动函数中的第一个将输入值截断为离散值0,0.2,0.4,0.6,0.8和1,因此Button的跳跃大小增加。 然后使用另一个缓动函数减小Button的大小,该函数对输入值应用一点随机变化。
这些缓动函数中的第一个被指定为Easing构造函数的lambda函数参数。 第二个是强制转换为Easing对象的方法:
public partial class UneasyScalePage : ContentPage
{
Random random = new Random();
public UneasyScalePage()
{
InitializeComponent();
}
async void OnButtonClicked(object sender, EventArgs args)
{
double scale = Math.Min(Width / button.Width, Height / button.Height);
await button.ScaleTo(scale, 1000, new Easing(t => (int)(5 * t) / 5.0));
await button.ScaleTo(1, 1000, (Easing)RandomEase);
}
double RandomEase(double t)
{
return t == 0 || t == 1 ? t : t + 0.25 * (random.NextDouble() - 0.5);
}
}
不幸的是,更容易制作像这样的脱节函数而不是更平滑和更有趣的传递函数。 那些往往有点复杂。
例如,假设您想要一个如下所示的缓动函数:
它开始快速,然后减速并逆转航向,但随后再次反转以迅速上升到最后一段。
您可能猜测这是一个多项式方程,或者至少它可以用多项式方程近似。 它有两个点,其中斜率为零,这进一步表明这是一个立方体,可以这样表示:
𝑓(𝑡) = 𝑎 ∙ 𝑡 ∙ 𝑡 ∙ 𝑡 + 𝑏 ∙ 𝑡 ∙ 𝑡 + 𝑐 ∙ 𝑡 + d
现在我们需要找到的是a,b,c和d的值,这些值将导致传递函数按我们的意愿运行。
对于端点,我们知道:
𝑓(0) = 0
𝑓(1) = 1
这意味着:
𝑑 = 0
还有
1 = 𝑎 + 𝑏 + c
如果我们进一步说曲线中的两个倾角在t等于1/3和2/3,并且这些点处的f(t)值分别是2/3和1/3,则:
2/3= 𝑎 ∙1/27 + 𝑏 ∙1/9+ 𝑐 ∙1/3
1/3= 𝑎 ∙8/27 + 𝑏 ∙4/9+ 𝑐 ∙2/3
如果它们被转换为整数系数,那么这两个方程式更易读和可操作,所以我们得到的是具有三个未知数的三个方程式:
1 = 𝑎 + 𝑏 + 𝑐
18 = 𝑎 + 3 ∙ 𝑏 + 9 ∙ 𝑐
9 = 8 ∙ 𝑎 + 12 ∙ 𝑏 + 18 ∙ 𝑐
通过一些操作,组合和工作,你可以找到a,b和c:
𝑎 = 9
𝑏 = −27/2
𝑐 =11/2
让我们看看它是否符合我们的想法。 CustomCubicEase程序具有与以前的项目相同的XAML文件。 缓动函数在此直接表示为Func 对象,因此可以方便地在两个ScaleTo调用中使用。 按钮首先按比例放大,然后在暂停一秒后,按钮缩放回正常状态:
public partial class CustomCubicEasePage : ContentPage
{
public CustomCubicEasePage()
{
InitializeComponent();
}
async void OnButtonClicked(object sender, EventArgs args)
{
Func<double, double> customEase = t => 9 * t * t * t - 13.5 * t * t + 5.5 * t;
double scale = Math.Min(Width / button.Width, Height / button.Height);
await button.ScaleTo(scale, 1000, customEase);
await Task.Delay(1000);
await button.ScaleTo(1, 1000, customEase);
}
}
如果你不认为让自己的缓和功能变得“有趣和轻松”,那么许多标准缓和功能的一个很好的来源是网站http://robertpenner.com/easing/。
如果您需要简单的谐波运动并将Math.Exp与指数增加或衰减相结合,也可以从Math.Sin和Math.Cos构造缓动函数。
让我们举一个例子:假设你想要一个按钮,当它被点击时,从它的左下角向下摆动,几乎就像按钮是贴在墙上有几个钉子的图片一样,其中一个钉子脱落了,所以 图片滑落,左下角有一个钉子。
您可以在AnimationTryout程序中按照此练习进行操作。 在Button的Clicked处理程序中,让我们首先设置AnchorX和AnchorY属性,然后调用RotateTo进行90度摆动:
button.AnchorX = 0;
button.AnchorY = 1;
await button.RotateTo(90, 3000);
这是动画完成时的结果:
但是这确实需要一个缓和功能,以便Button在稳定之前从那个角落来回摆动一下。 首先,让我们首先在RotateTo调用中添加一个do-nothing线性缓动函数:
await button.RotateTo(90, 3000, new Easing(t => t));
现在让我们添加一些正弦行为。 那是正弦或余弦。 我们希望摆动在开始时很慢,这意味着余弦而不是正弦。 让我们将参数设置为Math.Cos方法,以便当t从0变为1时,角度为0到10π。 这是余弦曲线的五个完整周期,这意味着Button来回摆动五次:
await button.RotateTo(90, 3000, new Easing(t => Math.Cos(10 * Math.PI * t)));
当然,这根本不对。 当t为零时,Math.Cos方法返回1,因此动画通过跳转到90度的值开始。 对于t的后续值,Math.Cos函数返回从1到-1的值,因此Button从90度到-90度摆动五次并回到90度,最后在90度休息。 这确实是我们希望动画结束的地方,但我们希望动画从0度开始。
不过,让我们暂时忽略这个问题。 让我们来解决最初看起来更复杂的问题。 我们不希望Button完全旋转180度五次。 我们希望按钮的摆动随着时间的推移而衰减。
有一种简单的方法可以做到这一点。 我们可以通过Math.Exp调用将Math.Cos方法与基于t的负参数相乘:
Math.Exp(-5 * t)
Math.Exp方法将数学常数e(约2.7)提高到指定的幂。当动画开始时t为0时,e为0,幂为1.当t为1时,e为负五次幂 小于.01,非常接近于零。 (在此调用中您不需要使用-5;您可以尝试查找看起来最佳的值。)
让我们将Math.Cos结果乘以Math.Exp结果:
await button.RotateTo(90, 3000, new Easing(t => Math.Cos(10 * Math.PI * t) * Math.Exp(-5 * t)));
我们非常接近。 Math.Exp确实阻止了Math.Cos调用,但是产品是向后的。当t为0时,乘积为1,当t为1时,乘积为0.我们可以通过简单地从1减去整个表达式来解决这个问题吗? 我们来试试吧:
await button.RotateTo(90, 3000,
new Easing(t => 1 - Math.Cos(10 * Math.PI * t) * Math.Exp(-5 * t)));
现在,当t为0时,缓动函数正确返回0,当t为1时,缓冲函数正确地返回1。
而且,更重要的是,宽松功能现在在视觉上也令人满意。 它看起来好像按钮从系泊中掉落并且在休息之前摇摆了几次。
现在让我们调用TranslateTo使Button退出并落到页面底部。 Button需要放多远?
Button最初位于页面的中心。 这意味着Button底部与页面之间的距离是页面高度的一半减去Button的高度:
(Height - button.Height) / 2
但是现在Button已经从其左下角摆动了90度,因此Button的宽度更接近页面底部。 这是对TranslateTo的完整调用,将Button放到页面底部并使其反弹一点:
await button.TranslateTo(0, (Height - button.Height) / 2 - button.Width,
1000, Easing.BounceOut);
按钮就像这样休息:
现在让我们将Button龙骨翻过来并倒置,这意味着我们想要围绕右上角旋转按钮。 这需要更改AnchorX和AnchorY属性:
button.AnchorX = 1;
button.AnchorY = 0;
但这是一个问题 - 一个大问题 - 因为AnchorX和AnchorY属性的更改实际上会改变Button的位置。 试试吧! 按钮突然跳起来向右。 Button跳转到的位置恰好是第一个RotateTo基于这些新的AnchorX和AnchorY值时的位置 - 围绕其右上角而不是左下角旋转。
你能想象出来吗? 这是一个小模型,显示按钮的原始位置,按钮从左下角顺时针旋转90度,按钮从右上角顺时针旋转90度:
当我们设置AnchorX和AnchorY的新值时,我们需要调整TranslationX和TranslationY属性,以便Button基本上从右上角的旋转位置移动到左下角的旋转位置。 TranslationX需要通过Button的宽度减小,然后增加其高度。 需要通过Button的高度和Button的宽度来增加TranslationY。 我们试试看:
button.TranslationX -= button.Width - button.Height;
button.TranslationY += button.Width + button.Height;
当AnchorX和AnchorY属性更改为按钮的右上角时,它会保留Button的位置。
现在按钮可以在它翻倒时绕其右上角旋转,当然还有一点反弹:
await button.RotateTo(180, 1000, Easing.BounceOut);
现在Button可以登上屏幕并同时淡出:
await Task.WhenAll
(
button.FadeTo(0, 4000),
button.TranslateTo(0, -Height, 5000, Easing.CubicIn)
);
FadeTo方法为Opacity属性设置动画,在这种情况下,从默认值1到指定为第一个参数的值0。
这是完整的程序,称为SwingButton(指第一个动画),最后将Button恢复到原始位置,以便您可以再次尝试:
public partial class SwingButtonPage : ContentPage
{
public SwingButtonPage()
{
InitializeComponent();
}
async void OnButtonClicked(object sender, EventArgs args)
{
// Swing down from lower-left corner.
button.AnchorX = 0;
button.AnchorY = 1;
await button.RotateTo(90, 3000,
new Easing(t => 1 - Math.Cos(10 * Math.PI * t) * Math.Exp(-5 * t)));
// Drop to the bottom of the screen.
await button.TranslateTo(0, (Height - button.Height) / 2 - button.Width,
1000, Easing.BounceOut);
// Prepare AnchorX and AnchorY for next rotation.
button.AnchorX = 1;
button.AnchorY = 0;
// Compensate for the change in AnchorX and AnchorY.
button.TranslationX -= button.Width - button.Height;
button.TranslationY += button.Width + button.Height;
// Fall over.
await button.RotateTo(180, 1000, Easing.BounceOut);
// Fade out while ascending to the top of the screen.
await Task.WhenAll
(
button.FadeTo(0, 4000),
button.TranslateTo(0, -Height, 5000, Easing.CubicIn)
);
// After three seconds, return the Button to normal.
await Task.Delay(3000);
button.TranslationX = 0;
button.TranslationY = 0;
button.Rotation = 0;
button.Opacity = 1;
}
}
当输入为0时,缓动函数应该返回0;当输入为1时,缓动函数应该返回1,但是有可能破坏这些规则,有时这是有意义的。 例如,假设您想要一个稍微移动元素的动画 - 也许它会以某种方式振动它 - 但动画应该将元素返回到最后的原始位置。 对于类似这样的事情,当输入为0和1时,缓动函数返回0是有意义的,但这些值之间的值不是0。
这是JiggleButton背后的想法,它位于Xamarin.FormsBook.Toolkit库中。 JiggleButton派生自Button并安装Clicked处理程序,其唯一目的是在您单击按钮时摇动按钮:
namespace Xamarin.FormsBook.Toolkit
{
public class JiggleButton : Button
{
bool isJiggling;
public JiggleButton()
{
Clicked += async (sender, args) =>
{
if (isJiggling)
return;
isJiggling = true;
await this.RotateTo(15, 1000, new Easing(t =>
Math.Sin(Math.PI * t) *
Math.Sin(Math.PI * 20 * t)));
isJiggling = false;
};
}
}
}
RotateTo方法似乎在一秒钟内将按钮旋转了15度。但是,自定义Easing对象有不同的想法。它仅由两个正弦函数的乘积组成。当t从0变为1时,第一个Math.Sin函数扫描正弦曲线的前半部分,因此当t为0时从0变为0,当t为0.5时变为1,当t为1时变为0。
第二个Math.Sin调用是抖动部分。当t从0变为1时,此调用将经历10个正弦曲线周期。如果没有第一次Math.Sin调用,这会将按钮从0度旋转到15度,然后旋转到-15度,然后再回到0度十次。但是第一个Math.Sin调用会在动画的开始和结束时抑制旋转,只允许在中间旋转15到15度。
涉及isJiggling字段的一些代码可以保护Clicked处理程序在一个新动画正在进行时启动它。这是使用await和动画方法的一个优点:您确切知道动画何时完成。
JiggleButtonDemo XAML文件创建三个JiggleButton对象,以便您可以使用它们:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:toolkit=
"clr-namespace:Xamarin.FormsBook.Toolkit;assembly=Xamarin.FormsBook.Toolkit"
x:Class="JiggleButtonDemo.JiggleButtonDemoPage">
<StackLayout>
<toolkit:JiggleButton Text="Tap Me!"
FontSize="Large"
HorizontalOptions="Center"
VerticalOptions="CenterAndExpand" />
<toolkit:JiggleButton Text="Tap Me!"
FontSize="Large"
HorizontalOptions="Center"
VerticalOptions="CenterAndExpand" />
<toolkit:JiggleButton Text="Tap Me!"
FontSize="Large"
HorizontalOptions="Center"
VerticalOptions="CenterAndExpand" />
</StackLayout>
</ContentPage>