参考文章:通过Measure & Arrange实现UWP瀑布流布局
“所谓瀑布流布局,是多列布局的一种形式,列中元素等比缩放使得自身与列等宽,每列再以StackPanel的形式布局,下一个元素自动排布到最短的那一列上。”
效果图:链接
参考文章中做了许多讲解,本文就不做重复工作了。但是原文并没有一个完整的Demo示例
下文将一步步带你实现,代码部分基本与参考文章一样,不同的地方会做讲解
具体实现
1、新建项目工程WaterfallDemo(废话,没有工程怎么show demo)
2、新建类WaterfallPanel,继承自Panel。这个类设计好之后以后可以多次使用
3、在类WaterfallPanel中重载MeasureOverride函数,代码如下:
protected override Size MeasureOverride(Size availableSize) { // 记录每个流的长度。因为我们用选取最短的流来添加下一个元素。 KeyValuePair<double, int>[] flowLens = new KeyValuePair<double, int>[ColumnNum]; foreach (int idx in Enumerable.Range(0, ColumnNum)) { flowLens[idx] = new KeyValuePair<double, int>(0.0, idx); } // 我们就用2个纵向流来演示,获取每个流的宽度。 double flowWidth = availableSize.Width / ColumnNum; // 为子控件提供沿着流方向上,无限大的空间 Size elemMeasureSize = new Size(flowWidth, double.PositiveInfinity); foreach (UIElement elem in Children) { // 让子控件计算它的大小。 elem.Measure(elemMeasureSize); Size elemSize = elem.DesiredSize; double elemLen = elemSize.Height; var pair = flowLens[0]; // 子控件添加到最短的流上,并重新计算最短流。 // 因为我们为了求得流的长度,必须在计算大小这一步时就应用一次布局。但实际的布局还是会在Arrange步骤中完成。 flowLens[0] = new KeyValuePair<double, int>(pair.Key + elemLen, pair.Value); flowLens = flowLens.OrderBy(p => p.Key).ToArray(); } return new Size(availableSize.Width, flowLens.Last().Key); }
protected override Size ArrangeOverride(Size finalSize) { // 同样记录流的长度。 KeyValuePair<double, int>[] flowLens = new KeyValuePair<double, int>[ColumnNum]; double flowWidth = finalSize.Width / ColumnNum; // 要用到流的横坐标了,我们用一个数组来记录(其实最初是想多加些花样,用数组来方便索引横向偏移。不过本例中就只进行简单的乘法了) double[] xs = new double[ColumnNum]; foreach (int idx in Enumerable.Range(0, ColumnNum)) { flowLens[idx] = new KeyValuePair<double, int>(0.0, idx); xs[idx] = idx * flowWidth; } foreach (UIElement elem in Children) { // 直接获取子控件大小。 Size elemSize = elem.DesiredSize; double elemLen = elemSize.Height; var pair = flowLens[0]; double chosenFlowLen = pair.Key; int chosenFlowIdx = pair.Value; // 此时,我们需要设定新添加的空间的位置了,其实比measure就多了一个Point信息。接在流中上一个元素的后面。 Point pt = new Point(xs[chosenFlowIdx], chosenFlowLen); // 调用Arrange进行子控件布局。并让子控件利用上整个流的宽度。 elem.Arrange(new Rect(pt, new Size(flowWidth, elemSize.Height))); // 重新计算最短流。 flowLens[0] = new KeyValuePair<double, int>(chosenFlowLen + elemLen, chosenFlowIdx); flowLens = flowLens.OrderBy(p => p.Key).ToArray(); } // 直接返回该方法的参数。 return finalSize; }
没错,就是多了ColumnNum变量,因为我们要让这个控件扩张性更高,不能局限于两列布局,因此把列数作为变量ColumnNum
public int ColumnNum { get { return (int)GetValue(ColumnCountProperty); } set { SetValue(ColumnCountProperty, value); } } // Using a DependencyProperty as the backing store for ColumnCount. This enables animation, styling, binding, etc... public static readonly DependencyProperty ColumnCountProperty = DependencyProperty.Register("ColumnNum", typeof(int), typeof(WaterfallPanel), new PropertyMetadata(2));
在空白处输入propdp,然后双击键盘Tab键,就会默认出现一堆代码,在这些代码上做些修改就搞定了。
MyProperty替换成你自定义的名称;ownerclass替换成当前类名,此处为WaterfallPanel;PropertyMetadata填写默认值,我填的是2
以上,自定义的Panel类就完成了。
6、新建类:MyItem,新增三个属性
public double Height { get; set; } public string Text { get; set; } public string Url { get; set; }
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <ItemsControl x:Name="ic"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <!-- 使用我们的自定义布局 --> <local:WaterfallPanel ColumnNum="3"/> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.Template> <ControlTemplate> <ScrollViewer> <ItemsPresenter/> </ScrollViewer> </ControlTemplate> </ItemsControl.Template> <ItemsControl.ItemTemplate> <DataTemplate> <Border Margin="10" Height="{Binding Height}" BorderBrush="{ThemeResource SystemControlBackgroundAccentBrush}" BorderThickness="1" HorizontalAlignment="Stretch"> <TextBlock Text="{Binding Text}"/> </Border> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </Grid>
可以看到我们的自定义控件WaterfallPanel的ColumnNum属性是可以设置的,我们设为3,当然也可以是其他值
public MainPage() { this.InitializeComponent(); Random r = new Random(DateTime.Now.Millisecond); ic.ItemsSource = Enumerable.Range(0, 30).Select(i => new MyItem { Text = i.ToString(), Height = r.Next(100, 300), Url = string.Format("ms-appx:///Assets/Images/{0}.jpg", i) }); }
Url是后面我们用来展示图片的效果,这里暂时没用
9、运行程序,效果如图
10、下面接着来实现图片的瀑布流展示。有的读者应该能自己实现了,还不会的就继续看下去吧。
我反正是遇到一些小困难,听我慢慢道来
首先先修改一下XAML,把DataTemplate中的TextBlock换成Image
<Image Source="{Binding Url}"/>
由于上例的Height属性是用一个随机数产生,如果我们要展示图片,自然不能用这个随机的Height
否则效果就成了这样——每张图片的宽度参差不齐
11、首先想到的方法是获取原始图片的宽和高,然后把宽设置为每个流的宽度,高度根据宽度等比例缩放
一开始想用如下代码来获取某张图片的宽和高
string url = "ms-appx:///Assets/Images/1.jpg"; BitmapImage bmp = new BitmapImage(new Uri(url)); //bmp.PixelWidth //bmp.PixelHeight结果并不能如愿,PixelWidth和PixelHeight都为0
问题下的回答给了个方法
var bitmapImage = new BitmapImage(uri); bitmapImage.ImageOpened += (sender, e) => { Debug.WriteLine("Width: {0}, Height: {1}", bitmapImage.PixelWidth, bitmapImage.PixelHeight); }; image.Source = bitmapImage;但是并不好用,还是靠自己吧(不知道读者们有没有其他的办法)
既然无法设置合适的图片高度值,那就干脆不用。去掉Border的Height属性就大功告成
请看效果图
12、如果把把ItemsControl换成ListView,再进行简单的Style设置,就可以让瀑布流与ListView的特性融合。这里就不做讲解了。
Demo源码下载: https://github.com/hebecherish/WaterfallDemo