2018-05-20 07:11
Avalonia 是一款尚在开发中的基于 .NET Core 的跨平台 UI 框架。目前用在个人项目中还是不错的,不过还需要大家在开源社区中多多支持。
我为它写了一个全新的 Grid
布局算法,此算法是 WPF 在通常情况下的性能的两倍。本文将分享我在此项目中实现的算法的原理。
Grid 算是 WPF/UWP 入门中非常重要的一个布局容器了。面对它那强大而熟悉的布局方式,大家应该没有什么疑问吧!
比如:
Auto
, *
和数值 Auto
表示 Grid
将按照元素的实际所需尺寸进行布局*
表示行列在布局中的比例,*
前面的数值表示比例值基本上大家所熟知的 Grid
布局差不多就这样么多了。
如果想了解 WPF/UWP 的布局单位,可以阅读我之前的一篇文字将 UWP 的有效像素(Effective Pixels)引入 WPF - 吕毅。
然而,事实上 Grid
的布局行为才没有那么简单呢!它诡异的地方在于没有定义好多种复杂布局情况下的交叉行为。我写了中英两篇文章来说明了这些不太符合预期的行为:
作为一个非常有潜力的 .NET Core 跨平台 UI 框架 Avalonia,应该认真定义好这些行为,而不是像 WPF/UWP 现有的 Grid
那样在某些情况下比较含糊,出现难以解释的布局行为。
如果你熟知 WPF/UWP 的布局系统,那么 MeasureOverride
和 ArrangeOverride
一定不陌生,虽然它们只是布局的一部分(为什么是一部分?详见 Visual->UIElement->FrameworkElement,带来更多功能的同时也带来了更多的限制 - 吕毅)。
不过,写一个 Grid
确实只需要关心这两个函数就够了。MeasureOverride
传入父级测量的可用尺寸,返回此 Grid
测量发现所需的最小尺寸;ArrangeOverride
传入父级实际可提供的可用尺寸,返回此 Grid
实际布局所用的尺寸。
如果行或列设置为 Auto
,那么 Grid
的行或者列将为这个元素的尺寸进行适配,并且元素的所需尺寸也会影响到 Grid
的最小所需尺寸;如果行或列设置为 *
,那么 Grid
的行列不会为此元素适配,但是元素的所需尺寸依然会影响到 Grid
的最小所需尺寸。
由于我们必须要计算 Grid
的最小所需尺寸,所以整个布局过程中,必须得到每个行列的最小所需尺寸。这意味着,即便我们不能确定此行或此列的尺寸,或者甚至在父级尺寸确定的情况下能够确定此行或此列时,也应该计算最小尺寸。而 Auto
、元素的 DesiredSize
、*
或者行列的最小值都会影响到此最小尺寸,所以这些都应该先考虑。而行或列的最大值应该在最后再考虑。
于是,我们将整个布局过程分成以下几步:
Auto
或 *
的元素(前者影响行列和最小尺寸,后者仅影响最小尺寸)*
展开后超过此最小尺寸的行列按最小值确定Auto
行列确定*
的尺寸Grid
所需的最小尺寸Grid
的布局算法似乎难以用语言描述,不过,我可以尝试用更具体的文字用接近代码的方式来描述:
*
的尺寸 *
行列的最小尺寸我在 Avalonia 的代码注释中,画出了每一个步骤的变化图。
// 1. 测量行列范围中包含 `Auto` 或 `*` 的元素(前者影响行列和最小尺寸,后者仅影响最小尺寸)
//
// 2. 将所有的已确定尺寸确定
// +-----------------------------------------------------------+
// | * | A | * | P | A | * | P | * | * |
// +-----------------------------------------------------------+
// |#fix#| |#fix#|
//
// 3. 将所有的有最小尺寸,且 `*` 展开后超过此最小尺寸的行列按最小值确定
// +-----------------------------------------------------------+
// | * | A | * | P | A | * | P | * | * |
// +-----------------------------------------------------------+
// | min | max | | | min | | min max | max |
// | fix | |#fix#| fix |
//
// 4. 将所有 `Auto` 行列确定
// +-----------------------------------------------------------+
// | * | A | * | P | A | * | P | * | * |
// +-----------------------------------------------------------+
// | min | max | | | min | | min max | max |
// |#fix#| | fix |#fix#| fix | fix |
//
// 5. 按照父级尺寸估算 `*` 的尺寸
// +-----------------------------------------------------------+
// | * | A | * | P | A | * | P | * | * |
// +-----------------------------------------------------------+
// | min | max | | | min | | min max | max |
// |#des#| fix |#des#| fix | fix | fix | fix | #des# |#des#|
//
// 6. 计算 `Grid` 所需的最小尺寸
//
// 7. 将估算缩得的尺寸作为实际尺寸进行测量
// +-----------------------------------------------------------+
// | * | A | * | P | A | * | P | * | * |
// +-----------------------------------------------------------+
// | min | max | | | min | | min max | max |
// |#fix#| fix |#fix#| fix | fix | fix | fix | #fix# |#fix#|
为了让代码更容易调试,我专门写了一个 GridLayout
类来完成布局过程,而且 GridLayout
的计算设计为与 Grid
布局过程无关。做法是,将 GridLayout
的大部分方法设计为“纯方法”(纯方法只随便调用,调用此方法不会改变任何系统状态,只有拿到其返回值才会真正发挥作用)。
具体的代码非常长,含单元测试供 1200+ 行,建议去 Avalonia 仓库查看:
在性能测试中,此算法还是表现不错的,以下是 Pull Request 中的性能测试截图(已经合并)。
本文会经常更新,请阅读原文: https://walterlv.com/post/grid-layout-algorithm.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名 吕毅 (包含链接: https://walterlv.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 (walter.lv@qq.com) 。