1. 程式人生 > >WPF自定義控制元件之列表滑動特效 PowerListBox

WPF自定義控制元件之列表滑動特效 PowerListBox

原文: WPF自定義控制元件之列表滑動特效 PowerListBox

列表控制元件是應用程式中常見的控制元件之一,對其做一些絢麗的視覺特效,可以讓軟體增色不少。

本人網上看過一個視訊,是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錄製幀數的原因,效果圖不是很流暢,但實際執行情況動畫效果是非常流暢的