参考文章:通过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);
}
4、在类WaterfallPanel中重载ArrangeOverride函数,代码如下:
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;
}
5、步骤3和4的代码与参考文章中有一处不同,认真阅读代码的应该可以发现。
没错,就是多了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; }
7、XAML页面布局:
<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,当然也可以是其他值
8、构造函数中为ItemControl控件设置ItemSource
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)
});
}
Height属性我们用一个随机数产生,是为了让每个Item的高度不同,体现瀑布流的效果
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
搜索网络在stackoverflow中找到同样的问题: 问题链接
问题下的回答给了个方法
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的特性融合。这里就不做讲解了。