先上效果图
首先安装 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 事件中获取.
- FlipView flipView;
- ScrollViewer scrollViewer;
- Compositor compositor;
- CompositionPropertySet scrollPropSet;
- public DependencyObject AssociatedObject { get; private set; }
- public void Attach(DependencyObject associatedObject)
- {
- AssociatedObject = associatedObject;
- if (associatedObject is FlipView flip) flipView = flip;
- else throw new ArgumentException("对象不是 FlipView");
- scrollViewer = Helper.FindVisualChild<ScrollViewer>(flipView, "ScrollingHost");
- if (scrollViewer == null)
- {
- flipView.Loaded += FlipView_Loaded;
- }
- else InitCompositionResources(scrollViewer);
- }
- private void FlipView_Loaded(object sender, RoutedEventArgs e)
- {
- flipView.Loaded -= FlipView_Loaded;
- var scroll = Helper.FindVisualChild<ScrollViewer>(flipView, "ScrollingHost");
- if (scroll == null) throw new ArgumentNullException("ScrollViewer 为空");
- else scrollViewer = scroll;
- InitCompositionResources(scrollViewer);
- }
- void InitCompositionResources(ScrollViewer scroll)
- {
- if (compositor == null) compositor = ElementCompositionPreview.GetElementVisual(flipView).Compositor;
- if (scroll == null) return;
- scrollPropSet = ElementCompositionPreview.GetScrollViewerManipulationPropertySet(scrollViewer);
- }
- View Code
- public static class Helper
- {
- public static T FindVisualChild<T>(DependencyObject obj, int Index = 0) where T : DependencyObject
- {
- if (Index == -1) return null;
- int count = VisualTreeHelper.GetChildrenCount(obj);
- int findedcount = 0;
- for (int i = 0; i <count; i++)
- {
- DependencyObject child = Windows.UI.Xaml.Media.VisualTreeHelper.GetChild(obj, i);
- if (child != null && child is T)
- {
- if (findedcount == Index)
- return (T)child;
- else
- {
- findedcount++;
- }
- }
- else
- {
- T childOfChild = FindVisualChild<T>(child, findedcount);
- if (childOfChild != null)
- return childOfChild;
- }
- }
- return null;
- }
- public static T FindVisualChild<T>(DependencyObject obj, string name) where T : DependencyObject
- {
- int count = VisualTreeHelper.GetChildrenCount(obj);
- int findedcount = 0;
- for (int i = 0; i <count; i++)
- {
- DependencyObject child = Windows.UI.Xaml.Media.VisualTreeHelper.GetChild(obj, i);
- if (child != null && child is T)
- {
- if ((child as FrameworkElement).Name == name)
- return (T)child;
- else
- {
- findedcount++;
- }
- }
- else
- {
- T childOfChild = FindVisualChild<T>(child, findedcount);
- if (childOfChild != null)
- return childOfChild;
- }
- }
- return null;
- }
- }
- 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 附加动画, 虽然损失了部分性能, 但是可以保证不出问题.
- void InitAnimation()
- {
- if (compositor != null)
- {
- for (int i = 0; i < flipView.Items.Count; i++)
- {
- var item = flipView.ContainerFromIndex(i);
- if (item is UIElement ele)
- {
- var visual = ElementCompositionPreview.GetElementVisual(ele);
- CenterPointAnimation.SetReferenceParameter("visual", visual);
- visual.StartAnimation("CenterPoint", CenterPointAnimation);
- visual.StopAnimation("Scale.X");
- visual.StopAnimation("Scale.Y");
- ScaleAnimation.SetReferenceParameter("visual", visual);
- visual.StartAnimation("Scale.X", ScaleAnimation);
- visual.StartAnimation("Scale.Y", ScaleAnimation);
- }
- }
- }
- }
最后在 Loaded 的最后也调用一次 InitAnimation, 大功告成.
源代码下载 http://files.cnblogs.com/files/blue-fire/AnimationFlipViewBehavior.zip
来源: http://www.bubuko.com/infodetail-2619026.html