上一篇我們已經把界面畫出來了,這篇我們就來制作交互的邏輯吧。上一篇的電梯:
http://www.cnblogs.com/tong-tong/archive/2012/07/15/2586543.html
回顧下效果:
<TextBlock Text="{Binding SelectedItem.Content, ElementName=ListBox1}"/> <ListBox x:Name="ListBox1"SelectedIndex="2"> <ListBoxItem Content="1"/> <ListBoxItem Content="2"/> <ListBoxItem Content="3"/> <ListBoxItem Content="4"/> <ListBoxItem Content="5"/> <ListBoxItem Content="6"/> <ListBoxItem Content="7"/> <ListBoxItem Content="8"/> <ListBoxItem Content="9"/> <ListBoxItem Content="0"/> </ListBox> <TextBlock Text="選中內容:"/>
1.交互性需求
我們需要實現一下幾個功能
- 隨着鼠標的移動上下滾動。
- 當放開鼠標后,自動對正。
- 設置ListBox的SelectedIndex為但前玻璃塊下面的項。
- load時根據XAML中設置的SelectedIndex自動滾動到相應項。
2.整體思路
首先我們最直觀看到的效果就是滾動,如何讓它滾動呢?我最開始的思路就是ListBox原生的滾動條,通過VisualTreeHelper.GetChild()獲取到ListBox模版里的scrollviewer運行時對象,然后Mousemove時通過myScrollViewer.ScrollToVerticalOffset(m_scrollOffset)方法來設置滾動的位置。經過嘗試這樣雖然可是實現第一點,跟隨鼠標上下滾動,但是沒有依賴項屬性來控制的話,就無法實現鼠標彈起后的自動對正動畫了,所以,這個思路果斷否決了。
那如何才能解決動畫問題呢?我們來回顧一下ListBox的幾個常用模版樣式:
Style:這個就是控制ListBox整體的外觀,上一篇我們幾乎所有的工作都是在改這個。
ItemContainerStyle:顧名思義就是ListBox子項的樣子,每一項是顯示些什么內容呢?結構如何呢就通過這個來設置。
ItemsPanel:再次顧名思義就是子項們的容器,我們都知道WPF的容器決定了它的children的布局方式。因為默認是StackPanel,所以我們的ListBox的子項通常看起來是一列或是一行。
ItemTemplate:這個東西我只是了解一下,實戰中沒用到過,我也不是很了解什么效果是必須用它才能做出來的,有知道的朋友麻煩留言告訴我一聲。
沒錯,或許你已經想到了,我們要用到的就是ItemsPanel,我們只要獲取到StackPanel的對象,然后設置他的RenderTransform為TranslateTransform,這樣就是可以通過改變TranslateTransform.Y的依賴項來實現鼠標拖動上下滾動以及鼠標彈起后自動對正的動畫效果了。
3.設計過程
Step.1 跟隨鼠標滾吧~!
首先我們要來獲取StackPanel運行時對象的實例。一開始我還糾結了半天怎么來獲取呢?后來突然想到一個比較另類的方法,就是StackPanel是可以添加Loaded事件的,只要在事件處理中獲取Sender就行了。
Xaml:
<StackPanel Background="#00000000" Height="Auto" Width="Auto" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Loaded="StackPanel_Loaded"/>
CS:(這里順便獲取一下item的呈現高度,因為我要根據它計算出可偏移的范圍,防止飛出控件)
StackPanel _Panel;//模版容器 double Y;//鼠標Y軸坐標 TranslateTransform _TTF;//容器偏移 double itemHeight;//每個item的高度 private void StackPanel_Loaded(object sender, RoutedEventArgs e) { //添加偏移屬性 _Panel = sender as StackPanel; _TTF = new TranslateTransform(); _Panel.RenderTransform = _TTF; //獲取item的實際高度 itemHeight = (ListBox1.ItemContainerGenerator.ContainerFromIndex(0) as ListBoxItem).ActualHeight; }
OK,獲取到這個對象我們就可以添加Mousemove事件來根據鼠標位置來改變StackPanel 的Y軸位移了。
首先我們給ListBox添加Mousemove事件和mousedown事件(WPF里我把這個事件加在StackPanel上是可以觸發事件的,而silverlight里這樣卻無論如何也觸發不了...)
CS:
private void VirtualizingStackPanel_MouseMove(object sender, MouseEventArgs e) { if (isPress)//鼠標按下時才滾動 { double mouseOffset = e.GetPosition(this).Y - Y + _TTF.Y;//計算出當前已偏移的位置 //在第一項和最后一項時就不能繼續偏移 if (mouseOffset >= ListBox1.ActualHeight / 2 || mouseOffset <= ListBox1.ActualHeight / 2 - ListBox1.Items.Count * itemHeight) { return; } _TTF.Y = mouseOffset; //偏移為鼠標位置減去之前的位置 Y = e.GetPosition(this).Y;//記錄但前位置 } } private void VirtualizingStackPanel_MouseDown(object sender, MouseButtonEventArgs e) { Y = e.GetPosition(this).Y;//記錄點擊的鼠標位置 isPress = true; }
這里我說明一下這個可移動范圍是怎么算出來的。如果已經看懂算法的博友可以跳過
(我們要讓它在第0項到達控件的中間位置時就不能繼續向下滾,TranslateTransform.Y 為正時為向下偏移,初始狀態下StackPanel上邊框是和ListBox的上邊框重合的,這時TranslateTransform.Y的值為0。也就是說TranslateTransform.Y的最大值即為ListBox實際高度的一半。TranslateTransform.Y 為負時為向上偏移。可以偏移的程度為StackPanel下邊框到達ListBox的中間時。這個長度為StackPanel的高減去ListBox的一半高度。當然因為向上偏移,所以值為負。這里我當時可能是腦子進水了,我居然用每一個子項的高度來乘以子項的數量來獲取StackPanel的實際高度...如果還沒想通的朋友可以多實驗幾次上面做好的控件就明白了)。
OK,這樣第一個功能就實現了。
Step.2 獲取當前處於最中間的項為選中項
當我們MouseUp的時候就可以決定選中項為最中間的項,同樣給ListBox添加MouseUp事件。
CS:
private void ListBox_MouseUp(object sender, MouseButtonEventArgs e) { isPress = false; //計算出ListBox中心線覆蓋在第幾項 double offset = (ListBox1.ActualHeight / 2 - _TTF.Y) / itemHeight; int selectIndex = (int)offset;//取整 ListBox1.SelectedIndex = selectIndex;//設置當前選中項 }
算法說明:用StackPanel 的偏移位置減去ListBox高的一半即為相對於中心位置的偏移量,向上偏移為負。除以item的高度即為相對於中心位置偏移了幾項。如果結果為2.333那中心線肯定是覆蓋在第三項上了。因為ListBox的Index索引是從0開始,所以直接取整就行了。
Step.3 要會自動對正才顯得高端哦~
所謂對正,即中心線和選中項的中心線重合。什么時候重合呢?即相對偏移項的小數點為0.5的時候。比如偏移了2.33項,當它繼續偏移為2.5的時候就重合了。所以算法就簡單了。我們只要讓動畫來偏移(0.5-0.333)*item的高度的距離就行了。在mouseUp事件里繼續添加動畫。
CS:
private void ListBox_MouseUp(object sender, MouseButtonEventArgs e) { isPress = false; //計算出ListBox中心線覆蓋在第幾項 double offset = (ListBox1.ActualHeight / 2 - _TTF.Y) / itemHeight; int selectIndex = (int)offset;//取整 ListBox1.SelectedIndex = selectIndex;//設置當前選中項 //計算出自動對正需要進行的偏移 double changeOffset = (offset - (int)offset - 0.5) * itemHeight + _TTF.Y; Storyboard sb = new Storyboard(); DoubleAnimation _DA = new DoubleAnimation(); _DA.To = changeOffset; _DA.Duration = new Duration(TimeSpan.FromMilliseconds(300)); sb.Children.Add(_DA); Storyboard.SetTarget(_DA, _TTF); Storyboard.SetTargetProperty(_DA, new PropertyPath("Y")); sb.Begin();//開始動畫 }
Step.4 Loaded時滾動到SelectedIndex項的位置
只要根據選中項計算出需要偏移的位置,然后再模擬一次MouseUp即可。在Loaded事件里面添加.....
CS:
private void StackPanel_Loaded(object sender, RoutedEventArgs e) { //添加偏移屬性 _Panel = sender as StackPanel; _TTF = new TranslateTransform(); _Panel.RenderTransform = _TTF; //獲取item的實際高度 itemHeight = (ListBox1.ItemContainerGenerator.ContainerFromIndex(0) as ListBoxItem).ActualHeight; if (ListBox1.SelectedIndex != -1)//更具初始設置的選中項把它置於最中間 { _TTF.Y = this.ActualHeight / 2 - (ListBox1.SelectedIndex+1) * itemHeight; } ListBox_MouseUp(null, null); }
算法說明:用(SelectedIndex+1)*item的高度計算出偏移量。再減去ListBox高的一半計算出相對於中心線的偏移量。因為偏移是下正上負,取負值。本來可以直接偏移到相應位置。但是,出現點動畫效果才顯的牛X。所以就模擬了一下MouseUp。當然也可以重新寫動畫,實現0到目標偏移位置的滾動,這樣效果更好。
做完收工。
后記
這個做完了以后怎么和先前說好的XAML的代碼有點不一樣啊??我原版做的是WPF的,我把各種事件處理都寫在ItemsPanel的StackPanel里了,所以很簡潔,而移植到silverlight時發現這樣寫怎么都獲取不到鼠標事件,於是,就果斷把事件添加在ListBox里了。
自從把11天梯積分打到1500分以后,一直作為路人的我,感到空前的寂寞。於是果斷轉戰“擼哦擼”,打了幾天,感覺不錯,特別是瑞茲的shift+QW QR QE...虐菜必備啊~~哈哈
