簡介
在以XAML為主的控件布局體系中,有用於完成布局的核心步驟,分別是measure和arrange。繼承體系中由UIElement類提供Measure和Arrange方法,並由其子類FrameworkElement類提供protected的MeasureOverride和ArrangeOverride方法來為自定義控件提供實現自定義布局的接口。本文通過一個瀑布流布局實現來為大家簡單地介紹這兩個核心方法。
所謂瀑布流布局,是多列布局的一種形式,列中元素等比縮放使得自身與列等寬,每列再以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)。配合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和子控件的大小計算都已結束。
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>
效果如下:
我們可以直接把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開發中出現更多有趣的設計和實現。