UWP中使用Composition API實現吸頂(2)

来源:http://www.cnblogs.com/blue-fire/archive/2017/06/13/6999351.html
-Advertisement-
Play Games

使用Composition API實現Pivot中多個頁吸頂。 ...


在上一篇中我們討論了不涉及Pivot的吸頂操作,但是一般來說,吸頂的部分都是Pivot的Header,所以在此我們將討論關於Pivot多個Item關聯同一個Header的情況。

老樣子,先做一個簡單的頁面,頁面有一個Grid當Header,一個去掉了頭部的Pivot,Pivot內有三個ListView,ListView設置了和頁面Header高度一致的空白Header。

<Page
    x:Class="TestListViewHeader.TestHeader2"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:TestListViewHeader"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <Pivot ItemsSource="{x:Bind ItemSource}" x:Name="_pivot" SelectionChanged="_pivot_SelectionChanged" >
            <Pivot.Template>
               <!--太長在這兒就不貼了-->
            </Pivot.Template>
            <Pivot.HeaderTemplate>
                <DataTemplate></DataTemplate>
            </Pivot.HeaderTemplate>
            <Pivot.ItemTemplate>
                <DataTemplate>
                    <ListView ItemsSource="{Binding }">
                        <ListView.Header>
                            <Grid Height="150" />
                        </ListView.Header>
                        <ListView.ItemTemplate>
                            <DataTemplate>
                                <TextBlock Text="{Binding }" />
                            </DataTemplate>
                        </ListView.ItemTemplate>
                    </ListView>
                </DataTemplate>
            </Pivot.ItemTemplate>
        </Pivot>
        <Grid Height="150" VerticalAlignment="Top" x:Name="_header">
            <Grid.RowDefinitions>
                <RowDefinition Height="100" />
                <RowDefinition Height="50" />
            </Grid.RowDefinitions>
            <Grid Background="LightBlue">
                <TextBlock FontSize="30" VerticalAlignment="Center" HorizontalAlignment="Center">我會被隱藏</TextBlock>
            </Grid>
            <Grid Grid.Row="1">
                <ListBox SelectedIndex="{x:Bind _pivot.SelectedIndex,Mode=TwoWay}" ItemsSource="{x:Bind ItemSource}">
                    <ListBox.ItemTemplate>
                        <DataTemplate>
                            <Grid>
                                <TextBlock Text="{Binding Title}" />
                            </Grid>
                        </DataTemplate>
                    </ListBox.ItemTemplate>
                    <ListBox.ItemsPanel>
                        <ItemsPanelTemplate>
                            <VirtualizingStackPanel Orientation="Horizontal" />
                        </ItemsPanelTemplate>
                    </ListBox.ItemsPanel>
                </ListBox>
            </Grid>
        </Grid>
    </Grid>
</Page>

Pivot的模板太長在這兒就不寫了,需要的話,找個系統內置的畫筆資源按F12打開generic.xaml,然後搜索Pivot就是了,其他控制項的模板也能通過這個方法獲取。

模板里修改這幾句就能去掉頭部:

<PivotPanel x:Name="Panel" VerticalAlignment="Stretch">
    <Grid x:Name="PivotLayoutElement">
    <Grid.RowDefinitions>
        <RowDefinition Height="0" />
        <RowDefinition Height="*" />
<!--太長不寫-->
</Grid.RowDefinitions>

然後是後臺代碼,這裡還會用到上一篇的FindFirstChild方法,在這兒就不貼出來了。

老樣子,全局的_headerVisual,最好在Page的Loaded事件里初始化我們所需要的這些變數,我偷懶了,直接放到了下麵的UpdateAnimation方法里。
然後我們寫一個UpdateAnimation方法,用來在PivotItem切換的時候更新動畫的參數。

先判斷下如果未選中頁就return,然後獲取到當前選中項的容器,再像上次一樣從容器里獲取ScrollViewer,不過這裡有個坑,稍後再說。

void UpdateAnimation()
{
    if (_pivot.SelectedIndex == -1) return;
    var SelectionItem = _pivot.ContainerFromIndex(_pivot.SelectedIndex) as PivotItem;
    if (SelectionItem == null) return;
    var _scrollviewer = FindFirstChild<ScrollViewer>(SelectionItem);
    if (_scrollviewer != null)
    {
        _headerVisual = ElementCompositionPreview.GetElementVisual(_header);
        var _manipulationPropertySet = ElementCompositionPreview.GetScrollViewerManipulationPropertySet(_scrollviewer);
        var _compositor = Window.Current.Compositor;

        var line = _compositor.CreateCubicBezierEasingFunction(new System.Numerics.Vector2(0, 0), new System.Numerics.Vector2(0.6f, 1));
        var _headerAnimation = _compositor.CreateExpressionAnimation("_manipulationPropertySet.Translation.Y > -100f ? _manipulationPropertySet.Translation.Y: -100f");
        _headerAnimation.SetReferenceParameter("_manipulationPropertySet", _manipulationPropertySet);
        _headerVisual.StartAnimation("Offset.Y", _headerAnimation);
    }
}

然後在Pivot的SelectionChanged事件里更新動畫:

private void _pivot_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    UpdateAnimation();
}

點下運行,上下滑一下,並沒有跟著動。左右切換一下之後,發現在第二次切換到PivotItem的時候就可以跟著動了,下斷看到第一次運行到"var _scrollviewer = FindFirstChild<ScrollViewer>(SelectionItem);"的時候_scrollviewer為null。想了好久才意識到,是不是控制項沒有Loaded的問題,所以才取不到子控制項?說改就改。

void UpdateAnimation()
{
    if (_pivot.SelectedIndex == -1) return;
    var SelectionItem = _pivot.ContainerFromIndex(_pivot.SelectedIndex) as PivotItem;
    if (SelectionItem == null) return;
    var _scrollviewer = FindFirstChild<ScrollViewer>(SelectionItem);
    if (_scrollviewer != null)
    {
        _headerVisual = ElementCompositionPreview.GetElementVisual(_header);
        var _manipulationPropertySet = ElementCompositionPreview.GetScrollViewerManipulationPropertySet(_scrollviewer);
        var _compositor = Window.Current.Compositor;

        var line = _compositor.CreateCubicBezierEasingFunction(new System.Numerics.Vector2(0, 0), new System.Numerics.Vector2(0.6f, 1));
        var _headerAnimation = _compositor.CreateExpressionAnimation("_manipulationPropertySet.Translation.Y > -100f ? _manipulationPropertySet.Translation.Y: -100f");
        _headerAnimation.SetReferenceParameter("_manipulationPropertySet", _manipulationPropertySet);
        _headerVisual.StartAnimation("Offset.Y", _headerAnimation);
    }
    else
        SelectionItem.Loaded += (s, a) =>
        {
            UpdateAnimation();
        };
}

再次運行,跟著動了。但是還有個問題,在每次切換的時候,Header都會回歸原位一次。這又是一個坑。
猜想在切換PivotItem的時候,_manipulationPropertySet.Translation.Y會有一個瞬間變成0。我踩過的坑大家就不要再踩了。
嘗試更新動畫前先停止動畫。

private void _pivot_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    _headerVisual?.StopAnimation("Offset.Y");
    UpdateAnimation();
}

運行,果然失敗了。
這時靈光一閃,動畫播放時需要時間的啊!這個切換的動畫大概是五步:

  1. 觸發SelectionChanged;
  2. 頁面左移並且逐漸消失;
  3. 卸載頁面;
  4. 裝載新頁面;
  5. 頁面從右側移動到中心並且逐漸顯現。

第一步開始之前觸發了SelectionChanged,然後停止動畫,更新動畫,我表達式動畫都開始播放了,他的第一步還慢悠悠的沒有走完...
簡單,在SelectionChanged裡加延時,就能解決(是嗎)Header歸位的問題(這裡又埋下一個坑):

private async void _pivot_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    _headerVisual?.StopAnimation("Offset.Y");
    await Task.Delay(180);
    UpdateAnimation();
}

運行,很完美。然後在手機上試了一下,差點哭了。
對於點擊和觸摸兩種操作方式,切換頁時觸發事件和播放動畫的順序不一樣!
觸摸造成的切換頁,大概是如下幾步:

  1. 滑動造成頁面位移,鬆手後頁面左移並且逐漸消失
  2. 觸發SelectionChanged;
  3. 卸載頁面;
  4. 裝載新頁面;
  5. 頁面從右側移動到中心並且逐漸顯現。

可是頁面消失後_manipulationPropertySet.Translation.Y會有一瞬間變成0啊!這時候的我真的是崩潰的,不過最後還是給我想出瞭解決方案。
_manipulationPropertySet.Translation.Y變成0的時候不理他不就好了,不能再機智。這樣也不需要SelectionChanged里寫延時了,感覺自己的代碼一下子變得優雅了很多呢。
修改_headerAnimation的表達式:

//var _headerAnimation = _compositor.CreateExpressionAnimation("_manipulationPropertySet.Translation.Y > -100f ? (_manipulationPropertySet.Translation.Y == 0?This.CurrentValue :_manipulationPropertySet.Translation.Y) : -100f");
//整理之後如下
var _headerAnimation = _compositor.CreateExpressionAnimation("Clamp(_manipulationPropertySet.Translation.Y,-100f,_manipulationPropertySet.Translation.Y == 0?This.CurrentValue : 0f)");
//註:This.CurrentValue是表達式動畫中固定的三個變數之一,代表設定動畫的屬性的當前值。另外兩個分別是This.StartingValue,代表動畫開始的值;還有Pi,代表圓周率...
//註2:Clamp(value,min,max),如果value小於min,則返回min;如果value大於max,則返回max;在兩者之間則返回value本身。

註:其中max,min,clamp都是表達式動畫中內置的函數,相關的信息可以查看附錄

再進行測試,完美通過,又填好一個坑。玩弄了這個Demo一會兒後,總覺得還有些不足,左右切換頁的時候,頭部上下移動太生硬了。我的設想是在調整頭部位置動畫的Complate事件里開始頭部的表達式動畫,說乾咱就乾:

var line = _compositor.CreateCubicBezierEasingFunction(new System.Numerics.Vector2(0, 0), new System.Numerics.Vector2(0.6f, 1));
var MoveHeaderAnimation = _compositor.CreateScalarKeyFrameAnimation();
MoveHeaderAnimation.InsertExpressionKeyFrame(0f, "_headerVisual.Offset.Y", line);
MoveHeaderAnimation.InsertExpressionKeyFrame(1f, "_manipulationPropertySet.Translation.Y > -100f ? _manipulationPropertySet.Translation.Y: -100f", line);
MoveHeaderAnimation.SetReferenceParameter("_headerVisual", _headerVisual);
MoveHeaderAnimation.SetReferenceParameter("_manipulationPropertySet", _manipulationPropertySet);
MoveHeaderAnimation.DelayTime = TimeSpan.FromSeconds(0.18d);
MoveHeaderAnimation.Duration = TimeSpan.FromSeconds(0.1d);

創建一個關鍵幀動畫,line是緩動效果。關鍵幀動畫ScalarKeyFrameAnimation可以插入兩種幀,一種是InsertKeyFrame(float,float,easingfunctuin),插入一個數值幀;一種是InsertExpressionKeyFrame(float,string,easingfunctuin),插入一個表達式幀,兩者的第一個參數是進度,最小是0最大是1;第三個參數都是函數,可以設置為線性,貝塞爾曲線函數和步進。

這時候就又發現了一個驚!天!大!秘!密!
CompositionAnimation和CompositionAnimationGroup是沒有Complated事件的!
只能手動給延時了。然後...
表達式動畫不!支!持!延!時!好尷尬。

同樣是動畫,看看隔壁家的StoryBoard,CompositionAnimation你們羞愧不羞愧。

經過一番必應之後,我發現我錯怪了他們,CompositionAnimation也可以做到Complated事件,只是方法有些曲折而已。

動畫完成事件

通過使用關鍵幀動畫,開發人員可以在完成精選動畫(或動畫組)時使用動畫批來進行聚合。 僅可以批處理關鍵幀動畫完成事件。 表達式動畫沒有一個確切終點,因此它們不會引發完成事件。 如果表達式動畫在批中啟動,該動畫將會像預期那樣執行,並且不會影響引發批的時間。

當批內的所有動畫都完成時,將引發批完成事件。 引發批的事件所需的時間取決於該批中時長最長的動畫或延遲最為嚴重的動畫。 在你需要瞭解選定的動畫組將於何時完成以便計劃一些其他工作時,聚合結束狀態非常有用。

批在引發完成事件後釋放。 還可以隨時調用 Dispose() 來儘早釋放資源。 如果批處理的動畫結束較早,並且你不希望繼續完成事件,你可能會想要手動釋放批對象。 如果動畫已中斷或取消,將會引發完成事件,並且該事件會計入設置它的批。 

在動畫開始前,新建一個ScopedBatch對象,然後播放動畫,緊接著關閉ScopedBatch,動畫運行完之後就會觸發ScopedBatch的Completed事件。在ScopedBatch處於運行狀態時,會收集所有動畫,關閉後開始監視動畫的進度。說的雲里來霧裡去的,還是看代碼吧。

var Betch = _compositor.CreateScopedBatch(Windows.UI.Composition.CompositionBatchTypes.Animation);
_headerVisual.StartAnimation("Offset.Y", MoveHeaderAnimation);
Betch.Completed += (s, a) =>
{
    var _headerAnimation = _compositor.CreateExpressionAnimation("_manipulationPropertySet.Translation.Y > -100f ? (_manipulationPropertySet.Translation.Y == 0?This.CurrentValue :_manipulationPropertySet.Translation.Y) : -100f");
    //_manipulationPropertySet.Translation.Y是ScrollViewer滾動的數值,手指向上移動的時候,也就是可視部分向下移動的時候,Translation.Y是負數。

    _headerAnimation.SetReferenceParameter("_manipulationPropertySet", _manipulationPropertySet);
    _headerVisual.StartAnimation("Offset.Y", _headerAnimation);
};
Betch.End();

我們把構造和播放_headerAnimation的代碼放到了ScopedBatch的Complated事件里,這時再運行一下,完美。

其實還是有點小問題,比如Header沒有設置Clip,上下移動的時候有時會超出預期的範圍之類的,有時間我們會繼續討論,這篇已經足夠長,再長會嚇跑人的。
Demo已經放到Github,裡面用到了一個寫的很糙的滑動返回控制項,等忙過這段時間整理下代碼就開源,希望能有大牛指點一二。

Github:https://github.com/cnbluefire/TestListViewHeader

總結一下,實現吸頂最核心的代碼就是獲取到ScrollViewer,不一定要是ListView的,明白了這一點,所有含有ScrollViewer的控制項都可以放到這個這個頁面使用。

滑動返回:


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 很多人都說使用反射會有性能問題,那到底會比直接調用慢多少呢,下麵就來測試一下。 直接調用vs反射調用 下麵就來寫個demo來驗證下直接調用和反射調用的性能差異,代碼如下: 測試結果: 從100萬次調用結果來看,確實就像很多人所說的,兩者在性能上具有數量級的差距。 為什麼反射有性能損失 既然反射性能有 ...
  • C#編譯器對靜態類進行瞭如下限制: 1,靜態類必須直接從基類System.Obect派生,從其他任何基類派生都沒有意義。繼承只適用於對象,而你不能創建靜態類的實例 2,靜態類不能實現任何介面,這是因為只有適用類的實例時,才可調用介面方法 3,靜態類只能定義靜態成員(欄位,方法,屬性和事件),任何實例 ...
  • 前面總結了反射的使用,這一篇結合一個完整的項目來總結下反射的實際應用。 項目結構 如下圖: 定義插件介面 在項目ConsoleApplication6.IService中,定義了兩個介面,Run代表行駛,Trun代表轉向,如下代碼: 插件程式實現 這裡新建了兩個項目分別實現插件程式,分別是Conso ...
  • 裝箱: 值類型比引用類型“輕”,原因是他們不作為對象在托管堆中分配,不被垃圾回收,也不通過指針進行引用。但是許多時候都需要獲取值類型的引用,例如,假定要創建ArrayList對象來容納一組point結構,代碼如下: public sealed class Program { public stati ...
  • 經過我三篇文章的解惑,webapi我相信大家沒有問題了! 先創建了一個UserModel 然後添加Web API Controller 註冊路由 在Global中註冊 這個時候用地址欄訪問地址:api/user/getadmin 這個時侯預設返回的是XML數據模型。 使用AJAX請求這個api,指定 ...
  • 由於公司的工作安排,一直在研究其他技術,所以一直沒時間更新博客,今天終於可以停下手頭的事情,寫一些新內容了。 應用場景:企業門戶網站會根據內容不同,設置不同的板塊,如新浪有體育,娛樂頻道,等等。有的情況下需要給不同的板塊設置不同的二級功能變數名稱,如新浪體育sports.sina.com.cn。 在asp. ...
  • 大學畢業已三年,菜鳥稱謂依然。畢業前使用過六個月的MVC,但是自從畢業後因為公司一直在用webForm,所以MVC就沒有再用過。直到最近打算用MVC做一個項目管理系統,才發現MVC已經變得陌生了,只有再從新學起。為了防止自己的拖延症拖延自己的學習計劃,特在博客園寫此文。學習期間,所有的感悟和整理的可 ...
  • 這個項目深刻的為我們講解了pc客戶端如何請求webapi 相信大家在看了我轉載的第一篇文章和這篇文章之後,對webapi再也不會懼怕了! 討論webapi的知識,歡迎加我微信 jkxx123321 交流 WCF的野心造成了它的龐大複雜,HTTP的單純造就了它的簡單優美。為了實現分散式Web應用,我們 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...