先上效果图
首先安装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; 11 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 }
然后创建两个表达式动画,分别作用在中心点和缩放上。
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,大功告成。