UWP:使用Behavior實現FlipView簡單縮放效果
先上效果圖
首先安裝Behavior SDK:在Nuget中搜索安裝 Microsoft.Xaml.Behaviors.Uwp.Managed 。
然後新建類,AnimationFlipViewBehavior.cs,並繼承DependencyObject和IBehavior接口:
namespace TestBehavior { public class AnimationFlipViewBehavior: DependencyObject, IBehavior { public DependencyObject AssociatedObject { get; set; } public void Attach(DependencyObject associatedObject) { AssociatedObject = associatedObject; } public void Detach() { } } }
Attach是添加Behavior時被調用的方法,Detach是移除Behavior時被調用的方法。
這時在Attach中判斷是否是FlipView,並且保存下來。然後按照老樣子獲取ScrollViewer,如果FlipView已經加載好了,就可以直接獲取到ScrollViewer,否則要在FlipView的Loaded事件中獲取。
1 FlipView flipView; 2 ScrollViewer scrollViewer; 3 Compositor compositor; 4 CompositionPropertySet scrollPropSet; 5 6 public DependencyObject AssociatedObject { get; private set; } 7 8 public void Attach(DependencyObject associatedObject) 9 { 10 AssociatedObject = associatedObject;View Code11 if (associatedObject is FlipView flip) flipView = flip; 12 else throw new ArgumentException("對象不是FlipView"); 13 scrollViewer = Helper.FindVisualChild<ScrollViewer>(flipView, "ScrollingHost"); 14 if (scrollViewer == null) 15 { 16 flipView.Loaded += FlipView_Loaded; 17 } 18 else InitCompositionResources(scrollViewer); 19 } 20 21 private void FlipView_Loaded(object sender, RoutedEventArgs e) 22 { 23 flipView.Loaded -= FlipView_Loaded; 24 var scroll = Helper.FindVisualChild<ScrollViewer>(flipView, "ScrollingHost"); 25 if (scroll == null) throw new ArgumentNullException("ScrollViewer為空"); 26 else scrollViewer = scroll; 27 28 InitCompositionResources(scrollViewer); 29 } 30 31 void InitCompositionResources(ScrollViewer scroll) 32 { 33 if (compositor == null) compositor = ElementCompositionPreview.GetElementVisual(flipView).Compositor; 34 if (scroll == null) return; 35 36 scrollPropSet = ElementCompositionPreview.GetScrollViewerManipulationPropertySet(scrollViewer); 37 }
1 public static class Helper 2 { 3 public static T FindVisualChild<T>(DependencyObject obj, int Index = 0) where T : DependencyObject 4 { 5 if (Index == -1) return null; 6 int count = VisualTreeHelper.GetChildrenCount(obj); 7 int findedcount = 0; 8 for (int i = 0; i < count; i++) 9 { 10 DependencyObject child = Windows.UI.Xaml.Media.VisualTreeHelper.GetChild(obj, i); 11 if (child != null && child is T) 12 { 13 if (findedcount == Index) 14 return (T)child; 15 else 16 { 17 findedcount++; 18 } 19 } 20 else 21 { 22 T childOfChild = FindVisualChild<T>(child, findedcount); 23 if (childOfChild != null) 24 return childOfChild; 25 } 26 } 27 return null; 28 } 29 public static T FindVisualChild<T>(DependencyObject obj, string name) where T : DependencyObject 30 { 31 int count = VisualTreeHelper.GetChildrenCount(obj); 32 int findedcount = 0; 33 for (int i = 0; i < count; i++) 34 { 35 DependencyObject child = Windows.UI.Xaml.Media.VisualTreeHelper.GetChild(obj, i); 36 if (child != null && child is T) 37 { 38 if ((child as FrameworkElement).Name == name) 39 return (T)child; 40 else 41 { 42 findedcount++; 43 } 44 } 45 else 46 { 47 T childOfChild = FindVisualChild<T>(child, findedcount); 48 if (childOfChild != null) 49 return childOfChild; 50 } 51 } 52 return null; 53 } 54 }View Code
然後創建兩個表達式動畫,分別作用在中心點和縮放上。
ExpressionAnimation CenterPointAnimation; ExpressionAnimation ScaleAnimation; void InitCompositionResources(ScrollViewer scroll) { if (compositor == null) compositor = ElementCompositionPreview.GetElementVisual(flipView).Compositor; if (scroll == null) return; scrollPropSet = ElementCompositionPreview.GetScrollViewerManipulationPropertySet(scrollViewer); if (CenterPointAnimation == null) { CenterPointAnimation = compositor.CreateExpressionAnimation("Vector3(visual.Size.X/2,visual.Size.Y/2,0)"); } if (ScaleAnimation == null) { ScaleAnimation = compositor.CreateExpressionAnimation("Clamp(1- (visual.Offset.X + scroll.Translation.X) / visual.Size.X * 0.4, 0f, 1f)"); ScaleAnimation.SetReferenceParameter("scroll", scrollPropSet); } }
這裏著重說一下ScaleAnimation。
表達式中的Clamp(value,min,max)是內置函數,當value在min和max之間的時候返回value,小於min則返回min,大於max則返回max。
FlipView中是一個ScrollViewer,橫向滾動,ScrollViewer內的元素的Visual.Offset.X控制Visual的位置,而不是默認為0。所以只要判斷visual.Offset.X和scroll.Translation.X的關系,就能做出動畫來。
然後寫一個方法,給所有Items的容器附加上這些動畫。
因為默認的Items並不是Observable的,有兩種解決方案,一是設置ItemsSource為一個ObservableCollection,然後註冊CollectionChanged事件。這樣做會讓控件和頁面後臺代碼耦合度提升。為了更幹凈的代碼結構,這裏用一個性能低一些的方法,註冊FlipView的SelectionChanged事件,在這裏調用InitAnimation方法。
如果每次只給SelectedItem和左右的Item附加動畫,PC上測試很完美,但是手機上,或者說觸摸操作的時候,會出現動畫未加載的問題。這裏涉及到一個FlipView和Pivot的大坑。
在鍵鼠操作和代碼操作SelectedIndex切換頁面的時候,是先觸發SelectionChanged事件,再播放動畫的。但是觸摸操作的時候,只有當你滑屏再送手後,系統才知道到底應不應該切換頁面。所以我們每次送手播放完動畫和觸發SelectionChanged並不同步,動畫自然就不會附加到後面的Item上,所以每次我們都給所有的Item附加動畫,雖然損失了部分性能,但是可以保證不出問題。
1 void InitAnimation() 2 { 3 if (compositor != null) 4 { 5 for (int i = 0; i < flipView.Items.Count; i++) 6 { 7 var item = flipView.ContainerFromIndex(i); 8 if (item is UIElement ele) 9 { 10 var visual = ElementCompositionPreview.GetElementVisual(ele); 11 CenterPointAnimation.SetReferenceParameter("visual", visual); 12 visual.StartAnimation("CenterPoint", CenterPointAnimation); 13 visual.StopAnimation("Scale.X"); 14 visual.StopAnimation("Scale.Y"); 15 ScaleAnimation.SetReferenceParameter("visual", visual); 16 visual.StartAnimation("Scale.X", ScaleAnimation); 17 visual.StartAnimation("Scale.Y", ScaleAnimation); 18 } 19 } 20 } 21 }
最後在Loaded的最後也調用一次InitAnimation,大功告成。
源代碼下載
UWP:使用Behavior實現FlipView簡單縮放效果