第二十二章:动画(十)

简介: 你自己的缓和功能您可以轻松制作自己的缓动功能。所需要的只是一个类型为Func 的方法,它是一个带有double参数和double返回值的函数。这是一个传递函数:它应该为0的参数返回0,并且对于1的参数应该返回1.但是在这两个值之间,任何事情都会发生。

你自己的缓和功能
您可以轻松制作自己的缓动功能。所需要的只是一个类型为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);
    }
}

不幸的是,更容易制作像这样的脱节函数而不是更平滑和更有趣的传递函数。 那些往往有点复杂。
例如,假设您想要一个如下所示的缓动函数:
2019_03_06_085609
它开始快速,然后减速并逆转航向,但随后再次反转以迅速上升到最后一段。
您可能猜测这是一个多项式方程,或者至少它可以用多项式方程近似。 它有两个点,其中斜率为零,这进一步表明这是一个立方体,可以这样表示:

𝑓(𝑡) = 𝑎 ∙ 𝑡 ∙ 𝑡 ∙ 𝑡 + 𝑏 ∙ 𝑡 ∙ 𝑡 + 𝑐 ∙ 𝑡 + 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);

这是动画完成时的结果:
2019_03_06_090454
但是这确实需要一个缓和功能,以便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);

按钮就像这样休息:
2019_03_06_091202
现在让我们将Button龙骨翻过来并倒置,这意味着我们想要围绕右上角旋转按钮。 这需要更改AnchorX和AnchorY属性:

button.AnchorX = 1;
button.AnchorY = 0;

但这是一个问题 - 一个大问题 - 因为AnchorX和AnchorY属性的更改实际上会改变Button的位置。 试试吧! 按钮突然跳起来向右。 Button跳转到的位置恰好是第一个RotateTo基于这些新的AnchorX和AnchorY值时的位置 - 围绕其右上角而不是左下角旋转。
你能想象出来吗? 这是一个小模型,显示按钮的原始位置,按钮从左下角顺时针旋转90度,按钮从右上角顺时针旋转90度:
2019_03_06_091420
当我们设置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>
目录
相关文章
|
存储 程序员
七夕快到了,用SwiftUI做一个表达爱意的心形动画
传统的七夕快到了,作为一个程序猿,最浪漫的礼物当然是自己写的啦! 思来想去也不知道写什么好,在某天在某音上学习时看到点赞的动画效果还不错,那不如就做一个表达爱意的动画吧。
288 0
七夕快到了,用SwiftUI做一个表达爱意的心形动画
|
JavaScript Android开发
第二十二章:动画(二十一)
使用AnimationExtensions为什么ViewExtensions不包含ColorTo动画? 这种方法没有你最初假设的那么明显有三个可能的原因:首先,VisualElement定义的唯一Color属性是BackgroundColor,但通常不是要设置动画的Color属性。
545 0
|
JavaScript Android开发
第二十二章:动画(二十)
实现贝塞尔动画一些图形系统实现动画,该动画沿着贝塞尔曲线移动视觉对象,甚至(可选地)旋转视觉对象,使其保持与曲线相切。Bezier曲线以法国工程师兼数学家PierreBézier的名字命名,他在雷诺工作期间开发了用于汽车车身交互式计算机辅助设计的曲线。
617 0
|
JavaScript Android开发
第二十二章:动画(十九)
更多你自己的等待方法之前,您已经了解了如何将TaskCompletionSource与Device.StartTimer一起使用来编写自己的异步动画方法。 您还可以将TaskCompletionSource与Animation类结合使用,编写自己的异步动画方法,类似于ViewExtensions类中的方法。
629 0
|
Android开发
第二十二章:动画(十八)
超越高级动画方法你到目前为止看到的ConcurrentAnimations中的例子仅限于Scale和Rotate属性的动画,因此它们没有显示任何你无法做的事情。ViewExtensions类中的方法。
702 0
|
Android开发
第二十二章:动画(十七)
子动画ConcurrentAnimations中的前两个示例是单个动画。 Animation类还支持子动画,这就是标记为“Animation 3”的Button的处理程序。 它首先使用无参数构造函数创建父动画对象。
681 0
|
JavaScript Android开发
第二十二章:动画(十五)
深入动画 在第一次遇到时,完整的Xamarin.Forms动画系统可能会有点混乱。 让我们从可用于定义动画的三个公共类的全局视图开始。整理课程除了Easing类之外,Xamarin.Forms动画系统还包含三个公共类。
823 0
|
JavaScript Android开发
第二十二章:动画(十六)
使用Animation类让我们对Animation类进行一些实验。 这涉及实例化Animation类型的对象,然后调用Commit,它实际上开始动画。 Commit方法不返回Task对象; 相反,Animation类完全通过回调提供通知。
705 0
|
JavaScript Android开发
第二十二章:动画(九)
缓解功能你已经看过以下关键帧动画,它以一种方式摆动Button,然后是其他: async void OnButtonClicked(object sender, EventArgs args) { await button.RotateTo(90, 250); await button.RotateTo(-90, 500); await button.RotateTo(0, 250); } 但动画看起来并不合适。
941 0
|
JavaScript Android开发
第二十二章:动画(十四)
你自己的等待动画在本章的下一节中,您将看到Xamarin.Forms实现的基础动画基础结构。这些底层方法允许您定义自己的动画函数,这些函数返回Task对象,并且可以与await一起使用。在第20章“异步和文件I / O”中,您了解了如何使用静态Task.Run方法创建执行的辅助线程,以执行像Mandelbrot计算这样的密集后台作业。
711 0