上一篇我们已经把界面画出来了,这篇我们就来制作交互的逻辑吧。上一篇的电梯:
回顾下效果:
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:
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...虐菜必备啊~~哈哈