UWP Community Toolkit 中有一個為圖片或磁貼提供輪播效果的控制項 - RotatorTile,本篇我們結合代碼詳細講解 RotatorTile 的實現。 RotatorTile 提供了一種類似 Windows 10 磁貼的輪播方式,可以輪流播放開發者設置的內容序列,支持設置輪... ...
概述
UWP Community Toolkit 中有一個為圖片或磁貼提供輪播效果的控制項 - RotatorTile,本篇我們結合代碼詳細講解 RotatorTile 的實現。
RotatorTile 提供了一種類似 Windows 10 磁貼的輪播方式,可以輪流播放開發者設置的內容序列,支持設置輪播方向,包括上下左右四個方向;接下來看看官方示例的截圖:
Doc: https://docs.microsoft.com/zh-cn/windows/uwpcommunitytoolkit/controls/rotatortile
Namespace: Microsoft.Toolkit.Uwp.UI.Controls; Nuget: Microsoft.Toolkit.Uwp.UI.Controls;
開發過程
代碼分析
RotatorTile 控制項包括 RotatorTile.cs 和 RotatorTile.xaml,分別是控制項的定義處理類和樣式文件,分別來看一下:
1. RotatorTile.xaml
RotatorTile.xaml 是 RotatorTile 控制項的樣式文件,我們看 Template 部分,輪播效果的實現主要是靠 StackPanel 中排列的兩個 Content,分別代表 current 和 next 內容,根據設置的輪播方向,設置 StackPanel 的排列方向;輪播時,使用 TranslateTransform 來實現輪播的元素切換動畫;
<Style TargetType="controls:RotatorTile"> <Setter Property="IsTabStop" Value="False"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="controls:RotatorTile"> <Grid Background="{TemplateBinding Background}"> <Canvas x:Name="Scroller" DataContext="{x:Null}"> <StackPanel x:Name="Stack"> <StackPanel.RenderTransform> <TranslateTransform x:Name="Translate" Y="0" /> </StackPanel.RenderTransform> <ContentPresenter x:Name="Current" Content="{Binding}" ContentTemplate="{TemplateBinding ItemTemplate}" DataContext="{x:Null}" /> <ContentPresenter x:Name="Next" Content="{Binding}" ContentTemplate="{TemplateBinding ItemTemplate}" DataContext="{x:Null}" /> </StackPanel> </Canvas> <Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" /> </Grid> </ControlTemplate> </Setter.Value> </Setter> <Setter Property="RotationDelay" Value="0:0:5" /> <Setter Property="ExtraRandomDuration" Value="0:0:5" /> </Style>
2. RotatorTile.cs
RotatorTile 控制項的定義和主要處理類,來看看類的結構:
首先看一下 OnApplyTemplate() 方法,他會獲取控制項的模板,根據當前輪播方向處理 StackPanel 容器,初始化並開始輪播動畫;這也是 RotatorTile 控制項的主要流程:使用 Timer,根據設置的間隔時間和輪播的方向,在 Tick 事件中不斷按照某個方向去做平移動畫,動畫中不斷更新當前顯示元素為下一個元素,並不斷相應中途的顯示元素集合變化事件;
同時控制項會響應 RotatorTile_SizeChanged 事件,根據新的尺寸去修改顯示元素和容器的尺寸;響應 RotatorTile_Loaded 和 RotatorTile_Unloaded,處理 Timer 的開始和結束處理;
RotatorTile.cs 繼承自 Control 類,先看一下它定義了哪些依賴屬性:
- ExtraRandomDuration - 一個隨機時間區間的上限,輪播時一個 0~ExtraRandomDuration 的隨機值會被作為輪播間隔使用;
- RotationDelay - 輪播的間隔,時間修改時會觸發 OnRotationDelayInSecondsPropertyChanged 事件;
- ItemsSource - 輪播內容集合的數據源,變化時觸發 OnItemsSourcePropertyChanged 事件;
- ItemTemplate - 輪播內容的內容模板;
- RotateDirection - 輪播的方向,分別上 下 左 右四個方向;
- CurrentItem - 輪播時當前的 Item,變化時觸發 OnCurrentItemPropertyChanged 事件;
首先來看 OnItemsSourcePropertyChanged 事件,它的主要邏輯在方法 Incc_CollectionChanged(s, e) 中:
- 首先 action 處理會被分為 5 種:Remove,Add,Replace,Move 和 Reset;
- 對 Remove action,根據刪除後的開始索引與當前索引,結束索引之間的關係,去更新下一個元素,或設置當前索引,或更新上下文;
- 對 Add action,根據添加後的開始索引與當前索引的關係,以及當前索引與 0 的關係,去開始輪播,或設置當前索引,或更新上下文;
- 對 Replace action,如果當前索引介於新的開始索引和結束索引之間,則更新下一個元素;
- 對 Move action,如果當前索引介於新的開始索引和結束索引之間,獲取它的新索引;
- 對 Reset action,重新開始輪播;
private void Incc_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { if (e.Action == NotifyCollectionChangedAction.Remove) { if (e.OldItems?.Count > 0) { int endIndex = e.OldStartingIndex + e.OldItems.Count; if (_currentIndex >= e.NewStartingIndex && _currentIndex < endIndex) { // Current item was removed. Replace with the next one UpdateNextItem(); } else if (_currentIndex > endIndex) { // Items were removed before the current item. Just update the changed index _currentIndex -= (endIndex - e.NewStartingIndex) - 1; } else if (e.NewStartingIndex == _currentIndex + 1) { // Upcoming item was changed, so update the datacontext _nextElement.DataContext = GetNext(); } } } else if (e.Action == NotifyCollectionChangedAction.Add) { int endIndex = e.NewStartingIndex + e.NewItems.Count; if (e.NewItems?.Count > 0) { if (_currentIndex < 0) { // First item loaded. Start the rotator Start(); } else if (_currentIndex >= e.NewStartingIndex) { // Items were inserted before the current item. Update the index _currentIndex += e.NewItems.Count; } else if (_currentIndex + 1 == e.NewStartingIndex) { // Upcoming item was changed, so update the datacontext _nextElement.DataContext = GetNext(); } } } else if (e.Action == NotifyCollectionChangedAction.Replace) { int endIndex = e.OldStartingIndex + e.OldItems.Count; if (_currentIndex >= e.OldStartingIndex && _currentIndex < endIndex + 1) { // Current item was removed. Replace with the next one UpdateNextItem(); } } else if (e.Action == NotifyCollectionChangedAction.Move) { int endIndex = e.OldStartingIndex + e.OldItems.Count; if (_currentIndex >= e.OldStartingIndex && _currentIndex < endIndex) { // The current item was moved. Get its new location _currentIndex = GetIndexOf(CurrentItem); } } else if (e.Action == NotifyCollectionChangedAction.Reset) { // Significant change or clear. Restart. Start(); } }
接著來看 OnCurrentItemPropertyChanged(d, e) 方法的處理,主要處理邏輯在 RotateToNextItem() 中:
- 首先判斷是否有兩個或者更多的元素,如果沒有則退出處理;
- 定義 Storyboard,動畫時間是 500ms,方向和輪播的目標屬性根據當前輪播的方向去計算;
- 在動畫結束時,開始準備下一個顯示的元素;
private void RotateToNextItem() { // Check if there's more than one item. if not, don't start animation bool hasTwoOrMoreItems = false; ... if (!hasTwoOrMoreItems) { return;} var sb = new Storyboard(); if (_translate != null) { var anim = new DoubleAnimation { Duration = new Duration(TimeSpan.FromMilliseconds(500)), From = 0 }; if (Direction == RotateDirection.Up) { anim.To = -ActualHeight; } else if (Direction == RotateDirection.Down) {...} else if (Direction == RotateDirection.Right) {...} else if (Direction == RotateDirection.Left) {...} anim.FillBehavior = FillBehavior.HoldEnd; anim.EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseOut }; Storyboard.SetTarget(anim, _translate); if (Direction == RotateDirection.Up || Direction == RotateDirection.Down) { Storyboard.SetTargetProperty(anim, "Y"); } else { Storyboard.SetTargetProperty(anim, "X"); } sb.Children.Add(anim); } sb.Completed += async (a, b) => { if (_currentElement != null) { _currentElement.DataContext = _nextElement.DataContext; } // make sure DataContext on _currentElement has had a chance to update the binding // avoids flicker on rotation await System.Threading.Tasks.Task.Delay(50); // Reset back and swap images, getting the next image ready sb.Stop(); if (_translate != null) { UpdateTranslateXY(); } if (_nextElement != null) { _nextElement.DataContext = GetNext(); // Preload the next tile } }; sb.Begin(); }
我們看到有兩個方法中都調用了 UpdateTranslateXY() 方法,來更新平移時的 X 或 Y:
對於 Left 和 Up,只需要充值 X 或 Y 為 0;對於 Right 和 Down,需要把對應的 X 或 Y 設置為 -1 × 對應的高度或寬度,讓動畫從負一倍尺寸平移到 0;
private void UpdateTranslateXY() { if (Direction == RotateDirection.Left || Direction == RotateDirection.Up) { _translate.X = _translate.Y = 0; } else if (Direction == RotateDirection.Right) { _translate.X = -1 * ActualWidth; } else if (Direction == RotateDirection.Down) { _translate.Y = -1 * ActualHeight; } }
調用示例
我們定義了一個 RotatorTile,動畫間隔 1s,方向向上,來看一下 gif 圖顯示的運行結果:
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <controls:RotatorTile x:Name="Tile1" Height="200" Background="LightGray" RotationDelay="0:0:1" ExtraRandomDuration="0:0:1" Direction="Up" ItemTemplate="{StaticResource PhotoTemplate}" /> </Grid>
總結
到這裡我們就把 UWP Community Toolkit 中的 RotatorTile 控制項的源代碼實現過程和簡單的調用示例講解完成了,希望能對大家更好的理解和使用這個控制項有所幫助。歡迎大家多多交流,謝謝!
最後,再跟大家安利一下 UWPCommunityToolkit 的官方微博:https://weibo.com/u/6506046490, 大家可以通過微博關註最新動態。
衷心感謝 UWPCommunityToolkit 的作者們傑出的工作,Thank you so much, UWPCommunityToolkit authors!!!