飛流直下的精彩 -- 淘寶UWP中瀑布流列表的實現


在淘寶UWP中,搜索結果列表是用戶了解寶貝的重要一環,其中的圖片效果對吸引用戶點擊搜索結果,查看寶貝詳情有比較大的影響。為此手機淘寶特意在搜索結果列表上采用了2種表現方式:一種就是普通的列表模式,而另一種則是突出寶貝圖片的瀑布流模式。

如果用戶搜索某些關鍵字,如女裝類的情況下,淘寶的搜索結果會自動切換到瀑布流模式,讓寶貝的美圖更加沖擊用戶的視覺。

但是UWP默認的列表控件並沒有這種效果,listview控件中雖然子元素可以不一樣大小,但是只能有1列,gridview控件雖然有多列,但每個子元素都只能取相同大小。經過一番搜索,也只有元素由固定大小的不同倍數構成的gridview控件可以使用,但效果並不理想。那么我們有沒有辦法能得到瀑布流的效果的控件呢?答案是肯定的。我們可能記得在listview中,如果我們要改變列表的擴展方向,需要在xaml中定義listview的itemspanel:

 <ListView>

<ListView.ItemsPanel>

<ItemsPanelTemplate>

<ItemsWrapGrid Orientation="Horizontal"></ItemsWrapGrid>

</ItemsPanelTemplate>

</ListView.ItemsPanel>

</ListView> 
View Code

 

在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);

}

}
View Code

 

這就是我們的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);

}

} 
View Code

 

接下來實現我們的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);

}

 

}
View Code

 

在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應用更加完善。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM