列表控件是應用程序中常見的控件之一,對其做一些絢麗的視覺特效,可以讓軟件增色不少。
本人網上看過一個視頻,是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錄制幀數的原因,效果圖不是很流暢,但實際運行情況動畫效果是非常流暢的
