在淘寶UWP中,搜索結果列表是用戶了解寶貝的重要一環,其中的圖片效果對吸引用戶點擊搜索結果,查看寶貝詳情有比較大的影響。為此手機淘寶特意在搜索結果列表上采用了2種表現方式:一種就是普通的列表模式,而另一種則是突出寶貝圖片的瀑布流模式。
如果用戶搜索某些關鍵字,如女裝類的情況下,淘寶的搜索結果會自動切換到瀑布流模式,讓寶貝的美圖更加沖擊用戶的視覺。
但是UWP默認的列表控件並沒有這種效果,listview控件中雖然子元素可以不一樣大小,但是只能有1列,gridview控件雖然有多列,但每個子元素都只能取相同大小。經過一番搜索,也只有元素由固定大小的不同倍數構成的gridview控件可以使用,但效果並不理想。那么我們有沒有辦法能得到瀑布流的效果的控件呢?答案是肯定的。我們可能記得在listview中,如果我們要改變列表的擴展方向,需要在xaml中定義listview的itemspanel:

<ListView> <ListView.ItemsPanel> <ItemsPanelTemplate> <ItemsWrapGrid Orientation="Horizontal"></ItemsWrapGrid> </ItemsPanelTemplate> </ListView.ItemsPanel> </ListView>
在gridview中設置最大的行數或列數時,我們也要定義ItemsWrapGrid。
這里的ItemsStackPanel,ItemsWrapGrid與我們之前在淘寶UWP--自定義Panel中所提到的panel有什么關系呢?
實際上它們都是繼承自panel的FrameworkElement,也就是說它們都可以對內部的子元素進行布局。不管listview還是gridview,他們列表的形式都是由itemsPanel決定的,listview只有1列,可以縱向或者橫向擴展,是由它使用的itemsPanel- ItemsStackPanel確定的,gridview可以有多列,可以縱向或者橫向擴展,也是由它使用了ItemsWrapGrid作為itemsPanel來決定的。那么如果我們根據淘寶UWP--自定義Panel中提到的方法,自定義一個panel,就可以實現瀑布流中形式的列表了。
整理需求
確定了要實現一個瀑布流的布局panel,我們接下來考慮一下我們的具體有哪些需求呢?在淘寶的搜索結果瀑布流中,只用了2列。但是考慮到我們的淘寶UWP可能運行在PC或者平板等橫向屏幕的設備上,如果也用2列的話會有很多圖只能在屏幕中顯示一部分。所以在PC或者平板等橫向屏幕的設備上,我們要讓瀑布流的列數增加,也就是說我們的panel需要能自定義列數。
在淘寶的搜索結果瀑布流中,寶貝的搜索結果是縱向擴展的,那么有沒有可能有情況需要使用橫向擴展的瀑布流呢?想想似乎是比較酷的,那么就為我們的panel加上擴展方向的選擇吧。
着手實現
在確定了具體需求之后就可以開始着手實現我們的自定義panel了。
我們的面板的名字就叫WaterfallPanel吧,需要繼承panel類型,能定義行數或者列數NumberOfColumnsOrRows,能定義擴展方向WaterfallOrientation,並實現MeasureOverride和ArrangeOverride方法:

public class WaterfallPanel :Panel { public int NumbersOfColumnsOrRows { get { return (int)GetValue(NumbersOfColumnsOrRowsProperty); } set { SetValue(NumbersOfColumnsOrRowsProperty, value); } } // Using a DependencyProperty as the backing store for NumbersOfColumnsOrRows. This enables animation, styling, binding, etc... public static readonly DependencyProperty NumbersOfColumnsOrRowsProperty = DependencyProperty.Register("NumbersOfColumnsOrRows", typeof(int), typeof(WaterfallPanel), new PropertyMetadata(2)); public Orientation WaterfallOrientation { get { return (Orientation)GetValue(WaterfallOrientationProperty); } set { SetValue(WaterfallOrientationProperty, value); } } // Using a DependencyProperty as the backing store for WaterfallOrientation. This enables animation, styling, binding, etc... public static readonly DependencyProperty WaterfallOrientationProperty = DependencyProperty.Register("WaterfallOrientation", typeof(Orientation), typeof(WaterfallPanel), new PropertyMetadata(Orientation.Vertical)); protected override Size MeasureOverride(Size availableSize) { return base.MeasureOverride(availableSize); } protected override Size ArrangeOverride(Size finalSize) { return base.ArrangeOverride(finalSize); } }
這就是我們的panel的雛形了,需要注意的是我們的NumberOfColumnsOrRows,和WaterfallOrientation屬性需要能在xaml中調用,因此必須寫成DependencyProperty的形式。在寫的時候可以用先輸入propdp,再按tab鍵,在vs自動生成的模板上進行修改的方法,能方便很多。考慮到用戶也可能會不輸入行列數或者擴展方向,我們給了它們默認值顯示2行或列,縱向擴展。
首先我們來實現MeasureOverride方法。MeasureOverride方法接受一個panel可以占據的空間大小availableSize,再根據這個availableSize給內部的子元素分配可以占據的空間大小。在瀑布流中,以縱向擴展為例,每個元素的最大寬度都是相等的,都是panel寬度的列數分之一。而每個元素的高度則可以自由擴展。因此根據這樣的思路我們的MeasureOverride方法的實現應該是:

protected override Size MeasureOverride(Size availableSize) { if (NumberOfColumnsOrRows < 1) { throw (new ArgumentOutOfRangeException("NumberOfColumnsOrRows", "NumberOfColumnsOrRows must >0"));//太窄 } var LenList = new List<double>(); for (int i = 0; i < NumberOfColumnsOrRows; i++) { LenList.Add(0); } if (WaterfallOrientation == Orientation.Vertical) { double maxWidth = availableSize.Width / NumberOfColumnsOrRows; Size maxSize = new Size(maxWidth, double.PositiveInfinity); foreach (var item in Children) { item.Measure(maxSize); var itemHeight = item.DesiredSize.Height; var minLen = LenList[0]; int minP = 0; for (int i = 1; i < NumberOfColumnsOrRows; i++) { if (LenList[i] < minLen) { minLen = LenList[i]; minP = i; } } LenList[minP] += itemHeight; } var maxLen = LenList[0]; int maxP = 0; for (int i = 1; i < NumberOfColumnsOrRows; i++) { if (LenList[i] > maxLen) { maxLen = LenList[i]; maxP = i; } } return new Size(availableSize.Width, LenList[maxP]); } else { double maxHeight = availableSize.Height / NumberOfColumnsOrRows; Size maxSize = new Size(double.PositiveInfinity, maxHeight); foreach (var item in Children) { item.Measure(maxSize); var itemWidth = item.DesiredSize.Width; var minLen = LenList[0]; int minP = 0; for (int i = 1; i < NumberOfColumnsOrRows; i++) { if (LenList[i] < minLen) { minLen = LenList[i]; minP = i; } } LenList[minP] += itemWidth; } var maxLen = LenList[0]; int maxP = 0; for (int i = 1; i < NumberOfColumnsOrRows; i++) { if (LenList[i] > maxLen) { maxLen = LenList[i]; maxP = i; } } return new Size(LenList[maxP], availableSize.Height); } }
接下來實現我們的ArrangeOverride方法。在ArrangeOverride方法中,會接受一個可以進行布局的空間大小finalSize,在這個空間中將子元素逐個定位在合適的位置。在我們的瀑布流panel中,我們要將子元素定位成瀑布流的效果。那么如何實現瀑布流的效果呢?以縱向的情況為例,瀑布流中每個元素的寬度一致而長度不一,排成一定數量的列,每列長度雖然參差但差距不大,並列排在panel中形成瀑布的樣子。我們可以將panel分成若干列,將子元素分配到這些列中按縱向擴展的順序排布,每次分配時都挑總長最短的列,將新元素分配到這列。這樣就能讓各個列的長度差距不大,滿足瀑布流的效果。按照這個思路,我們實現了ArrangeOverride方法:

protected override Size ArrangeOverride(Size finalSize) { if (NumberOfColumnsOrRows < 1) { throw (new ArgumentOutOfRangeException("NumberOfColumnsOrRows", "NumberOfColumnsOrRows must >0"));//太窄 } var LenList = new List<double>(); var posXorYList = new List<double>(); if (WaterfallOrientation == Orientation.Vertical) { double maxWidth = finalSize.Width / NumberOfColumnsOrRows; //列的長度和左上角的x值 for (int i = 0; i < NumberOfColumnsOrRows; i++) { LenList.Add(0); posXorYList.Add(i * maxWidth); } foreach (var item in Children) { var itemHeight = item.DesiredSize.Height; var minLen = LenList[0]; int minP = 0; for (int i = 1; i < NumberOfColumnsOrRows; i++) { if (LenList[i] < minLen) { minLen = LenList[i]; minP = i; } } item.Arrange(new Rect(posXorYList[minP], LenList[minP], item.DesiredSize.Width, item.DesiredSize.Height)); LenList[minP] += item.DesiredSize.Height; } var maxLen = LenList[0]; int maxP = 0; for (int i = 1; i < NumberOfColumnsOrRows; i++) { if (LenList[i] > maxLen) { maxLen = LenList[i]; maxP = i; } } return new Size(finalSize.Width, LenList[maxP]); } else { double maxHeight = finalSize.Height / NumberOfColumnsOrRows; //行的長度和左上角的y值 for (int i = 0; i < NumberOfColumnsOrRows; i++) { LenList.Add(0); posXorYList.Add(i * maxHeight); } foreach (var item in Children) { var itemWidth = item.DesiredSize.Width; var minLen = LenList[0]; int minP = 0; for (int i = 1; i < NumberOfColumnsOrRows; i++) { if (LenList[i] < minLen) { minLen = LenList[i]; minP = i; } } item.Arrange(new Rect(LenList[minP], posXorYList[minP], item.DesiredSize.Width, item.DesiredSize.Height)); LenList[minP] += item.DesiredSize.Width; } var maxLen = LenList[0]; int maxP = 0; for (int i = 1; i < NumberOfColumnsOrRows; i++) { if (LenList[i] > maxLen) { maxLen = LenList[i]; maxP = i; } } return new Size(LenList[maxP], finalSize.Height); } }
在MeasureOverride方法和ArrangeOverride方法實現之后,我們的瀑布流panel就可以說初步完成了。實際的運行效果和我們的淘寶UWP版中是基本一致的,只不過在淘寶UWP版的不斷迭代中,我們又對一些細節做了優化。另外需要注意的是如果使用橫向瀑布流,需要把WaterfallPanel所屬的listview或gridview的scrollviewer相關的值進行設置:
ScrollViewer.VerticalScrollMode="Disabled" ScrollViewer.HorizontalScrollMode="Enabled" ScrollViewer.HorizontalScrollBarVisibility="Hidden" ScrollViewer.VerticalScrollBarVisibility="Disabled"
否則會由於listview或gridview的默認設置是縱向擴展,從而在MeasureOverride方法傳入的availableSize的height是無限大,最終導致計算錯誤而應用崩潰。
這樣看來只要掌握了方法和思路,自定義panel也並沒有想象中那么困難。小伙伴們也可以嘗試創建自己獨有的列表控件,如果你有一些奇思妙想的話,也歡迎分享出來。
讓我們共同進步,讓UWP應用更加完善。