參考文章:通過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、運作程式,效果如圖
10、下面接着來實作圖檔的瀑布流展示。有的讀者應該能自己實作了,還不會的就繼續看下去吧。
我反正是遇到一些小困難,聽我慢慢道來
首先先修改一下XAML,把DataTemplate中的TextBlock換成Image
<Image Source="{Binding Url}"/>
由于上例的Height屬性是用一個随機數産生,如果我們要展示圖檔,自然不能用這個随機的Height
否則效果就成了這樣——每張圖檔的寬度參差不齊
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屬性就大功告成
請看效果圖
12、如果把把ItemsControl換成ListView,再進行簡單的Style設定,就可以讓瀑布流與ListView的特性融合。這裡就不做講解了。
Demo源碼下載下傳: https://github.com/hebecherish/WaterfallDemo