概述 New UWP Community Toolkit V2.2.0 的版本發佈日誌中提到了 Carousel 的調整,本篇我們結合代碼詳細講解 Carousel 的實現。 Carousel 是一種傳送帶形態的控制項,在圖片展示類的應用中有非常多的應用,它擁有很好的流暢度,可以做很多的自定義,並集成 ...
概述
New UWP Community Toolkit V2.2.0 的版本發佈日誌中提到了 Carousel 的調整,本篇我們結合代碼詳細講解 Carousel 的實現。
Carousel 是一種傳送帶形態的控制項,在圖片展示類的應用中有非常多的應用,它擁有很好的流暢度,可以做很多的自定義,並集成了滑鼠,觸摸板,鍵盤等的操作。我們來看一下官方的介紹和官網示例中的展示:
The
Carousel
control provides a new control, inherited from theItemsControl
, representing a nice and smooth carousel.
This control lets you specify a lot of properties for a flexible layouting.
TheCarousel
control works fine with mouse, touch, mouse and keyboard as well.
Doc: https://docs.microsoft.com/zh-cn/windows/uwpcommunitytoolkit/controls/carousel
Namespace: Microsoft.Toolkit.Uwp.UI.Controls; Nuget: Microsoft.Toolkit.Uwp.UI.Controls;
開發過程
代碼分析
先來看看 Carousel 的類結構組成:
- Carousel.cs - Carousel 控制項類,繼承自 ItemsControl
- Carousel.xaml - Carousel 的樣式文件,包含了 Carousel,CarouselItem,CarouselPanel 的樣式
- CarouselItem.cs - CarouselItem 是 Carousel 控制項的列表中的選擇器 ItemTemplate
- CarouselPanel.cs - CarouselPanel 是 Carousel 控制項的 ItemPanelTemplate
下麵來看一下幾個主要類中的主要代碼實現,因為篇幅關係,我們只摘錄部分關鍵代碼實現:
1. Carousel.cs
在具體分析代碼前,我們先看看 Carousel 類的組成:
可以看到,作為一個集合類控制項,Carousel 也註冊了 SelectedItem 和 SelectedIndex 依賴屬性,並且因為控制項可以控制元素的深度,旋轉角度,動畫時長和類型,列表方向等,註冊了 TransitionDuration,ItemDepth,EasingFunction,ItemMargin,ItemRotationX,Orientation 等依賴屬性。而部分依賴屬性的 PropertyChanged 事件由 OnCarouselPropertyChanged(d, e) 來實現;
下麵來看一下 Carousel 類的構造方法:
構造方法中,首先設置了樣式,Tab 導航模式;定義了滑鼠滾輪,滑鼠點擊和鍵盤事件,並註冊了數據源變化事件來得到正確的 SelectedItem 和 SelectedIndex。
public Carousel() { // Set style DefaultStyleKey = typeof(Carousel); SetValue(AutomationProperties.NameProperty, "Carousel"); IsHitTestVisible = true; IsTabStop = false; TabNavigation = KeyboardNavigationMode.Once; // Events registered PointerWheelChanged += OnPointerWheelChanged; PointerReleased += CarouselControl_PointerReleased; KeyDown += Keyboard_KeyUp; // Register ItemSource changed to get correct SelectedItem and SelectedIndex RegisterPropertyChangedCallback(ItemsSourceProperty, (d, dp) => { ... }); }
在鍵盤按鍵抬起的事件處理中,分別對應 Down,Up,Right 和 Left 做了處理,我們只截取了 Down 的處理過程;可以看到,如果列表方向為縱向,則 Down 按鍵會觸發 SelectedIndex++,也就是當前選擇項下移一位;對應其他三個按鍵也是類似的操作;OnPointerWheelChanged 的實現方式類似,這裡不贅述;
private void Keyboard_KeyUp(object sender, KeyRoutedEventArgs e) { switch (e.Key) { case Windows.System.VirtualKey.Down: case Windows.System.VirtualKey.GamepadDPadDown: case Windows.System.VirtualKey.GamepadLeftThumbstickDown: if (Orientation == Orientation.Vertical) { if (SelectedIndex < Items.Count - 1) { SelectedIndex++; } else if (e.OriginalKey != Windows.System.VirtualKey.Down) { FocusManager.TryMoveFocus(FocusNavigationDirection.Down); } e.Handled = true; } break; ... } }
接著看一下 PrepareContainerForItemOverride(element, item) 方法,它為 Item 設置了初始的 3D 旋轉的中心點,Item 變換的中心點;並根據當前選擇項確定 Item 是否被選中;
protected override void PrepareContainerForItemOverride(DependencyObject element, object item) { base.PrepareContainerForItemOverride(element, item); var carouselItem = (CarouselItem)element; carouselItem.Selected += OnCarouselItemSelected; carouselItem.RenderTransformOrigin = new Point(0.5, 0.5); carouselItem.IsTabStop = Items.IndexOf(item) == SelectedIndex; carouselItem.UseSystemFocusVisuals = true; PlaneProjection planeProjection = new PlaneProjection(); planeProjection.CenterOfRotationX = 0.5; planeProjection.CenterOfRotationY = 0.5; planeProjection.CenterOfRotationZ = 0.5; var compositeTransform = new CompositeTransform(); compositeTransform.CenterX = 0.5; compositeTransform.CenterY = 0.5; compositeTransform.CenterZ = 0.5; carouselItem.Projection = planeProjection; carouselItem.RenderTransform = compositeTransform; if (item == SelectedItem) { carouselItem.IsSelected = true; } }
2. Carousel.xaml
如上面類結構介紹時所說,Carousel.xaml 是 Carousel 控制項的樣式文件;下麵代碼中我把非關鍵部分用 ‘...’ 代替了,可以看到,主要是兩個部分的樣式:CarouselItem 和 Carousel,CarouselPanel 作為 Carousel 的 ItemsPanelTemplate;Carousel 控制項的 easing mode 是 'EaseOut'。
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:Microsoft.Toolkit.Uwp.UI.Controls"> <Style TargetType="local:CarouselItem"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="local:CarouselItem"> <Grid BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}"> <VisualStateManager.VisualStateGroups> ... </VisualStateManager.VisualStateGroups> <ContentPresenter .../> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style> <Style TargetType="local:Carousel"> <Setter Property="ItemsPanel"> <Setter.Value> <ItemsPanelTemplate> <local:CarouselPanel /> </ItemsPanelTemplate> </Setter.Value> </Setter> <Setter Property="EasingFunction"> <Setter.Value> <ExponentialEase EasingMode="EaseOut" /> </Setter.Value> </Setter> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="local:Carousel"> <Grid>
... </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style> </ResourceDictionary>
3. CarouselItem.cs
在前面 Carousel.xaml 中我們看到了 CarouselItem 的樣式,有針對 VisualStateManager 的樣式狀態,而 CarouselItem 類則定義了這些狀態變化事件對應的處理方法。分別有 OnIsSelectedChanged,OnPointerEntered,OnPointerExited 和 OnPointerPressed,在觸發這些狀態時,CarouselItem 會對應切換到那個狀態時的樣式。
public CarouselItem() { // Set style DefaultStyleKey = typeof(CarouselItem); RegisterPropertyChangedCallback(SelectorItem.IsSelectedProperty, OnIsSelectedChanged); } protected override void OnPointerEntered(PointerRoutedEventArgs e) {...} protected override void OnPointerExited(PointerRoutedEventArgs e) {...} protected override void OnPointerPressed(PointerRoutedEventArgs e) {...} internal event EventHandler Selected; private void OnIsSelectedChanged(DependencyObject sender, DependencyProperty dp) { var item = (CarouselItem)sender; if (item.IsSelected) { Selected?.Invoke(this, EventArgs.Empty); VisualStateManager.GoToState(item, SelectedState, true); } else { VisualStateManager.GoToState(item, NormalState, true); } }
4. CarouselPanel.cs
同樣在具體分析代碼前,我們先看看 CarouselPanel 類的組成:
CarouselPanel 類繼承自 Panel 類,可以看到它接收的事件響應,有 OnTapped,OnManipulationDelta 和 OnManipulationCompleted,分別對應點按,觸摸移動和移動結束的處理。其中:
- OnTapped 的處理主要是根據當前控制項的可視化範圍和尺寸,判斷點擊的點對應哪個元素被選中;
- OnManipulationDelta 則是根據觸控操作的方向和量度等,決定 Item 的動畫幅度,動畫速度和每個元素變換狀態,以及選中元素的變化;
- OnManipulationCompleted 則是在觸控結束後,確定結束動畫,以及結束時應該選中那個元素;
- UpdatePosition() 方法則是在 OnManipulationDelta 方法觸發到 first 或 last 元素時,需要重新設置動畫;
- GetProjectionFromManipulation(sender, e) 則是在 OnManipulationDelta 方法中,根據當前觸控的手勢,決定當前 Item 的 Projection;
- GetProjectionFromSelectedIndex(i) 是根據當前選中的索引,來取得 Item 的 Projection;
- ApplyProjection(element, proj, storyboard) 是應用獲取到的 Projection,包括旋轉,變換等動畫;
而因為 CarouselPanel 類繼承自 Panel 類,所以它也重寫了 MeasureOverride(availableSize) 和 ArrangeOverride(finalSize) 方法:
MeasureOverride(availableSize) 方法的實現中,主要是根據寬度和高度是否設置為無限值,如果是,且方向和元素排列順序一致,則尺寸為當前方向三個元素的寬度,然後把計算後的尺寸傳出去;
protected override Size MeasureOverride(Size availableSize) { var containerWidth = 0d; var containerHeight = 0d; foreach (FrameworkElement container in Children) { container.Measure(availableSize); // get containerWidth and containerHeight for max } var width = 0d; var height = 0d; // It's a Auto size, so we define the size should be 3 items if (double.IsInfinity(availableSize.Width)) { width = Carousel.Orientation == Orientation.Horizontal ? containerWidth * (Children.Count > 3 ? 3 : Children.Count) : containerWidth; } else { width = availableSize.Width; } // It's a Auto size, so we define the size should be 3 items if (double.IsInfinity(availableSize.Height)) { height = Carousel.Orientation == Orientation.Vertical ? containerHeight * (Children.Count > 3 ? 3 : Children.Count) : containerHeight; } else { height = availableSize.Height; } Clip = new RectangleGeometry { Rect = new Rect(0, 0, width, height) }; return new Size(width, height); }
ArrangeOverride(finalSize) 方法則是排列元素的處理,因為 Carousel 控制項有動畫處理,所以在排列時需要考慮到元素排列的動畫,以及 Zindex;
protected override Size ArrangeOverride(Size finalSize) { double centerLeft = 0; double centerTop = 0; Clip = new RectangleGeometry { Rect = new Rect(0, 0, finalSize.Width, finalSize.Height) }; for (int i = 0; i < Children.Count; i++) { FrameworkElement container = Children[i] as FrameworkElement; ... // get the good center and top position // Get rect position // Placing the rect in the center of screen ... // Get the initial projection (without move) var proj = GetProjectionFromSelectedIndex(i); // apply the projection to the current object ApplyProjection(container, proj); // calculate zindex and opacity int zindex = (Children.Count * 100) - deltaFromSelectedIndex; Canvas.SetZIndex(container, zindex); } return finalSize; }
調用示例
示例中我們實現了橫向的 Carousel 控制項,作為一個圖片列表,可以看到當前選中的 Item 的 ZIndex 是最高的,向兩側依次降低,而在滑動過程中,伴隨著 3D 和變換的動畫,ZIndex 也會一起變化,而滑動結束時,選中項重新計算,每一項的 Project 也會重新計算。
<Grid> <Border Margin="0"> <controls:Carousel x:Name="CarouselControl" InvertPositive="True" ItemDepth="238" ItemMargin="-79" ItemRotationX="4" ItemRotationY="9" ItemRotationZ ="-3" Orientation="Horizontal" SelectedIndex="5"> <controls:Carousel.EasingFunction> <CubicEase EasingMode="EaseOut" /> </controls:Carousel.EasingFunction> <controls:Carousel.ItemTemplate> <DataTemplate> <Image Width="200" Height="200" VerticalAlignment="Bottom" Source="{Binding Thumbnail}" Stretch="Uniform" /> </DataTemplate> </controls:Carousel.ItemTemplate> </controls:Carousel> </Border> </Grid>
總結
到這裡我們就把 UWP Community Toolkit 中的 Carousel 控制項的源代碼實現過程和簡單的調用示例講解完成了,希望能對大家更好的理解和使用這個控制項有所幫助,讓你的圖片列表控制項更加炫酷靈動。歡迎大家多多交流,謝謝!
最後,再跟大家安利一下 UWPCommunityToolkit 的官方微博:https://weibo.com/u/6506046490, 大家可以通過微博關註最新動態。
衷心感謝 UWPCommunityToolkit 的作者們傑出的工作,Thank you so much, UWPCommunityToolkit authors!!!