在淘宝UWP中,搜索结果列表是用户了解宝贝的重要一环,其中的图片效果对吸引用户点击搜索结果,查看宝贝详情有比较大的影响。为此手机淘宝特地在搜索结果列表上采用了2种表现方式:一种就是普通的列表模式,而另外一种则是突出宝贝图片的瀑布流模式。html
若是用户搜索某些关键字,如女装类的状况下,淘宝的搜索结果会自动切换到瀑布流模式,让宝贝的美图更加冲击用户的视觉。ide
可是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有什么关系呢?spa
实际上它们都是继承自panel的FrameworkElement,也就是说它们均可以对内部的子元素进行布局。无论listview仍是gridview,他们列表的形式都是由itemsPanel决定的,listview只有1列,能够纵向或者横向扩展,是由它使用的itemsPanel- ItemsStackPanel肯定的,gridview能够有多列,能够纵向或者横向扩展,也是由它使用了ItemsWrapGrid做为itemsPanel来决定的。那么若是咱们根据淘宝UWP--自定义Panel中提到的方法,自定义一个panel,就能够实现瀑布流中形式的列表了。code
肯定了要实现一个瀑布流的布局panel,咱们接下来考虑一下咱们的具体有哪些需求呢?在淘宝的搜索结果瀑布流中,只用了2列。可是考虑到咱们的淘宝UWP可能运行在PC或者平板等横向屏幕的设备上,若是也用2列的话会有不少图只能在屏幕中显示一部分。因此在PC或者平板等横向屏幕的设备上,咱们要让瀑布流的列数增长,也就是说咱们的panel须要能自定义列数。htm
在淘宝的搜索结果瀑布流中,宝贝的搜索结果是纵向扩展的,那么有没有可能有状况须要使用横向扩展的瀑布流呢?想一想彷佛是比较酷的,那么就为咱们的panel加上扩展方向的选择吧。blog
在肯定了具体需求以后就能够开始着手实现咱们的自定义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应用更加完善。