我的面板我做主 -- 淘宝UWP中自定义Panel的实现
在Windows10 UWP开发平台上内置的XMAL布局面板包括RelativePanel、StackPanel、Grid、VariableSizedWrapGrid 和 Canvas。在开发淘宝UWP应用时,遇到以下业务场景。
业务场景
场景一:淘宝商品提供的一些消费者保障服务
场景二:淘宝商品的SKU属性展示
实现分析
系统默认的面板容器控件显然不符合要求了。在WPF里面有WrapPanel,但是在UWP应用里面没有,这个时候就需要自定义个Panel了来实现WrapPanel的功能,实现起来不是很复杂。在MSDN的文档上已经给出了详细的实现说明:Xaml自定义面板,主要就是自定义一个Panel的派生类,然后重写(MeasureOverride 和 ArrangeOverride)方法。
以下是MSDN上对两个方法的解释说明
MeasureOverride方法
MeasureOverride 方法有返回值,当 Measure 方法在面板上受到布局中的父元素调用时,布局系统将使用该值作为面板自身的起始 DesiredSize。方法内的逻辑选择与它返回的内容同等重要,而且逻辑经常影响返回的值。
所有 MeasureOverride 实现应当循环访问 Children,并且对每个子元素调用 Measure 方法。调用 Measure 方法可为DesiredSize 属性创建值。这可能会通知面板本身需要多少空间,以及如何在元素间划分空间或为特定的子元素调整大小。
以下是 MeasureOverride 方法非常基本的框架:
protected override Size MeasureOverride(Size availableSize) { Size returnSize; //TODO might return availableSize, might do something else //loop through each Child, call Measure on each foreach (UIElement child in Children) { child.Measure(new Size()); // TODO determine how much space the panel allots for this child, that's what you pass to Measure Size childDesiredSize = child.DesiredSize; //TODO determine how the returned Size is influenced by each child's DesiredSize //TODO, logic if passed-in Size and net DesiredSize are different, does that matter? } return returnSize; }
ArrangeOverride方法
ArrangeOverride 方法有 Size 返回值,当 Arrange 在面板上受到布局中的父元素调用时,布局系统将在呈现面板本身时使用该值。通常输入 finalSize 和 ArrangeOverride 返回的 Size 相同。如果不相同,这意味着面板正尝试将自己调整为不同的大小,而不是布局中的其他参与者声明可用的大小。最终大小基于之前已通过面板代码运行布局的度量传递,这是通常不返回不同大小的原因:这意味着你在故意忽略度量逻辑。
不要返回具有 Infinity 组件的 Size。尝试使用这样的 Size 将从内部布局引发异常。
所有 ArrangeOverride 实现应当循环访问 Children,并且对每个子元素调用 Arrange 方法。和 Measure 一样,Arrange 没有返回值。与 Measure 不同,经计算的属性不会设置为结果(但是, 问题中的元素通常引发 LayoutUpdated 事件)。
以下是 ArrangeOverride 方法非常基本的框架:
protected override Size ArrangeOverride(Size finalSize) { //loop through each Child, call Arrange on each foreach (UIElement child in Children) { Point anchorPoint = new Point(); //TODO more logic for topleft corner placement in your panel // for this child, and based on finalSize or other internal state of your panel child.Arrange(new Rect(anchorPoint, child.DesiredSize)); //OR, set a different Size } return finalSize; //OR, return a different Size, but that's rare }
创建自定义Panel控件
下面用一个简单的demo演示一下,就知道这两个方法的作用了。
首先新建一个MyPanel类继承自Panel类,将Mypanel的背景色设置成灰色,在MyPanel里面放入6个Border控件,每个Border控件设置不同的背景颜色,固定Width和Height为100或者200,方便查看各个控件的大小区域。
UI xaml:
<Page x:Class="AppArrange.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:AppArrange" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <local:MyPanel Background="Gray" HorizontalAlignment="Left" VerticalAlignment="Top"> <Border Background="Red" Width="100" Height="100"> <TextBlock Text="1" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="32"/> </Border> <Border Background="Green" BorderThickness="1" Width="100" Height="200"> <TextBlock Text="2" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="32"/> </Border> <Border Background="Yellow" Width="200" Height="100"> <TextBlock Text="3" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="32"/> </Border> <Border Background="OrangeRed" Width="100" Height="100"> <TextBlock Text="4" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="32"/> </Border> <Border Background="Orange" Width="100" Height="100"> <TextBlock Text="5" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="32"/> </Border> <Border Background="Orchid" Width="100" Height="100"> <TextBlock Text="6" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="32"/> </Border> </local:MyPanel> </Grid> </Page>
Code behind:
public class MyPanel : Panel { protected override Size MeasureOverride(Size availableSize) { return base.MeasureOverride(availableSize); } protected override Size ArrangeOverride(Size finalSize) { return base.ArrangeOverride(finalSize); } }
这个时候MeasureOverride和ArrangeOverride什么都没有做,如果直接运行会是什么样子呢?
这个时候界面就是一片空白。添加的6个Border控件没有显示。Mypanel的灰色背景也没有显示,说明Mypanel的size是0,没有显示出来。
下面来实现MeasureOverride方法,遍历每个子控件并调用Measure方法。
public class MyPanel : Panel { protected override Size MeasureOverride(Size availableSize) { foreach (FrameworkElement child in Children) { child.Measure(availableSize); } return availableSize; } protected override Size ArrangeOverride(Size finalSize) { return base.ArrangeOverride(finalSize); } }
这个时候Mypanel的面板显示出来了,背景颜色是灰色,但是子控件没有显示。
接下来,实现ArrangeOverride方法
protected override Size ArrangeOverride(Size finalSize) { double x = 0; double y = 0; foreach (FrameworkElement child in Children) { child.Arrange(new Rect(new Point(x, y), child.DesiredSize)); x += child.DesiredSize.Width; y += child.DesiredSize.Height; } return finalSize; }
运行结果:
子控件出来了,超出Page范围的会被遮住。这个时候就可以根据需要定义每个子控件的(x,y)坐标进行布局了。如果要横向布局,就递增x坐标,如果纵向布局就递增y坐标。
横向布局
递增x坐标
protected override Size ArrangeOverride(Size finalSize) { double x = 0; double y = 0; foreach (FrameworkElement child in Children) { child.Arrange(new Rect(new Point(x, y), child.DesiredSize)); x += child.DesiredSize.Width; // y += child.DesiredSize.Height; } return finalSize; }
运行结果
纵向布局
递增y坐标
protected override Size ArrangeOverride(Size finalSize) { double x = 0; double y = 0; foreach (FrameworkElement child in Children) { child.Arrange(new Rect(new Point(x, y), child.DesiredSize)); //x += child.DesiredSize.Width; y += child.DesiredSize.Height; } return finalSize; }
运行结果:
接下来的问题,就是横向或者纵向布局的时候,判断何时该换行了。要换行就要计算依次排列的子控件的宽和高,同时和Mypanel的大小进行比较是否超出边界。
这其中还有个问题是MeasureOverride和 ArrangeOverride 都会返回一个size大小。这两个大小有什么不一样吗。
MeasureOverride 返回值:此对象在布局过程中基于其对子对象分配大小的计算或者基于固定容器大小等其他因素而确定的它所需的大小。
ArrangeOverride 返回值:元素在布局中排列后使用的实际大小。
MeasureOverride的输入size和返回size
可以做几个实验看看实际效果:
1. 如果给Measure传递的值较小,例如比最小的子控件还小:
protected override Size MeasureOverride(Size availableSize) { foreach (FrameworkElement child in Children) { child.Measure(new Size(50,50)); } return availableSize; }
运行结果
子控件会被裁剪部分区域,显示不完整,以适应较小的size。
2. 如果给Measure传递的值较大,比子控件的大小要大。
protected override Size MeasureOverride(Size availableSize) { foreach (FrameworkElement child in Children) { child.Measure(new Size(400,400)); } return availableSize; }
运行结果
结果显示还是正常的大小,没有变化。
总结:
说明子控件的DesiredSize的会受到Measure传递的大小的限制,过小就会被裁剪,过大,不受影响,以实际的DesiredSize显示。Measure方法就是给控件分配一个可以显示的大小范围。
下面看看MeasureOverride返回值,实际上和Measure的作用是一样的,给MeasureOverride方法的参数size是MyPanel的父控件给MyPanel分配的显示区域大小,实际上会受到MyPanel 的Width、Height、HorizontalAlignment、VerticalAlignment等设置的影响,这里就不展开了。
如果子控件的大小显示区域超过了MyPanel的父控件给MyPanel分配的显示区域大小,子控件的显示区域会被裁剪。这个时候可以根据业务需要调整MeasureOverride 返回size或者调整每个子控件的Measure输入size,缩小子控件,使每个子控件都完整显示出来。
下面看看如果设置的MeasureOverride返回值过小是什么效果
protected override Size MeasureOverride(Size availableSize) { foreach (FrameworkElement child in Children) { child.Measure(availableSize); } return new Size(150,150); }
运行结果:
灰色区域是容器的大小,各个子控件已经超出容器控件的大小范围了,MyPanel的父控件分配的大小是整个page的大小,在遍历子控件Border时分配给子控件也是page的大小显示区域,但是每个子控件都设置了Width和Height,而child.Measure(availableSize)的大小比子控件自己的size大,所以最后会显示控件实际的大小,不会受child.Measure(availableSize)的影响,但是MyPanel的MeasureOverride最后返回的时候,size被修改小了,这样整个容器就显示被修改后的大小,子控件溢出边界。
所以在自定义容器控件的时候,MeasureOverride 方法返回的size应该是所有子控件显示区域的最小size。而计算显示区域的最小size,应该根据子控件的布局方式来判断。
ArrangeOverride的输入size和返回size
需要注意的是:通常情况下ArrangeOverride输入的size和返回的size一样,如果返回的size过小,也会遇到和MeasureOverride返回的size过小一样的显示问题。所以不建议修改ArrangeOverride的返回size,直接将输入size返回就可以了,ArrangeOverride方法主要作用是给子控件定位坐标和大小,完成布局。
有了以上的准备,应该就知道怎么实现淘宝的业务了,主要是在水平方向上依次排列子控件,然后自动换行。
首先在MeasureOverride里面计算子控件需要显示大小区域,在ArrangeOverride里面根据子控件的大小排列方式计算显示坐标,实现自动换行。
protected override Size ArrangeOverride(Size finalSize) { double x = 0; double y = 0; double maxHeight = 0; foreach (FrameworkElement child in Children) { if (maxHeight < child.DesiredSize.Height) { maxHeight = child.DesiredSize.Height; } if ((x + child.DesiredSize.Width) > finalSize.Width) { x = 0; y += maxHeight; maxHeight = 0; } child.Arrange(new Rect(new Point(x, y), child.DesiredSize)); x += child.DesiredSize.Width; } return finalSize; }
运行结果:
如果要实现更复杂的功能,例如要同时支持可以横向纵向排列子控件,就需要做一些封装了,这里就不一一展开了,最后附上有完整功能的WrapPanel实现代码供大家参考。可以实现横向和纵向排列。