1. 程式人生 > >UWP實現吸頂的Pivot

UWP實現吸頂的Pivot

話不多說,先上效果

這裡使用了一個ScrollProgressProvider.cs,我們這篇文章先解析一下整體的動畫思路,以後再詳細解釋這個Provider的實現方式。

結構

整個頁面大致結構是

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>
    <Grid x:Name="Target">
        <TextBlock />
        <Header />
    </Grid>
    <Pivot.ItemTemplate Grid.RowSpan="2">
        <Pivot.ItemTemplate>
            <DataTemplate>
                <ScrollViewer x:Name="sv">
                    <StackPanel>
                        <Border Margin="0,250,0,0" />
                    </StackPanel>
                </ScrollViewer>
            </DataTemplate>
        </DataTemplate>
    </Pivot.ItemTemplate>
</Grid>

這個Header是修改的ListBox,當然也可以用ListView代替。
隱藏Pivot預設Header的方式是在Pivot的樣式中找到如下行。

<PivotPanel x:Name="Panel" VerticalAlignment="Stretch">
    <Grid x:Name="PivotLayoutElement">
        <Grid.RowDefinitions>
            <RowDefinition Height="0" /><!--修改這行為0-->
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
    ...

動畫過程大致就是在Pivot頁面切換時,查詢到當頁的ScrollViewer,繫結動畫。

查詢

大家在爬檢視樹時,應該經常遇到元素還未載入的情況,這裡為了解決這種狀況,封裝了一個WaitForLoaded方法。

private async Task<T> WaitForLoaded<T>(FrameworkElement element, Func<T> func, Predicate<T> pre, CancellationToken cancellationToken)
{
    TaskCompletionSource<T> tcs = null;
    try
    {
        tcs = new TaskCompletionSource<T>();
        cancellationToken.ThrowIfCancellationRequested();
        var result = func.Invoke();
        if (pre(result)) return result;


        element.Loaded += Element_Loaded;

        return await tcs.Task;

    }
    catch
    {
        element.Loaded -= Element_Loaded;
        var result = func.Invoke();
        if (pre(result)) return result;
    }

    return default;


    void Element_Loaded(object sender, RoutedEventArgs e)
    {
        if (tcs == null) return;
        try
        {
            cancellationToken.ThrowIfCancellationRequested();
            element.Loaded -= Element_Loaded;
            var _result = func.Invoke();
            if (pre(_result)) tcs.SetResult(_result);
            else tcs.SetCanceled();
        }
        catch
        {
            System.Diagnostics.Debug.WriteLine("canceled");
        }
    }

}

使用起來是這樣的

CancellationTokenSource cts;
private async void EventChanged(object sender, EventArgs e)
{
    if (cts != null) cts.Cancel();
    cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
    var child = await WaitForLoaded(element, () => find_element_method(), c => judge_find_success_method(), cts.Token);
}

我們在Pivot的SelectionChanged事件裡,修改ScrollProgressProvider託管的ScrollViewer,provider就會自動將ScrollViewer設定到正確的位置。

接下來在Page的Loaded事件中繫結動畫,這裡有兩種選擇。provider提供了ProgressChanged事件和GetProgressPropertySet方法。可以在ProgressChanged事件中直接設定元素的值來實現動畫,不過由於ScrollViewer的限制,ProgressChanged事件觸發頻率不是很高,所以更推薦使用GetProgressPropertySet獲取到CompositionPropertySet,通過Composition Api實現動畫。

var providerProp = provider.GetProgressPropertySet();
var gv = ElementCompositionPreview.GetElementVisual(Target); // 容器Visual
var tv = ElementCompositionPreview.GetElementVisual(HeaderText); //文字Visual

ScrollProgressProvider生成的PropertySet內有progress和threshold兩個欄位可以用作動畫。
Composition Api提供了Lerp(start, end, progress)方法,用在此處剛好合適。
我們需要定義容器平移,文字平移和文字縮放三個動畫。

容器平移向上移動閾值的高度

var gvOffsetExp = Window.Current.Compositor.CreateExpressionAnimation("Vector3(0f, -provider.threshold * provider.progress, 0f)");
gvOffsetExp.SetReferenceParameter("provider", providerProp);
gv.StartAnimation("Offset", gvOffsetExp);

文字平移動畫從容器中心平移到左下角

var startOffset = "Vector3((host.Size.X - this.Target.Size.X) / 2, (host.Size.Y - 50 - this.Target.Size.Y) / 2, 1f)";
var endOffset = $"Vector3(0f, provider.threshold, 1f)";
var offsetExp = Window.Current.Compositor.CreateExpressionAnimation($"lerp({startOffset}, {endOffset}, provider.progress)");
offsetExp.SetReferenceParameter("host", gv);
offsetExp.SetReferenceParameter("provider", providerProp);
tv.StartAnimation("Offset", offsetExp);

文字縮放

var scale = "(50f / this.Target.Size.Y)";
var startScale = "Vector3(1f, 1f, 1f)";
var endScale = $"Vector3({scale}, {scale}, 1f)";
var scaleExp = Window.Current.Compositor.CreateExpressionAnimation($"lerp({startScale}, {endScale}, provider.progress)");
scaleExp.SetReferenceParameter("host", gv);
scaleExp.SetReferenceParameter("provider", providerProp);
tv.StartAnimation("Scale", scaleExp);

GitHub: https://github.com/cnbluefire/ShyHeaderPivot
ExpressionAnimation:
https://docs.microsoft.com/en-us/uwp/api/Windows.UI.Composition.ExpressionAnimation
CompositionAnimation: https://docs.microsoft.com/zh-cn/windows/uwp/composition/composition-animation
我的部落格: 超威藍