瀑布流已经有点年代了吧,不过wp上还真是挺少资料的。今天抽空把自己之前搞过的东西写出来,避免大家重复劳动。
一、简单的瀑布流排版加入ui虚拟化。
最近看了 段博琼 ui虚拟化的一篇博文,链接:http://www.cnblogs.com/hebeiDGL/p/3410575.html
觉得还不错,于是下载了他的demo稍微改了一下瀑布流效果。
demo截图如下:
主要改动:
1:自定义WaterFallPanel继承Panel,用于实现瀑布流排版,并保持容器children距离顶部高度的信息:
public class WaterFallPanel : Panel { public WaterFallPanel() { /**默认2列**/ ColumnCount = 2; ColumnHeight = new double[ColumnCount]; childwidth = 480 / ColumnCount; } double childwidth; static double[] ColumnHeight; /// <summary> /// 列数 /// </summary> public int ColumnCount { get { return (int)this.GetValue(ColumnCountProperty); } set { this.SetValue(ColumnCountProperty, value); } } public static DependencyProperty ColumnCountProperty = DependencyProperty.Register("WaterFallPanel", typeof(int), typeof(WaterFallPanel), new PropertyMetadata(new PropertyChangedCallback((o, e) => { ColumnHeight = new double[(int)e.NewValue]; if (o == null || e.NewValue == e.OldValue) return; o.SetValue(ColumnCountProperty, e.NewValue); }))); protected override Size MeasureOverride(Size availableSize) { Size resultSize = new Size(0, 0); //List<DependencyObject> Children = this.GetVisualChildren().ToList(); for (int i = 0; i < Children.Count; i++) { Children[i].Measure(availableSize); } #region 测量值 for (int i = 0; i < ColumnHeight.Count(); i++) { ColumnHeight[i] = 0; } heightlist.Clear(); for (int i = 0; i < Children.Count; i++) { double miniheight = ColumnHeight.Min(); int h = 0; for (; h < ColumnHeight.Length; h++) { if (ColumnHeight[h] == miniheight) { break; } } ColumnHeight[h] += Children[i].DesiredSize.Height; heightlist.Add(ColumnHeight.Min()); } #endregion resultSize.Height = ColumnHeight.Max(); if (Children.Count == 0) { resultSize.Height = 0; resultSize.Width = 480; } else { resultSize.Width = (Children.Count + 1) * Children[0].DesiredSize.Width; } return resultSize; } protected override Size ArrangeOverride(Size finalSize) { for (int i = 0; i < ColumnHeight.Count(); i++) { ColumnHeight[i] = 0; } #region 排列值 heightlist.Clear(); for (int i = 0; i < Children.Count; i++) { double miniheight = ColumnHeight.Min(); int h = 0; for (; h < ColumnHeight.Length; h++) { if (ColumnHeight[h] == miniheight) { break; } } Children[i].Arrange(new Rect(new Point(Children[i].DesiredSize.Width * h, ColumnHeight[h]), Children[i].DesiredSize)); ColumnHeight[h] += Children[i].DesiredSize.Height; if (h == 0) { if (Children[i].DesiredSize.Width > childwidth) { ColumnHeight[h + 1] += Children[i].DesiredSize.Height; } } heightlist.Add(ColumnHeight.Min()); } if (Children.Count < ColumnCount) finalSize.Width = childwidth * (Children.Count + 1); else finalSize.Width = childwidth * (ColumnCount + 1); #endregion return finalSize; } private List<double> heightlist = new List<double>(); public double GetItemHeight(int item) { if (item >= 0 && item < heightlist.Count) { return heightlist[item]; } else { return 0; } } }
2:MainPage.xaml中修改itemspanl
<ItemsControl.ItemsPanel> <ItemsPanelTemplate> <LocalControl:WaterFallPanel x:Name="test" Loaded="test_Loaded"/> </ItemsPanelTemplate> </ItemsControl.ItemsPanel>
3:MainPage.cs中修改虚拟化的计算方式:
void Visualizition() { // 自定义一个“字典”,用来保存每项的 Y坐标 和它“本身”的引用 Dictionary<double, StackPanel> dic = new Dictionary<double, StackPanel>(); double height_sum = 0; for (int i = 0; i < listbox.Items.Count; i++) { // 因为 ListBox 是通过数据绑定生成的列表,所以需要通过 ItemContainerGenerator 获得 // 每一项的 StackPanel 这个父控件 FrameworkElement fe = listbox.ItemContainerGenerator.ContainerFromIndex(i) as FrameworkElement; StackPanel sp = FindFirstElementInVisualTree<StackPanel>(fe); dic.Add(height_sum, sp); // 累加 Y高度 if (height_sum <= waterpanel.GetItemHeight(i)) { height_sum = waterpanel.GetItemHeight(i)+1; } else { height_sum = waterpanel.GetItemHeight(i); } // 设置它的高度为自己的实际高度 sp.Height = sp.ActualHeight; } // 每0.5秒钟,循环检查一次列表中,哪些项在屏幕内,如果在屏幕内,则显示,如果 // 在屏幕外,则隐藏 Observable.Interval(TimeSpan.FromSeconds(.5)).ObserveOnDispatcher().Subscribe((_) => { foreach (var keyValue in dic) { if (((scrollViewer.VerticalOffset - 700) > keyValue.Key || keyValue.Key > (scrollViewer.VerticalOffset + 900))) { keyValue.Value.Children[0].Visibility = System.Windows.Visibility.Collapsed; keyValue.Value.Children[1].Visibility = System.Windows.Visibility.Collapsed; } else { keyValue.Value.Children[0].Visibility = System.Windows.Visibility.Visible; keyValue.Value.Children[1].Visibility = System.Windows.Visibility.Visible; } } }); }
其中WaterFallPanel的获取用了比较奇葩的一个方式。
这样做的好处是代码简单明了,适合新手了解ui虚拟化。不好的地方则是每次想实现瀑布流排版,都得手动写入虚拟化计算代码。代码耦合过高。
二、通过自定义listbox实现瀑布流&虚拟化。降低代码耦合度。
demo截图:
1、自定义PowerListBox继承listbox
public class PowerListBox:ListBox { // 自定义一个“字典”,用来保存每项的 Y坐标 和它“本身”的引用 Dictionary<double, Border> dic = new Dictionary<double, Border>(); public PowerListBox() { DefaultStyleKey = typeof(PowerListBox); Loaded += PowerListBox_Loaded; Unloaded += PowerListBox_Unloaded; } private ScrollViewer _scrollView; private bool _isScrolling; void PowerListBox_Loaded(object sender, System.Windows.RoutedEventArgs e) { if (DesignerProperties.GetIsInDesignMode(this)) return; ApplyTemplate(); _scrollView.ScrollToVerticalOffset(_scrollView.VerticalOffset); var border = _scrollView.Descendants<Border>().FirstOrDefault(); Debug.Assert(border != null); var vsGroup = VisualStateManager.GetVisualStateGroups(border).OfType<VisualStateGroup>().FirstOrDefault(s => s.Name == "ScrollStates"); Debug.Assert(vsGroup != null); vsGroup.CurrentStateChanging += vsGroup_CurrentStateChanging; if (ItemsRootPanel != null && ItemsRootPanel.Children != null) { double height_sum=0; dic.Clear(); for (int i = 0; i < ItemsRootPanel.Children.Count; i++) { // 因为 ListBox 是通过数据绑定生成的列表,所以需要通过 ItemContainerGenerator 获得 // 每一项的 StackPanel 这个父控件 Border fe = VisualTreeHelper.GetChild(ItemsRootPanel.Children[i], 0) as Border; dic.Add(height_sum, fe); // 累加 Y高度 //var height = .GetItemHeight(i); if (height_sum <= (ItemsRootPanel as WaterFallPanel).GetItemHeight(i)) { height_sum = (ItemsRootPanel as WaterFallPanel).GetItemHeight(i) + 1; } else { height_sum = (ItemsRootPanel as WaterFallPanel).GetItemHeight(i); } // 设置它的高度为自己的实际高度 fe.Height = fe.ActualHeight; } } } void PowerListBox_Unloaded(object sender, System.Windows.RoutedEventArgs e) { var border = _scrollView.Descendants<Border>().FirstOrDefault(); Debug.Assert(border != null); var vsGroup = VisualStateManager.GetVisualStateGroups(border).OfType<VisualStateGroup>().FirstOrDefault(s => s.Name == "ScrollStates"); Debug.Assert(vsGroup != null); vsGroup.CurrentStateChanging -= vsGroup_CurrentStateChanging; } public override void OnApplyTemplate() { base.OnApplyTemplate(); _scrollView = GetTemplateChild("ScrollViewer") as ScrollViewer; Debug.Assert(_scrollView != null); } void vsGroup_CurrentStateChanging(object sender, VisualStateChangedEventArgs e) { if (e.NewState.Name == "NotScrolling") { _isScrolling = false; CompositionTarget.Rendering -= CompositionTarget_Rendering; } else { _isScrolling = true; CompositionTarget.Rendering += CompositionTarget_Rendering; } } void CompositionTarget_Rendering(object sender, EventArgs e) { RefreshDisplayArea(); } void RefreshDisplayArea() { foreach (var keyValue in dic) { if (((_scrollView.VerticalOffset - 20) > keyValue.Key || keyValue.Key > (_scrollView.VerticalOffset + 600))) { keyValue.Value.Child.Visibility = Visibility.Collapsed; } else { keyValue.Value.Child.Visibility = Visibility.Visible; } } //for (int i = 0; i < ItemsRootPanel.Children.Count; i++) //{ // var item = GetItem(i); // var height = (ItemsRootPanel as WaterFallPanel).GetItemHeight(i) ; // if (((_scrollView.VerticalOffset - 20) > height || height > (_scrollView.VerticalOffset + 607))) // { // FrameworkElement fe = VisualTreeHelper.GetChild(item, 0) as FrameworkElement; // fe.Visibility = Visibility.Collapsed; // //keyValue.Value.Children[0].Visibility = System.Windows.Visibility.Collapsed; // //keyValue.Value.Children[1].Visibility = System.Windows.Visibility.Collapsed; // } // else // { // FrameworkElement fe = VisualTreeHelper.GetChild(item, 0) as FrameworkElement; // fe.Visibility = Visibility.Visible; // //keyValue.Value.Children[0].Visibility = System.Windows.Visibility.Visible; // //keyValue.Value.Children[1].Visibility = System.Windows.Visibility.Visible; // } //} } private ListBoxItem GetItem(int index) { if (index < 0 || index >= Items.Count) return null; if (ItemsSource != null) { var isVirtualizing = VirtualizingStackPanel.GetIsVirtualizing(this); if (!isVirtualizing) { Debug.Assert(ItemsRootPanel != null); return ItemsRootPanel.Children[index] as ListBoxItem; } Debug.Assert(ItemContainerGenerator != null); return ItemContainerGenerator.ContainerFromIndex(index) as ListBoxItem; } return Items[index] as ListBoxItem; } #region FirstVisibleIndex public static readonly DependencyProperty FirstVisibleIndexProperty = DependencyProperty.Register("FirstVisibleIndex", typeof(int), typeof(PowerListBox), new PropertyMetadata(0)); public int FirstVisibleIndex { get { return (int)GetValue(FirstVisibleIndexProperty); } private set { SetValue(FirstVisibleIndexProperty, value); } } #endregion #region LastVisibleIndex public static readonly DependencyProperty LastVisibleIndexProperty = DependencyProperty.Register("LastVisibleIndex", typeof(int), typeof(PowerListBox), new PropertyMetadata(0)); public int LastVisibleIndex { get { return (int)GetValue(LastVisibleIndexProperty); } private set { SetValue(LastVisibleIndexProperty, value); } } #endregion private Panel _itemsRootPanel; private Panel ItemsRootPanel { get { if (_itemsRootPanel == null && ItemsPresenter != null) { _itemsRootPanel = ItemsPresenter.ChildrenEx().FirstOrDefault() as Panel; } return _itemsRootPanel; } } private ItemsPresenter _itemsPresenter; private ItemsPresenter ItemsPresenter { get { return _itemsPresenter ?? (_itemsPresenter = _scrollView.Descendants<ItemsPresenter>().FirstOrDefault()); } } }
其他见demo。
备注:
1、虚拟化过程中:注意容器的高度为自己的实际高度
// 设置它的高度为自己的实际高度
// 很重要,如果不为父容器指定固定高度,当子元素隐藏后,父容器高度变为0px sp.Height = sp.ActualHeight;
2、计算虚拟化区域有很多种方法,demo中只是简单粗暴的计算,有很多可以进行优化。
3、瀑布流排版只是见当实现两行的,需要多行的话请自行改动,详见附加链接,当然林政的瀑布流排版有点问题,改改更健康。
4、求赞、谢谢。
demo链接:
1:http://files.cnblogs.com/fatlin/VirtualizationListBoxDemo.rar
2:http://files.cnblogs.com/fatlin/TextListbox.rar
其他相关链接:
瀑布流:http://www.cnblogs.com/Smallcode/archive/2012/10/19/2730810.html