天天看點

第二十二章:動畫(十)

你自己的緩和功能

您可以輕松制作自己的緩動功能。所需要的隻是一個類型為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>           

繼續閱讀