转载

通过Measure & Arrange实现UWP瀑布流布局

简介

在以XAML为主的控件布局体系中,有用于完成布局的核心步骤,分别是measure和arrange。继承体系中由 UIElement 类提供 MeasureArrange 方法,并由其子类 FrameworkElement 类提供protected的 MeasureOverrideArrangeOverride 方法来为自定义控件提供实现自定义布局的接口。本文通过一个瀑布流布局实现来为大家简单地介绍这两个核心方法。

所谓瀑布流布局,是多列布局的一种形式,列中元素等比缩放使得自身与列等宽,每列再以 StackPanel 的形式布局,下一个元素自动排布到最短的那一列上。

大致效果可以参考百度图片首页,点击“摄影”,“美食”或“宠物”后进入的页面效果。(宠物here:http://image.baidu.com/channel?c=%E5%AE%A0%E7%89%A9&t=%E5%85%A8%E9%83%A8&s=0)

MeasureOverride方法

一言以蔽之,获取大小。

每个控件有提供给外部调用的 Measure 方法,用来决定该控件需要的空间。这个方法会对布局设置进行简单的处理,比如对 Margin 等属性进行预处理,然后把主要的步骤交给 MeasureOverride 方法。

这一方法的参数代表了该控件本身能拥有的大小。布局时需要考虑到它。

在这一方法中,控件需要做的就是遍历所有子控件,并调用他们的 Measure 方法,按照自己的布局方式对这些空间的大小进行运算。最后递归出一个总的空间大小,然后返回给它的父控件。

在这一过程中,按照需要,可能连子控件的位置信息也需要考虑(比如我们的瀑布流)。

所有的控件在计算完自己的所需控件后,会设置自己的 DesiredSize 属性,表明它所需的尺寸。这一属性在之后的 Arrange 过程中可以使用(不过不要在非自定义布局的情况下使用哦)。

此时控件和子控件的大小都已经确定了。

我们通过继承 Panel 来实现自己的瀑布流布局,这么做的目的,主要是可以将 Panel 用于 ItemsControl 及其子类的 ItemsPanel 属性(Panel类此时或许可以有另一个名字:LayoutPolicy)。配合 ItemTemplateItemsSource ,可以方便的填充和具象数据。

让我们看看如何实现一个这样行为的 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和子控件的大小计算都已结束。

ArrangeOverride方法

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> 

效果如下:

通过Measure &amp; Arrange实现UWP瀑布流布局

我们可以直接把 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 &amp; Arrange实现UWP瀑布流布局

总结

这篇博客只是为大家介绍了一下对 MeasureArrange 的简单尝试,但XAML中的控件却全部依赖这样的规则来完成布局。

每当大家遇到不同的控件组合达到的效果时,比如用 Canvas 可以让内容画在范围之外, StackPanel 对其内容的处理等等,往往可以通过分析那个控件树的Measure和Arrange过程从中获得解答。

希望本文抛砖引玉,让UWP开发中出现更多有趣的设计和实现。

正文到此结束
Loading...