• 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);
    

    触摸

    触摸比起鼠标点击要更复杂一些。
    Pivot应该是UWP内置控件里比较玄学的一个了。
    对于鼠标操作,Pivot会先触发SelectionChanged事件,再触发PivotItemLoaded事件,并且播放动画。
    而对于触摸事件,整个顺序是相反的。手指开始滑动界面时,可以被看到的Item会开始加载,并且触发PivotItemLoaded事件,松手之后才开始计算是否应该导航到其他页,并且决定是否触发SelectionChanged事件。这样就会有一个问题,我们在SelectionChanged中修改ScrollViewer偏移之前,我们已经能看到他了,这时的高度是不正确的。我们需要抽象出一个可以在鼠标和触摸触发事件时将下一个Item的ScrollViewer设置为正确偏移的方法。
    我的想法很简单,将所有已加载的页内的ScrollViewer缓存下来,随着Progress的改变而改变,做法也很简单。

    
    private HashSet<ScrollViewer> scrolls = new HashSet<ScrollViewer>();
    private async void Pivot_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        ...
        scrolls.Remove(provider.ScrollViewer);
    }
    
    private void Pivot_PivotItemLoaded(Pivot sender, PivotItemEventArgs args)
    {
        var sv = (args.Item.ContentTemplateRoot as FrameworkElement).FindName("sv") as ScrollViewer;
        if (sv != provider.ScrollViewer)
        {
            sv.ChangeView(null, provider.Progress * provider.Threshold, null, true);
            scrolls.Add(sv);
        }
    }
    
    
    private void Pivot_PivotItemUnloading(Pivot sender, PivotItemEventArgs args)
    {
        var sv = (args.Item.ContentTemplateRoot as FrameworkElement).FindName("sv") as ScrollViewer;
        if (sv != null)
        {
            scrolls.Remove(sv);
        }
    }
    
    private void Provider_ProgressChanged(object sender, double args)
    {
        foreach (var sv in scrolls)
        {
            sv.ChangeView(null, provider.Progress * provider.Threshold, null, true);
        }
    }
    
    
    

    需要注意的是,我们要在加载完成事件中获取ScrollViewer,而在卸载开始事件中移除ScrollViewer。

    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
    我的博客: 超威蓝火

  • 相关阅读:
    记录JavaScript的util.js类库
    Shiro登录中遇到了问题
    【转载】JavaScript导出Excel
    react-router
    react 表单
    html5定位getLocation()
    html5存储方式localstorage和sessionStorage
    position导致Safari工具栏不自动隐藏
    input type="datetime-local" 时placeholder不显示
    vuex(1.0版本写法)
  • 原文地址:https://www.cnblogs.com/blue-fire/p/11376450.html
Copyright © 2020-2023  润新知