經過Measure & Arrange實現UWP瀑布流佈局

簡介

在以XAML爲主的控件佈局體系中,有用於完成佈局的核心步驟,分別是measure和arrange。繼承體系中由UIElement類提供MeasureArrange方法,並由其子類FrameworkElement類提供protected的MeasureOverrideArrangeOverride方法來爲自定義控件提供實現自定義佈局的接口。本文經過一個瀑布流佈局實現來爲你們簡單地介紹這兩個核心方法。數組

所謂瀑布流佈局,是多列布局的一種形式,列中元素等比縮放使得自身與列等寬,每列再以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

 

MeasureOverride方法

一言以蔽之,獲取大小。佈局

每一個控件有提供給外部調用的Measure方法,用來決定該控件須要的空間。這個方法會對佈局設置進行簡單的處理,好比對Margin等屬性進行預處理,而後把主要的步驟交給MeasureOverride方法。動畫

這一方法的參數表明了該控件自己能擁有的大小。佈局時須要考慮到它。spa

在這一方法中,控件須要作的就是遍歷全部子控件,並調用他們的Measure方法,按照本身的佈局方式對這些空間的大小進行運算。最後遞歸出一個總的空間大小,而後返回給它的父控件。設計

在這一過程當中,按照須要,可能連子控件的位置信息也須要考慮(好比咱們的瀑布流)。code

全部的控件在計算完本身的所需控件後,會設置本身的DesiredSize屬性,代表它所需的尺寸。這一屬性在以後的Arrange過程當中可使用(不過不要在非自定義佈局的狀況下使用哦)。blog

此時控件和子控件的大小都已經肯定了。繼承

 

咱們經過繼承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>

效果以下:

 

咱們能夠直接把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>

 

總結

這篇博客只是爲你們介紹了一下對MeasureArrange的簡單嘗試,但XAML中的控件卻所有依賴這樣的規則來完成佈局。

每當你們遇到不一樣的控件組合達到的效果時,好比用Canvas可讓內容畫在範圍以外,StackPanel對其內容的處理等等,每每能夠經過分析那個控件樹的Measure和Arrange過程從中得到解答。

但願本文拋磚引玉,讓UWP開發中出現更多有趣的設計和實現。

相關文章
相關標籤/搜索