列表控件是应用程序中常见的控件之一,对其做一些绚丽的视觉特效,可以让软件增色不少。
本人网上看过一个视频,是windows phone 7系统上的一个App的列表滚动效果,效果非常炫
现在在WPF上用ListBox重现此效果
首先我们来分析一下,这种实时滚动的效果是如何实现的,有哪些步骤
1.获取ListBox模板内部的ScrollViewer和ItemsPanel
2.监听ScrollViewer的滚动事件ScrollChange, 获取ItemsPanel的布局方向
3.在滚动事件发生时计算当前可视化区域中的第一项和最后一项,这是此滑动效果的核心算法所在,算法的效率决定了滑动效果的流畅性
4.根据滚动的方向和布局的方向依次对指定的Item做动画效果。
重写ListBoxItem
public class PowerListBoxItem : ListBoxItem
声明构造函数并赋初始值
static PowerListBoxItem(){DefaultStyleKeyProperty.OverrideMetadata(typeof(PowerListBoxItem), new FrameworkPropertyMetadata(typeof(PowerListBoxItem)));}public PowerListBoxItem(){ItemStatus = ItemStatusEnum.Out; //默认Item状态为"退出"duration = new TimeSpan(0, 0, 0, 0, 300);//easingFunction = new PowerEase() { EasingMode = EasingMode.EaseIn, Power = 4 };easingFunction = new CircleEase() { EasingMode = EasingMode.EaseInOut };}
定义PowerListBoxItem的成员属性
/// <summary>/// PowerListBoxItem模板中的内容控件/// </summary>private FrameworkElement contentControl;/// <summary>/// 动画间隔时间/// </summary>private TimeSpan duration;private IEasingFunction easingFunction; //动画缓动函数private IList<AnimationModel> DownInAnimationList; //定义Item从下往上运动的动画内容集合private IList<AnimationModel> UpInAnimationList; //定义Item从上往下运动的动画内容集合/// <summary>/// 项枚举状态,指明Item运动的方向/// </summary>internal enum ItemStatusEnum{UpIn, DownIn, RightIn, LeftIn, Out}private ItemStatusEnum _itemStatus;internal ItemStatusEnum ItemStatus{get { return _itemStatus; }set{if (_itemStatus == value) //状态相同时不再刷新状态return;_itemStatus = value;PlayAnimation(); //执行动画}}
重写ListBox
[StyleTypedProperty(Property = "ItemContainerStyle", StyleTargetType = typeof(PowerListBoxItem))]public class PowerListBox : ListBox{static PowerListBox(){DefaultStyleKeyProperty.OverrideMetadata(typeof(PowerListBox), new FrameworkPropertyMetadata(typeof(PowerListBox)));}public PowerListBox(){DefaultStyleKey = typeof(PowerListBox);}}protected override DependencyObject GetContainerForItemOverride(){return new PowerListBoxItem(); //指定PowerListBox的项为PowerListBoxItem}protected override bool IsItemItsOwnContainerOverride(object item){return item is PowerListBoxItem;}
定义PowerList的成员属性
/// <summary>/// ListBox内部的滚动试图/// </summary>private ScrollViewer _scrollView;/// <summary>/// 容器的布局方向/// </summary>private Orientation _panelOrientation;/// <summary>/// 当前可视化视图的第一项/// </summary>private int firstVisibleIndex;/// <summary>/// 当前可视化视图的最后一项/// </summary>private int lastVisibleIndex;/// <summary>/// 上次滚动时可视化视图的第一项/// </summary>private int oldFirstVisibleIndex;/// <summary>/// 上次滚动时可视化视图的最后一项/// </summary>private int oldLastVisibleIndex;/// <summary>/// 标识,是否已找到第一项/// </summary>private bool isFindFirst;/// <summary>/// 当前累计已遍历过的Item高度或宽度的值,用于寻找第一项和最后一项/// </summary>private double cumulativeNum;
获取PowerListBox内部的ScrollViewer和ItemsPanel,并监听滚动事件
public override void OnApplyTemplate(){_scrollView = VisualHelper.FindFirstVisualChild<ScrollViewer>(this);if (_scrollView == null)return;_scrollView.CanContentScroll = false; //不按Item为步长滚动_scrollView.PanningMode = PanningMode.Both;_scrollView.ScrollChanged += _scrollView_ScrollChanged; //监听滚动事件var panel = this.ItemsPanel.LoadContent(); //读取布局容器if (panel is StackPanel)_panelOrientation = (panel as StackPanel).Orientation;else if (panel is VirtualizingPanel)_panelOrientation = (panel as VirtualizingStackPanel).Orientation;base.OnApplyTemplate();}private void _scrollView_ScrollChanged(object sender, ScrollChangedEventArgs e){//Console.WriteLine("itemCount:{0} VerticalOffset:{1} ViewportHeight:{2} ContentVerticalOffset:{3}",//Items.Count, _scrollView.VerticalOffset, _scrollView.ViewportHeight, _scrollView.ContentVerticalOffset);
//每次滚动时都计算当前可视化区域的首尾项calculationIndex(); refreshItemStatus(); //刷新Item状态}
计算可视化区域的第一项和最后一项
private void calculationIndex(){oldFirstVisibleIndex = firstVisibleIndex;oldLastVisibleIndex = lastVisibleIndex;isFindFirst = false;if (_panelOrientation == Orientation.Vertical){cumulativeNum = 0.0;for (int i = 0; i < Items.Count; i++){var _item = this.ItemContainerGenerator.ContainerFromIndex(i) as PowerListBoxItem;cumulativeNum += _item.ActualHeight + _item.Margin.Top + _item.Margin.Bottom;//遍历Items, 累计Item高度,第一个超过滚动条垂直偏移量的Item就是当前可视化区域中的第一项if (!isFindFirst && cumulativeNum >= _scrollView.VerticalOffset){firstVisibleIndex = i;isFindFirst = true;}//累计Item高度超过滚动条垂直偏移量和滚动区显示高度的和,就是当前可视化区域的最后一项if (cumulativeNum >= (_scrollView.VerticalOffset + _scrollView.ViewportHeight)){lastVisibleIndex = i;break;}}}}
确定当前可视化区域的首尾项之后,刷新Item的状态
private void refreshItemStatus(){Console.WriteLine("firstIndex: {0} lastIndex: {1} oldFirstIndex: {2} oldLastIndex: {3} {4}",firstVisibleIndex, lastVisibleIndex, oldFirstVisibleIndex, oldLastVisibleIndex, firstVisibleIndex > oldFirstVisibleIndex ? "Down In" : firstVisibleIndex < oldFirstVisibleIndex ? "UpIn" : "normal");if ((firstVisibleIndex == oldFirstVisibleIndex && lastVisibleIndex == oldLastVisibleIndex) || oldFirstVisibleIndex == 0 && oldLastVisibleIndex == 0)return;//Console.WriteLine("firstVisibleIndex:{0} oldFirstVisibleIndex:{1}", firstVisibleIndex, oldFirstVisibleIndex);//判断滚动方向if (firstVisibleIndex > oldFirstVisibleIndex){//垂直 滚动条往下,内容网上//水平 滚动条往右,内容往左for (var i = oldLastVisibleIndex; i <= lastVisibleIndex; i++){var _item = this.ItemContainerGenerator.ContainerFromIndex(i) as PowerListBoxItem;_item.ItemStatus = _panelOrientation == Orientation.Vertical ? PowerListBoxItem.ItemStatusEnum.DownIn : PowerListBoxItem.ItemStatusEnum.RightIn;//Console.WriteLine("DownIn {0}", i);}}else if (lastVisibleIndex < oldLastVisibleIndex){//垂直 滚动条往上,内容网下//水平 滚动条往左,内容往右for (var i = oldFirstVisibleIndex; i >= firstVisibleIndex; i--){var _item = this.ItemContainerGenerator.ContainerFromIndex(i) as PowerListBoxItem;_item.ItemStatus = _panelOrientation == Orientation.Vertical ? PowerListBoxItem.ItemStatusEnum.UpIn : PowerListBoxItem.ItemStatusEnum.LeftIn;//Console.WriteLine("UpIn {0}", i);}}}
定义PowerListBox的默认外观
<Style TargetType="{x:Type local:PowerListBox}"><Setter Property="Background" Value="Transparent"/><Setter Property="BorderThickness" Value="0"/><Setter Property="BorderBrush" Value="Transparent"/><Setter Property="Padding" Value="0"/><Setter Property="Template"><Setter.Value><ControlTemplate TargetType="{x:Type local:PowerListBox}"><ScrollViewer x:Name="ScrollViewer" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Foreground="{TemplateBinding Foreground}" Padding="{TemplateBinding Padding}"><ItemsPresenter/></ScrollViewer></ControlTemplate></Setter.Value></Setter></Style><Style TargetType="{x:Type local:PowerListBoxItem}"><Setter Property="Background" Value="Transparent"/><Setter Property="BorderThickness" Value="0"/><Setter Property="BorderBrush" Value="Transparent"/><Setter Property="Padding" Value="0"/><Setter Property="HorizontalContentAlignment" Value="Stretch"/><Setter Property="VerticalContentAlignment" Value="Stretch"/><Setter Property="Margin" Value="0,8"/><Setter Property="Template"><Setter.Value><ControlTemplate TargetType="{x:Type local:PowerListBoxItem}"><Border x:Name="LayoutRoot" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" HorizontalAlignment="{TemplateBinding HorizontalAlignment}" VerticalAlignment="{TemplateBinding VerticalAlignment}"><ContentControl x:Name="ContentContainer" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" Foreground="{TemplateBinding Foreground}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" RenderTransformOrigin="0.5,0.5"><ContentControl.RenderTransform><TransformGroup><TranslateTransform/></TransformGroup></ContentControl.RenderTransform></ContentControl></Border></ControlTemplate></Setter.Value></Setter></Style>
调用 PowerListBox
<local:PowerListBox ItemsSource="{Binding TestModelList}" ><local:PowerListBox.ItemTemplate><DataTemplate><Grid><Grid.ColumnDefinitions><ColumnDefinition Width="150"/><ColumnDefinition/></Grid.ColumnDefinitions><TextBlock Text="{Binding Name}" VerticalAlignment="Center" FontSize="20"/><Border Width="100" Height="120" Background="#FF4949D3" Grid.Column="1" HorizontalAlignment="Left"><TextBlock Text="{Binding Id}" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="40" Foreground="Black"/></Border></Grid></DataTemplate></local:PowerListBox.ItemTemplate>
</local:PowerListBox>
效果图
由于gif录制帧数的原因,效果图不是很流畅,但实际运行情况动画效果是非常流畅的