天天看點

Win10開發:瀑布流布局

參考文章:通過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));
           

這麼複雜的代碼記不住怎麼辦,因為我們用的是全宇宙最強IDE,是以這裡有個小技巧:

在空白處輸入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、運作程式,效果如圖

Win10開發:瀑布流布局

10、下面接着來實作圖檔的瀑布流展示。有的讀者應該能自己實作了,還不會的就繼續看下去吧。

我反正是遇到一些小困難,聽我慢慢道來

首先先修改一下XAML,把DataTemplate中的TextBlock換成Image

<Image Source="{Binding Url}"/>
           

由于上例的Height屬性是用一個随機數産生,如果我們要展示圖檔,自然不能用這個随機的Height

否則效果就成了這樣——每張圖檔的寬度參差不齊

Win10開發:瀑布流布局

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屬性就大功告成

請看效果圖

Win10開發:瀑布流布局

12、如果把把ItemsControl換成ListView,再進行簡單的Style設定,就可以讓瀑布流與ListView的特性融合。這裡就不做講解了。

Demo源碼下載下傳: https://github.com/hebecherish/WaterfallDemo

繼續閱讀