LongListSelector 锁定组头(sticky header )之我的实现
LongListSelector如何实现类似于WP7手机程序列表的效果,将屏幕显示范围内的第一个分组的GroupHeader一直显示在列表的最上方。 国内国外论坛上问的不少,可都没有实现。
无耐之下自己动手搞定,闲话少续,开讲.....
核心思想:取得当前LongListSelector控件可视范围内的第一条数据,取得它的分组名称,然后自己制做一个组头(在我的示例它叫做borderGroupName),覆盖在LongListSelector控件之上。
<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"
................................................................
注意:LongListSelector放在Canvas中会产生冲突,所以仅把borderGroupName要覆盖到LongListSelector只能通过<CompositeTransform TranslateX="-78"/> 这一句来实现了,但万幸Canvas.ZIndex这个属性指定还是有效果的,为borderGroupName设置Canvas.ZIndex="10",LongListSelector的ZIndex只要比它小就OK了
然后说一下后台逻辑:
首先 提前计算一下每个分组的位置,我是把它保存到变量cityGroupSetting,并写到独立存储里
请劳记cityGroupSetting,所有分组位置信息都放在它里面
/// <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();
}
}
在继续下面之前,先了解一下LongListSelector前台的构成
ScrollBar*2(一竖一横)+ ScrollContentPresenter --> ScrollViewer --> (ListBox==>TemplatedListBox) --> LongListSelector
在滚动LongListSelector时,改变的值实际上是ScrollBar的Value
scrollBarlist[0] 是它的竖向滚动条,也是一会后面都会用到的
以下先取得LongListSelect的ScrollBar,并捕捉它的ValueChanged事件
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;
}
在ValueChanged事件中对当前ScrollBar的值与cityGroupSetting中存储的分组位置信息进行比较,取得GroupHeader的名称
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);
}
}
你可能以为这样就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中写下代码,在固定范围内根据鼠标移动像素来设置
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;
}
}
}
当代码写到这的时候我心说差不过完成了吧,可一测试又发现了其它问题,当我们的ListBox到顶得时候,我们继续往下拉,内容会继续往下走一段距离之后不能再继续拉动,放手之后,内容会回弹到原来的位置。所以就涉及到当头问发生压缩的时候,要隐藏我的borderGroupName。
关于这个问题
建议读一下这篇文章:
这里介绍的方法是对以上代码的扩展,在7.1中,我们可以拿到VerticalCompression和HorizontalCompression两种VisualStateGroup,可以用来检测ListBox的上下左右方向的压缩状态。
至此,核心逻辑已说明完毕。
需注意的:
仅适用于静态数据,且LongListSelector大小固定,在LongListSelector大小不同时,ScrollBar.MaxValue也不同。对应各分组的位置也不同。
在极端情况下分组的名称也是会有显示不准确的时候,比如说拿鼠标一个像素一个像素的去拖,正常触控操作还是可以满足的。
如果希望完全模仿wp系统的样式,希望你能找出那个ScrollBar.Value 与像素的准确比例。然后动态的改变borderGroupName的位置。
SystemTray最好不要显示,因为打开分组选择的时候,它会自去把SystemTray推上去,然后页面整体上移。动画会有明显卡顿感的感觉(可以看一下大众点评的城市选择),选择完成后页面在自已移下来。
总体来说这个基本可用,但并不十分完美,希望能抛砖引玉,来个大神把它弄得更完美些,或者微软哪天好心把ToolKit升下级,那是最好了。
示例代码下载地址:
https://skydrive.live.com/redir?resid=FABBBC498CBEABF8!177&authkey=!AOa5Ztr_ylVqVk0