在以XAML为主的控件布局体系中,有用于完成布局的核心步骤,分别是measure和arrange。继承体系中由UIElement类提供Measure和Arrange方法,并由其子类FrameworkElement类提供protected的MeasureOverride和ArrangeOverride方法来为自定义控件提供实现自定义布局的接口。本文经过一个瀑布流布局实现来为你们简单地介绍这两个核心方法。数组
所谓瀑布流布局,是多列布局的一种形式,列中元素等比缩放使得自身与列等宽,每列再以StackPanel的形式布局,下一个元素自动排布到最短的那一列上。dom
大体效果能够参考百度图片首页,点击“摄影”,“美食”或“宠物”后进入的页面效果。(宠物here:http://image.baidu.com/channel?c=%E5%AE%A0%E7%89%A9&t=%E5%85%A8%E9%83%A8&s=0)ide
一言以蔽之,获取大小。布局
每一个控件有提供给外部调用的Measure方法,用来决定该控件须要的空间。这个方法会对布局设置进行简单的处理,好比对Margin等属性进行预处理,而后把主要的步骤交给MeasureOverride方法。动画
这一方法的参数表明了该控件自己能拥有的大小。布局时须要考虑到它。spa
在这一方法中,控件须要作的就是遍历全部子控件,并调用他们的Measure方法,按照本身的布局方式对这些空间的大小进行运算。最后递归出一个总的空间大小,而后返回给它的父控件。设计
在这一过程当中,按照须要,可能连子控件的位置信息也须要考虑(好比咱们的瀑布流)。code
全部的控件在计算完本身的所需控件后,会设置本身的DesiredSize属性,代表它所需的尺寸。这一属性在以后的Arrange过程当中可使用(不过不要在非自定义布局的状况下使用哦)。blog
此时控件和子控件的大小都已经肯定了。继承
咱们经过继承Panel来实现本身的瀑布流布局,这么作的目的,主要是能够将Panel用于ItemsControl及其子类的ItemsPanel属性(Panel类此时或许能够有另外一个名字:LayoutPolicy)。配合ItemTemplate和ItemsSource,能够方便的填充和具象数据。
让咱们看看如何实现一个这样行为的MeasureOverride:
protected override Size MeasureOverride(Size availableSize) { // 记录每一个流的长度。由于咱们用选取最短的流来添加下一个元素。 KeyValuePair<double, int>[] flowLens = new KeyValuePair<double, int>[2]; foreach (int idx in Enumerable.Range(0, 2)) { flowLens[idx] = new KeyValuePair<double, int>(0.0, idx); } // 咱们就用2个纵向流来演示,获取每一个流的宽度。 double flowWidth = availableSize.Width / 2; // 为子控件提供沿着流方向上,无限大的空间 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);
}
返回值是该元素自己实际须要的大小。
可看出咱们也没有考虑缩放的问题。若是子控件要求的大小(特别是宽度)比流的宽度要大,就会致使显示不全的状况。这一点咱们能够经过ViewBox来调整,不必定要在这个panel里实现(固然有特殊需求的除外)。
至此,panel和子控件的大小计算都已结束。
Arrange,一言以蔽之,设置位置和大小。
这里的大小,就是经过Measure系列方法肯定的DesiredSize。
在ArrangeOverride方法中,咱们要作的,一样是遍历子控件,利用它们在Measure过程当中肯定的大小,来为它们加上位置信息。
能够看到,虽然咱们的瀑布流panel在measure过程当中也记录了位置信息,但只是用于计算总大小。而在arrange过程当中,位置信息将被确实的利用上。
让咱们看看ArrangeOverride方法的实现。对本例来讲,它和MeasureOverride十分类似。
protected override Size ArrangeOverride(Size finalSize) { // 一样记录流的长度。 KeyValuePair<double, int>[] flowLens = new KeyValuePair<double, int>[2]; double flowWidth = finalSize.Width / 2; // 要用到流的横坐标了,咱们用一个数组来记录(其实最初是想多加些花样,用数组来方便索引横向偏移。不过本例中就只进行简单的乘法了) double[] xs = new double[2]; foreach (int idx in Enumerable.Range(0, 2)) { 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;
}
至此,整个流的布局都已经完成。
让咱们看看,这个瀑布流实现了怎样的效果。
咱们先定义个结构,主要使用随机数来形成流中元素良莠不齐的效果:
class MyItem { private double _height = double.NaN; public double Height { get { if (double.IsNaN(_height)) { Random r = new Random(); _height = 200 + (r.NextDouble() - 0.5) * 100; } return _height; }
} public string Text { get; set; }
}
ic是一个ItemsControl(也能够是其子类,如ListView。这样咱们的panel就只负责布局,至于子控件的点击行为,动画行为,所有交给ListView)。
咱们在UI事件中设置数据源:
ic.ItemsSource = Enumerable.Range(0, 30).Select(i => new MyItem { Text = i.ToString() });
XAML中对ItemsControl的设置以下。Border尝试占满其水平空间。同时全部的流内容能够上下滚动。
<ItemsControl x:Name="ic"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <!-- 使用咱们的自定义布局 --> <local:MyPanel /> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.Template> <ControlTemplate> <ScrollViewer> <ItemsPresenter/> </ScrollViewer> </ControlTemplate> </ItemsControl.Template> <ItemsControl.ItemTemplate> <DataTemplate> <Border Margin="10" Height="{Binding Height}" BorderBrush="Aqua" BorderThickness="5" HorizontalAlignment="Stretch"> <TextBlock Text="{Binding Text}" HorizontalAlignment="Center" VerticalAlignment="Center"/> </Border> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl>
效果以下:
咱们能够直接把ItemsControl换成ListView,再进行简单的Style设置,直接让咱们的瀑布流与ListView的丰富特性融合:
<ListView x:Name="ic" SelectionMode="Multiple"> <ListView.ItemsPanel> <ItemsPanelTemplate> <local:MyPanel /> </ItemsPanelTemplate> </ListView.ItemsPanel> <ListView.ItemContainerStyle> <Style TargetType="ListViewItem"> <Setter Property="HorizontalContentAlignment" Value="Stretch"/> </Style> </ListView.ItemContainerStyle> <ListView.ItemTemplate> <DataTemplate> <Border Margin="10" Height="{Binding Height}" BorderBrush="Aqua" BorderThickness="5"> <TextBlock Text="{Binding Text}" HorizontalAlignment="Center" VerticalAlignment="Center"/> </Border> </DataTemplate> </ListView.ItemTemplate> </ListView>
这篇博客只是为你们介绍了一下对Measure和Arrange的简单尝试,但XAML中的控件却所有依赖这样的规则来完成布局。
每当你们遇到不一样的控件组合达到的效果时,好比用Canvas可让内容画在范围以外,StackPanel对其内容的处理等等,每每能够经过分析那个控件树的Measure和Arrange过程从中得到解答。
但愿本文抛砖引玉,让UWP开发中出现更多有趣的设计和实现。