UWP Composition API - PullToRefresh


背景:

之前用ScrollViewer 來做過 PullToRefresh的控件,在項目一些特殊的條件下總有一些問題,比如ScrollViewer不會及時到達指定位置。
於是便有了使用Composition API來重新實現PullToRefresh控件。本控件的難點不是實現,而是對Composition API的一些探索。

本文的一些觀點或者說結論不一定是全對的,都是通過實驗得到的,Composition API 可用的資料實在是太少了。

成品效果圖:

 

資料:

Composition API 資料

1.官方Sample

2. 原作者 Nick Waggoner 供職於微軟 native Windows UI platform 鏈接是對文章的翻譯 VALID VOID 

3.比較舊的資料


在網上查閱了些資料,看到網上有大神已經實現過了。了解了大概的實現過程,因為自己想做的效果還是跟大神的有差距的,所以

還是自己動手封裝成控件。

實現原理:

這里引用 VALID VOID里面的話

輸入驅動動畫

 自大約五年前觸摸漸成主流起,創造低延遲體驗成為了一種普遍需求。使用手指或筆在屏幕上操作,使得人眼獲得了更直觀的參照點來辨識操作的延遲和流暢性。為使操作流暢,主流操作系統公司均將更多的操作移交至系統和 GPU (如 ChromeIE)執行。在 Windows 上,這由 DirectManipulation 這一或多或少是針對於觸摸構建的動畫引擎實現的。它解決了關鍵的延遲挑戰,也就是如何自然地以展示從輸入驅動到事件驅動過渡的動效。但另一方面,它也幾乎沒有提供對定制慣性觀感的支持,就像福特 T 型車那樣——“只要車是黑色的,你可以把它塗成任意你喜歡的顏色”。2

 

ElementCompositionPreview.GetScrollViewerManipulationPropertySet 是讓你能夠把玩輸入驅動動效的第一步。雖然它仍然沒給你任何對內容滾動時觀感進行控制的額外能力,但它確實允許你對次級內容應用表達式動畫。例如,我們終於能完成我們的基礎視差滾動代碼:

 

// 創建驅動視差滾動的表達式動畫。 ExpressionAnimation parallaxAnimation = compositor.CreateExpressionAnimation("MyForeground.Translation.Y / MyParallaxRatio"); // 設置對前景對象的引用。 CompositionPropertySet MyPropertySet = ElementCompositionPreview.GetScrollViewerManipulationPropertySet(MyScrollViewer); parallaxAnimation.SetReferenceParameter("MyForeground", MyPropertySet); // 設置背景對象視差滾動的速度。 parallaxAnimation.SetScalarParameter("MyParallaxRatio", 0.5f); // 對背景對象開始視差動畫。 backgroundVisual.StartAnimation("Offset.Y", parallaxAnimation); 

 

使用這一技巧,你能夠實現多種優秀的效果:視差滾動、粘性表頭、自定義滾動條等等。唯一缺失的就是定制操作本身的觀感……

我講一下我的理解:使用過ScrollViewer 和Manipulation相關事件的童鞋都知道,想要得到一些ScrollViewer 觸摸的詳情太難了,

DirectManipulationStarted和DirectManipulationCompleted得到信息太少了,而Manipulation其他的事件又需要設置ManipulationMode,這樣全部的情況都要你自己來處理。當看到

ElementCompositionPreview.GetScrollViewerManipulationPropertySet(MyScrollViewer); 

的時候,你是不是感覺有點親切。看上去你拿到了ScrollViewer 的一些Manipulation 的信息。。話說這里是最最坑爹了,

MyForeground 其實是GetScrollViewerManipulationPropertySet返回的東東,

但MyForeground.Translation.Y這是什么鬼東西。。Manipulation 的Translation??? 后來我在網上搜索了一下,

但我查了下CompositionPropertySet 並沒發現有相關Translation的屬性啊? 

難道跟Manipulation事件里面的參數里面的Translation 是一樣的嗎??這是我的推測。

網上都是這樣用的,但是沒有文檔。。除了Translation不知道還有其他屬性能使用不。

暫時沒有尋找到答案,希望知道的童鞋可以留言,萬分感激。。。

上面這段的代碼的意思就是說把Manipulation.Translation.Y 映射到backgroundVisual的Offset.Y上面。

也就是說你現在已經可以找到ScrollViewer滾動的時候一些有用的數值了。。值得說的是,用鼠標滾動的時候 映射依然能生效。

這種映射是實時的,是會有慣性效果的。

實現過程:

因為要用到ScrollViewer,所以我做了2種,一種是刷新內容第一元素是ScrollViewer的,一種不是ScrollViewer的。

如果刷新內容第一元素是ScrollViewer,模板如下:

  <ControlTemplate TargetType="local:PullToRefreshGrid1">
                    <Grid>
                        <ContentControl x:Name="Header" Opacity="0" VerticalAlignment="Top" ContentTemplate="{TemplateBinding HeaderTemplate}" HorizontalContentAlignment="Center" VerticalContentAlignment="Bottom" />
                        <ContentPresenter x:Name="Content" ContentTemplate="{TemplateBinding ContentTemplate}" ContentTransitions="{TemplateBinding ContentTransitions}" Content="{TemplateBinding Content}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                    </Grid>
                </ControlTemplate>

如果刷新內容第一元素不是ScrollViewer,那么我為它添加了一個ScrollViewer:

  <ControlTemplate TargetType="local:PullToRefreshGrid">
                    <Grid>
                        <ContentControl x:Name="Header" Opacity="0" VerticalAlignment="Top" ContentTemplate="{TemplateBinding HeaderTemplate}" HorizontalContentAlignment="Center" VerticalContentAlignment="Bottom" />
                        <ScrollViewer x:Name="ScrollViewer" VerticalSnapPointsType="MandatorySingle"  VerticalSnapPointsAlignment="Near"
                          VerticalScrollMode="Enabled" VerticalScrollBarVisibility="Hidden" VerticalContentAlignment="Stretch" VerticalAlignment="Stretch">
                            <ContentPresenter x:Name="Content" ContentTemplate="{TemplateBinding ContentTemplate}" ContentTransitions="{TemplateBinding ContentTransitions}" Content="{TemplateBinding Content}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                        </ScrollViewer>
                    </Grid>
                </ControlTemplate>

當然,其實第2種也是統用的,我只是想減少控件的Child,如果你不知道刷新內容里面有沒有ScrollViewer,那么用第2種就好了。有人會說為啥控件的名字這么奇怪。。那是因為我之前也寫過PullToRefresh控件(PullToRefreshControl,PullToRefreshPanel),我把名字都想完了。。實在想不出更好的了。。大家體諒下。。( ╯□╰ )

好了,重點就是拿到這個ScrollViewer 然后跟Header,產生某種關系(你懂的)。。。

            if (RefreshThreshold == 0.0)
            {
                RefreshThreshold = headerHeight;
            }
            ratio = RefreshThreshold / headerHeight;

            _offsetAnimation = _compositor.CreateExpressionAnimation("(min(max(0, ScrollManipulation.Translation.Y * ratio) / Divider, 1)) * MaxOffsetY");
            _offsetAnimation.SetScalarParameter("Divider", (float)RefreshThreshold);
            _offsetAnimation.SetScalarParameter("MaxOffsetY", (float)RefreshThreshold * 5 / 4);
            _offsetAnimation.SetScalarParameter("ratio", (float)ratio);
            _offsetAnimation.SetReferenceParameter("ScrollManipulation", _scrollerViewerManipulation);

            _opacityAnimation = _compositor.CreateExpressionAnimation("min((max(0, ScrollManipulation.Translation.Y * ratio) / Divider), 1)");
            _opacityAnimation.SetScalarParameter("Divider", (float)headerHeight);
            _opacityAnimation.SetScalarParameter("ratio", (float)1);
            _opacityAnimation.SetReferenceParameter("ScrollManipulation", _scrollerViewerManipulation);

            _headerVisual = ElementCompositionPreview.GetElementVisual(_header);

            _contentVisual = ElementCompositionPreview.GetElementVisual(_scrollViewerBorder);

            _headerVisual.StartAnimation("Offset.Y", _offsetAnimation);
            _headerVisual.StartAnimation("Opacity", _opacityAnimation);
            _contentVisual.StartAnimation("Offset.Y", _offsetAnimation);

RefreshThreshold 是到達Release to refresh的一個點。。可以由用戶設定,默認是header的高度。

 

MaxOffsetY 是當到達RefreshThreshold之后我還能拖動的最大值,這里設置為RefreshThreshold 的5/4。

(min(max(0, ScrollManipulation.Translation.Y * ratio) / Divider, 1)) * MaxOffsetY

我再來講講這個表達式的意思,ScrollManipulation.Translation.Y 大家都已經知道了。是ScrollViewer進行Manipulation的Y值,向下是正值,向上時負值,初始為0.

綜合起來就是最大值為MaxOffsetY最小值為0.。這個速率是根據RefreshThreshold/headerHeight來的,因為你會發現,你向下drag scrollViewer的時候是有一定最大值的,當RefreshThreshold比較大的時候,你很難ScrollManipulation.Translation.Y值達到RefreshThreshold。

min((max(0, ScrollManipulation.Translation.Y * ratio) / Divider), 1)

這個也比較簡單,是給header的Opaicty做了一個動畫。

            _headerVisual.StartAnimation("Offset.Y", _offsetAnimation);
            _headerVisual.StartAnimation("Opacity", _opacityAnimation);
            _contentVisual.StartAnimation("Offset.Y", _offsetAnimation);

完成之后我們將動畫開始就好了。這樣在向下拖scrollviewer的時候,scrollviewer和header就都會向下移動。

接下來我們需要監聽 offset。

        private void ScrollViewer_DirectManipulationStarted(object sender, object e)
        {
            Windows.UI.Xaml.Media.CompositionTarget.Rendering += OnCompositionTargetRendering;
            _refresh = false;
            _header.Opacity = 1;
        }

在開始manipulat的時候,注冊CompositionTarget.Rendering事件。在這個事件里面我們就可以實時獲得offset的變化了。

         private void OnCompositionTargetRendering(object sender, object e)
        {
            _headerVisual.StopAnimation("Offset.Y");

            var offsetY = _headerVisual.Offset.Y;
            IsReachThreshold = offsetY >= RefreshThreshold;
            _scrollViewerBorder.Clip = new RectangleGeometry() { Rect = new Rect(0, 0, _content.Width, _content.Height - offsetY) };
            Debug.WriteLine(IsReachThreshold + "," + _headerVisual.Offset.Y + "," + RefreshThreshold);
            _headerVisual.StartAnimation("Offset.Y", _offsetAnimation);

            if (!_refresh)
            {
                _refresh = IsReachThreshold;
            }

            if (_refresh)
            {
                _pulledDownTime = DateTime.Now;
            }

            if (_refresh && offsetY <= 1)
            {
                _releaseTime = DateTime.Now;
            }

        }

這里有點坑爹的是,發現如果不StopAnimation,那么Offset.Y永遠都是0.。。很囧啊。。

最后我們在ScrollViewer_DirectManipulationCompleted事件里面處理是否要 觸發PullToRefresh事件就ok了。

         private void ScrollViewer_DirectManipulationCompleted(object sender, object e)
        {
            Windows.UI.Xaml.Media.CompositionTarget.Rendering -= OnCompositionTargetRendering;

            var cancelled = (_releaseTime - _pulledDownTime) > TimeSpan.FromMilliseconds(250);

            if (_refresh)
            {
                _refresh = false;
                if (cancelled)
                {
                    Debug.WriteLine("Refresh cancelled...");
                }
                else
                {
                    Debug.WriteLine("Refresh now!!!");
                    if (PullToRefresh != null)
                    {
                        _headerVisual.StopAnimation("Offset.Y");
                        LastRefreshTime = DateTime.Now;
                        _headerVisual.StartAnimation("Offset.Y", _offsetAnimation);
                        PullToRefresh(this, null);
                    }
                }
            }
        }

最后說下Header模板(HeaderTemplate)是可以定義的。。它的DataContext是綁定到這個控件上的,有用的屬性有(LastRefreshTime,IsReachThreshold等),你可以用它們創造你屬於你喜歡的Header樣式。

通過本控件,初步了解Composition API 的一些用法。下一篇,我會講講一些更多的探索。。

開源有益,源碼GitHub地址

問題:

1.Visual 是繼承IDisposable,我們需要在什么時候Dispose 掉它呢? 還是它自己管理的?

我試過在unload的時候去dispose它。但是會出了Win32的異常。。官方sample里面對這個也講的不清楚。

希望知道的童鞋能留言告知,萬分感謝。另外希望有對這個比較了解的童鞋能提供一些sample,資料,再次感謝。

補充:
使用條件:

    // Windows build 10240 and later. 
    if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 1))
    {
        ...
    }

    // Windows build10586 and later.
    if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 2))
    {
        ...
    }

    // Windows build14332 and later.
    if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 3))
    {
        ...
    }
調試了下
1. Windows build14332 and later: 1,2,3都為true。 
2. Windows build10586 and later: 1,2為true。
3. Windows build 10240 and later: 1為true。

因為10586之前的版本是不支持Composition API的。所以使用的時候記得判斷:

    // Windows build10586 and later.
    if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 2))
    {
        ...
    }

 
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM