虚位以待(AD)
虚位以待(AD)
首页 > 软件编程 > WindowsPhone/WindowsMobile > LongListSelector锁定组头(sticky header )之我的实现

LongListSelector锁定组头(sticky header )之我的实现
类别:WindowsPhone/WindowsMobile   作者:码皇   来源:互联网   点击:

LongListSelector如何实现类似于WP7手机程序列表的效果,将屏幕显示范围内的第一个分组的GroupHeader一直显示在列表的最上方。国内国外论坛上问的不少,可都没有实现。无耐之下自己动手搞定,闲话少续,开讲

LongListSelector如何实现类似于WP7手机程序列表的效果,将屏幕显示范围内的第一个分组的GroupHeader一直显示在列表的最上方。
国内国外论坛上问的不少,可都没有实现。

无耐之下自己动手搞定,闲话少续,开讲.....

核心思想:取得当前LongListSelector控件可视范围内的第一条数据,取得它的分组名称,然后自己制做一个组头(在我的示例它叫做borderGroupName),覆盖在LongListSelector控件之上。


[html]
<span style="font-size:12px;">  <Border BorderThickness="0" BorderBrush="White"  Visibility="Visible" 
                      Background="Black"   Canvas.ZIndex="10"   Width="300"   Height="80"   VerticalAlignment="Top" RenderTransformOrigin="0.5,0.5"  Grid.Row="1"  Name="borderGroupName"   > 
                    <Border.RenderTransform> 
                        <CompositeTransform TranslateX="-78"/> 
                    </Border.RenderTransform> 
                    <StackPanel> 
                        <Border Background="Transparent" Margin="12,8,0,8" Width="300" > 
                            <Border Background="{StaticResource PhoneAccentBrush}"    
                                         Width="62" Height="62"                   
                                    MouseLeftButtonDown="Border_MouseLeftButtonDown" 
                                        HorizontalAlignment="Left"> 
                                <TextBlock Text="a"  
                                               Foreground="#FFFFFF"  
                                               FontSize="48" 
                                           Margin="8,0,0,0" 
                                               FontFamily="{StaticResource PhoneFontFamilySemiLight}" 
                                               HorizontalAlignment="Left" 
                                               Name="txtGroupName" 
                                                  
                                               VerticalAlignment="Bottom"/> 
                            </Border> 
                        </Border> 
                    </StackPanel> 
                </Border> 
 
                <Border BorderThickness="0" BorderBrush="Red"   Canvas.ZIndex="0"> 
                    <toolkit:LongListSelector x:Name="regionSelector" Background="Transparent" 
                                          GroupViewOpened="LongListSelector_GroupViewOpened" 
                                          GroupViewClosing="LongListSelector_GroupViewClosing" 
                                             ................................................................</span> 

注意:LongListSelector放在Canvas中会产生冲突,所以仅把borderGroupName要覆盖到LongListSelector只能通过<CompositeTransform TranslateX="-78"/>  这一句来实现了,但万幸Canvas.ZIndex这个属性指定还是有效果的,为borderGroupName设置Canvas.ZIndex="10",LongListSelector的ZIndex只要比它小就OK了

然后说一下后台逻辑:

首先 提前计算一下每个分组的位置,我是把它保存到变量cityGroupSetting,并写到独立存储里

请劳记cityGroupSetting,所有分组位置信息都放在它里面

[csharp]
<span style="font-size:12px;">/// <summary> 
        /// 计算位置 
        /// 注意:如果控件大小发生改变,刚需重新计算 
        /// </summary> 
        private void btnGroup_Click(object sender, RoutedEventArgs e) 
        { 
            Debug.WriteLine("BeginTime " + System.DateTime.Now.TimeOfDay.ToString()); 
            List<CityInGroup> tempList = (List<CityInGroup>)(regionSelector.ItemsSource); 
            list = new List<CityInGroup>(); 
            foreach (var item in tempList) 
            { 
                if (item.HasItems) 
                    list.Add(item); 
            } 
 
            cityGroupSetting = new CityGroupSetting(); 
            cityGroupSetting.GroupPositions = new List<GroupPosition>(); 
            cityGroupSetting.GroupPositions.Add(new GroupPosition("a", 0)); 
            regionSelector.ScrollToGroup((list[0])); 
 
            //由于直接跳转所有组,输出结果不准确.引入计时器 
            _timer.Tick += new EventHandler(_timer_Tick); 
            _timer.Start(); 
            Debug.WriteLine("EndTime " + System.DateTime.Now.TimeOfDay.ToString()); 
        } 
 
        /// <summary> 
        /// 计算分组位置,保存为xml文件 
        /// </summary> 
        void _timer_Tick(object sender, EventArgs e) 
        { 
            if (groupIndex < list.Count) 
            { 
                CityInGroup cityGroup = list[groupIndex]; 
                if (cityGroup.HasItems) 
                { 
                    if (groupIndex > 1) 
                    { 
                        Debug.WriteLine(list[groupIndex - 1].Key + "   Max " + scrollBarlist[0].Maximum + "  Value " + scrollBarlist[0].Value + "  " + System.DateTime.Now.TimeOfDay.ToString()); 
                        cityGroupSetting.GroupPositions.Add(new GroupPosition(list[groupIndex - 1].Key, int.Parse(scrollBarlist[0].Value.ToString()))); 
                    } 
                    regionSelector.ScrollToGroup((cityGroup)); 
                } 
                groupIndex++; 
            } 
            else 
            { 
                _timer.Stop(); 
                Debug.WriteLine(list[groupIndex - 1].Key + "   Max " + scrollBarlist[0].Maximum + "  Value " + scrollBarlist[0].Value + "  " + System.DateTime.Now.TimeOfDay.ToString()); 
                cityGroupSetting.GroupPositions.Add(new GroupPosition(list[groupIndex - 1].Key, int.Parse(scrollBarlist[0].Value.ToString()))); 
                cityGroupSetting.Save(); 
            } 
        }</span> 
在继续下面之前,先了解一下LongListSelector前台的构成

ScrollBar*2(一竖一横)+ ScrollContentPresenter  -->  ScrollViewer  -->  (ListBox==>TemplatedListBox)  -->  LongListSelector

在滚动LongListSelector时,改变的值实际上是ScrollBar的Value

scrollBarlist[0] 是它的竖向滚动条,也是一会后面都会用到的

以下先取得LongListSelect的ScrollBar,并捕捉它的ValueChanged事件

[csharp]
<span style="font-size:12px;">     List<ScrollBar> scrollBarlist = new List<ScrollBar>();        
     void CitySelect_Loaded(object sender, RoutedEventArgs e) 
    { 
            GetChildren(regionSelector, ref scrollBarlist); 
            scrollBarlist[0].ValueChanged += CitySelect_ValueChanged; 
    } 
 
        private IList<ScrollBar> GetChildren(UIElement element, ref List<ScrollBar> list) 
        { 
            int count = VisualTreeHelper.GetChildrenCount(element); 
            for (int i = 0; i < count; i++) 
            { 
                DependencyObject child = VisualTreeHelper.GetChild(element, i); 
                if (child is ScrollBar) 
                { 
                    list.Add((ScrollBar)child); 
                } 
                UIElement uiElementChild = child as UIElement; 
                if (uiElementChild != null) 
                { 
                    GetChildren(uiElementChild, ref list); 
                } 
            } 
            return list; 
        }</span> 

在ValueChanged事件中对当前ScrollBar的值与cityGroupSetting中存储的分组位置信息进行比较,取得GroupHeader的名称

[csharp]
<span style="font-size:12px;"> borderGroupName.Visibility = Visibility.Visible; 
            double value = scrollBarlist[0].Value; 
            double v1 = 0; 
            double v2 = 0; 
 
            if (value < .1) 
                borderGroupName.Visibility = Visibility.Collapsed; 
            else 
            { 
                borderGroupName.Visibility = System.Windows.Visibility.Visible; 
 
                string groupName = null; 
 
                for (int i = 0; i < cityGroupSetting.GroupPositions.Count - 1; i++) 
                { 
                    var item1 = cityGroupSetting.GroupPositions[i]; 
                    var item2 = cityGroupSetting.GroupPositions[i + 1]; 
                    v1 = item1.Value - value; 
                    v2 = item2.Value - value; 
                    if (Math.Abs(v1) < 0.1) 
                    { 
                        groupName = item1.GroupName; 
                        break; 
                    } 
                    if (Math.Abs(v2) < 0.1) 
                    { 
                        groupName = item2.GroupName; 
                        break; 
                    } 
                    if (i == cityGroupSetting.GroupPositions.Count - 2 && value >= item2.Value) 
                    { 
                        groupName = item2.GroupName; 
                        break; 
                    } 
                    //.17 为GruopHeader模版与它的容器下边界之间的像素换算出的大概Value,其实在实际手指操作中有无它关系不并大,因为手指操作精度并没那么高 
                   //但本着尽量精确,在此把它加上,它与ScollBar的maxValue是成比例的 
                    if (value >= item1.Value && value <= item2.Value - .17) 
                    { 
                        groupName = item1.GroupName; 
                        break; 
                    } 
                if (groupName != null) 
                { 
                    txtGroupName.Text = groupName; 
                    Debug.WriteLine("GroupName  " + txtGroupName.Text + " v1 " + v1 + " v2 " + v2); 
                } 
            }</span> 

你可能以为这样就ok了,可事实是总有意想不到的操蛋的问题在等着你 ,上面说了根据ValueChanged判断当前的GroupHeader显内容。

坑爹的问题出现了,两次ValueChanged之间会有较大的跨度,具体结果就是在小范围移动的时候,明明你的数据内容都到了b组了,GroupHaeder还显示的是a。

请看我的调试信息   y:为移动像素,后面的那个值 是ScrollBar.Value;


Y: -179  0 ValueChanged
Y: -195  2.47457627118644 ValueChanged
Y: -331  2.47457627118644 ValueChanged
Y: -387  5.32203389830508 ValueChanged
Y: -484  5.32203389830508 ValueChanged
Y: -544  8.10169491525424 ValueChanged

可以看到当我都移动了179像素的时候 ,value还没发生改变,直到195才改变,但它的也并不是很有规律,这点让我很是无语

没法子,经过无数次的测试,在MouseMove中写下代码,在固定范围内根据鼠标移动像素来设置

[csharp]
<span style="font-size:12px;">      private void LongListSelector_MouseMove(object sender, MouseEventArgs e) 
        { 
            if (isBtnDown) 
            { 
                y = (int)e.GetPosition(null).Y - stratY; 
                //txtNum.Text = y.ToString(); 
                //经测试仅鼠标移动超过22个像素才会引发Scrolling事件,个人猜测是MS对触摸屏设置的一个误差范围值 
                if (Math.Abs(y) < 200 && Math.Abs(y) > 22) 
                { 
                    //像素与ScrollBar的Value 的比例 
                    //经测试并不绝对准确,它总是有好像会有一定范围的变化 
                    //仅相对准确的 
                    double scaleValue = 78; 
 
                    double j = -y / scaleValue; 
                    double v = startValue + j; 
                    Debug.WriteLine("Y: " + y + " postion " + v + " scale " + scaleValue); 
                    if (v > 0) 
                        scrollBarlist[0].Value = v; 
                } 
            } 
        }</span> 
当代码写到这的时候我心说差不过完成了吧,可一测试又发现了其它问题,当我们的ListBox到顶得时候,我们继续往下拉,内容会继续往下走一段距离之后不能再继续拉动,放手之后,内容会回弹到原来的位置。所以就涉及到当头问发生压缩的时候,要隐藏我的borderGroupName。
关于这个问题

建议您读一下这篇文章:

http://blogs.msdn.com/b/slmperf/archive/2011/06/30/windows-phone-mango-change-listbox-how-to-detect-compression-end-of-scroll-states.aspx
这里介绍的方法是对以上代码的扩展,在7.1中,我们可以拿到VerticalCompression和HorizontalCompression两种VisualStateGroup,可以用来检测ListBox的上下左右方向的压缩状态。其中也包括了示例代码下载。
 

至此,核心逻辑已说明完毕。

需注意的:

仅适用于静态数据,且LongListSelector大小固定,在LongListSelector大小不同时,ScrollBar.MaxValue也不同。对应各分组的位置也不同。

在极端情况下分组的名称也是会有显示不准确的时候,比如说拿鼠标一个像素一个像素的去拖,正常手指操作准确性还是可以保证的。

如果希望完全模仿wp系统的样式,希望你能找出那个ScrollBar.Value 与像素的准确比例。然后动态的改变borderGroupName的位置。

SystemTray最好不要显示,因为打开分组选择的时候,它会自去把SystemTray推上去,然后页面整体上移。动画会有明显卡顿感的感觉(可以看一下大众点评的城市选择),选择完成后页面在自已移下来。

 


摘自 zl1911的专栏

相关热词搜索: LongListSelector 锁定 sticky