第二十二章:動畫(十四)

你自己的等待動畫
在本章的下一節中,您將看到Xamarin.Forms實現的基礎動畫基礎結構。這些底層方法允許您定義自己的動畫函數,這些函數返回Task對象,並且可以與await一起使用。
在第20章“異步和文件I / O”中,您瞭解瞭如何使用靜態Task.Run方法創建執行的輔助線程,以執行像Mandelbrot計算這樣的密集後臺作業。 Task.Run方法返回一個Task對象,該對象可以在後臺作業完成時發出信號。
但動畫並不是那樣的。動畫不需要花費大量時間來處理數字。它只需要做一些非常簡單和簡單的事情 - 比如設置一個Rotation屬性 - 每16毫秒一次。該作業可以在用戶界面線程中運行 - 事實上,實際的屬性訪問必須在用戶界面線程中運行 - 並且可以使用Device.StartTimer或Task.Delay來處理時間。
您不應該使用Task.Run來實現動畫,因爲執行的輔助線程是不必要的並且是浪費的。但是,當您實際坐下來編寫類似於Xamarin.Forms動畫方法(如RotateTo)的動畫方法時,您可能會遇到障礙。該方法必須返回一個Task對象,並且可能使用Device.StartTimer作爲計時,但這似乎不可能。
這是第一次嘗試編寫這樣的方法。 參數包括目標VisualElement,from和to值以及持續時間。 它使用Device.StartTimer和Stopwatch來計算Rotation屬性的當前設置,並在動畫完成時退出Device.StartTimer回調:

Task MyRotate(VisualElement visual, double fromValue, double toValue, uint duration)
{
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();
    Device.StartTimer(TimeSpan.FromMilliseconds(16), () =>
    {
        double t = Math.Min(1, stopwatch.ElapsedMilliseconds / (double)duration);
        double value = fromValue + t * (toValue - fromValue);
        visual.Rotation = value;
        bool completed = t == 1;
 
        if (completed)
        {
            // Need to signal that the Task has completed. But how?
        }
        return !completed; 
    });
    // Need to return a Task object here but where does it come from?
}

在兩個關鍵點上,該方法不知道該怎麼做。在方法調用Device.StartTimer之後,它需要退出並將Task對象返回給調用者。但是這個Task對象來自哪裏? Task類有一個構造函數,但是像Task.Run一樣,該構造函數創建了第二個執行線程,並且沒有理由創建該線程。此外,當動畫結束時,該方法需要以某種方式表示任務已完成。
幸運的是,存在一個完全符合您要求的類。 它叫做TaskCreationSource。 它是一個泛型類,其中type參數與要創建的Task對象的type參數相同。 askCreationSource對象的Task屬性提供了您需要的Task對象。 這是您的異步方法返回的內容。 當您的方法完成處理後臺作業時,它可以在TaskCreationSource對象上調用SetResult,表示作業已完成。
以下TryAwaitableAnimation程序顯示如何在從Button的Clicked處理程序調用的MyRotateTo方法中使用TaskCreationSource:

public partial class TryAwaitableAnimationPage : ContentPage
{
    public TryAwaitableAnimationPage()
    {
        InitializeComponent();
    }
    async void OnButtonClicked(object sender, EventArgs args)
    {
        Button button = (Button)sender;
        uint milliseconds = UInt32.Parse((string)button.StyleId);
        await MyRotate(button, 0, 360, milliseconds);
    }
    Task MyRotate(VisualElement visual, double fromValue, double toValue, uint duration)
    {
        TaskCompletionSource<object> taskCompletionSource = new TaskCompletionSource<object>();
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        Device.StartTimer(TimeSpan.FromMilliseconds(16), () =>
            {
                double t = Math.Min(1, stopwatch.ElapsedMilliseconds / (double)duration);
                double value = fromValue + t * (toValue - fromValue);
                visual.Rotation = value;
                bool completed = t == 1;
 
                if (completed)
                {
                    taskCompletionSource.SetResult(null);
                }
                return !completed; 
        });
        return taskCompletionSource.Task;
    }
}

注意TaskCreationSource的實例化,該對象的Task屬性的返回值,以及動畫完成後對Device.StartTimer回調內的SetResult的調用。
TaskCreationSource沒有非通用形式,因此如果您的方法只返回Task對象而不是Task 對象,則在定義TaskCreationSource實例時需要指定類型。 按照慣例,您可以使用object來實現此目的,在這種情況下,您的方法使用null參數調用SetResult。
TryAwaitableAnimation XAML文件實例化共享此Clicked處理程序的三個Button元素。 它們中的每一個都將自己的動畫持續時間定義爲StyleId屬性。 (正如您所記得的,StyleId不在Xamarin.Forms中使用,僅供應用程序員使用,作爲將任意數據附加到元素的便捷方式。)

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="TryAwaitableAnimation.TryAwaitableAnimationPage">
    <StackLayout>
        <StackLayout.Resources>
            <ResourceDictionary>
                <Style TargetType="Button">
                    <Setter Property="Text" Value="Tap Me!" />
                    <Setter Property="FontSize" Value="Large" />
                    <Setter Property="HorizontalOptions" Value="Center" />
                    <Setter Property="VerticalOptions" Value="CenterAndExpand" />
                </Style>
            </ResourceDictionary>
        </StackLayout.Resources>
        <Button Clicked="OnButtonClicked" StyleId="5000" />
        <Button Clicked="OnButtonClicked" StyleId="2500" />
        <Button Clicked="OnButtonClicked" StyleId="1000" />
    </StackLayout>
</ContentPage>

即使這些Button元素中的每一個都通過調用MyRotate來設置動畫,您也可以讓所有按鈕同時旋轉。每次調用MyRotate都會獲得自己的局部變量集,並在每個Device.StartTimer回調中使用這些局部變量。
但是,如果在按鈕仍處於旋轉狀態時點擊按鈕,則會向該按鈕應用第二個動畫,並且兩個動畫相互爭鬥。代碼需要的是在應用新動畫時取消上一個動畫的方法。
一種方法是MyRotate方法維護Dictionary 定義爲字段。每當它開始動畫時,MyRotate都會將目標VisualElement作爲該字典的鍵添加,其值爲false。動畫結束時,它會從字典中刪除此條目。一個單獨的方法(可能名爲CancelMyRotate)可以將字典中的值設置爲true,這意味着取消動畫。 Device.StartTimer回調可以通過檢查特定VisualElement的字典值開始,如果動畫已被取消,則從回調返回false。但是你會在討論中發現如何用更少的代碼來完成它。
現在您已經看到了ViewExtensions類中實現的高級動畫函數,讓我們來探討Xamarin.Forms動畫系統的其餘部分如何實現這些函數
並允許您啓動,控制和取消動畫。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章