“第 26 章: 自定义布局

Download Sample下载示例

注意

本书于 2016 年春季出版,之后再未更新。 书中有许多内容仍然有价值,但有些内容已过时,有些主题不再完全正确或完整。

Xamarin.Forms 包含多个派生自 Layout<View> 的类:

  • StackLayout
  • Grid
  • AbsoluteLayout
  • RelativeLayout

本章节介绍了如何创建你自己的派生自 Layout<View> 的类。

布局概述

没有处理 Xamarin.Forms 布局的集中系统。 每个元素都负责确定自身尺寸,以及如何在特定区域中呈现自身。

父元素和子元素

每个有子元素的元素都负责在自身内部定位这些子元素。 最终由父元素根据可用尺寸和子元素所需尺寸来决定子元素的尺寸。

调整大小和定位

布局从页面的可视树的顶部开始,然后遍历所有分支。 布局中最重要的公共方法是由 VisualElement 定义的 Layout。 作为其他元素的父元素,每个元素都会对自己的所有子元素调用 Layout,以 Rectangle 值的形式为子元素指定相对于自身的尺寸和位置。 这些 Layout 调用通过可视化树传播。

若要让某元素出现在屏幕上,必须调用 Layout,这会设置以下只读属性。 它们与传递给方法的 Rectangle 一致:

  • Rectangle 类型的 Bounds
  • double 类型的 X
  • double 类型的 Y
  • double 类型的 Width
  • double 类型的 Height

Layout 调用前,HeightWidth 的 mock 值为 -1。

调用 Layout 还会触发对以下受保护方法的调用:

最后,以下事件触发:

OnSizeAllocated 方法由 PageLayout 替代,这两个类是 Xamarin.Forms 中唯一可以有子元素的类。 重写的方法调用

然后,LayoutChildren 对每个元素的子元素调用 Layout。 如果至少一个子元素有新的 Bounds 设置,则会触发以下事件:

约束和尺寸请求

为了能够智能地对自己的所有子元素调用 LayoutLayoutChildren 必须知道子元素的首选或所需尺寸。 因此,对每个子元素调用 Layout 之前,通常先调用

在本书出版后,GetSizeRequest 方法已遭弃用,并替换为

Measure 方法充分考虑 Margin 属性,并包含 MeasureFlag 类型的参数,它有两个成员:

对于许多元素,GetSizeRequestMeasure 从呈现器获取元素的本机尺寸。 这两种方法都有宽度和高度约束参数。 例如,Label 使用宽度约束来确定如何换行多行文本。

GetSizeRequestMeasure 都返回 SizeRequest 类型的值,它有两个属性:

这两个值通常是相同的,且一般可以忽略 Minimum 值。

VisualElement 还定义了类似于 GetSizeRequest 的受保护方法,它是从 GetSizeRequest 调用:

此方法现已遭弃用,并替换为:

每个派生自 LayoutLayout<T> 的类都必须重写 OnSizeRequestOnMeasure。 正是在这一方面,布局类确定自己的尺寸,这通常基于子元素的尺寸(通过对子元素调用 GetSizeRequestMeasure 来获取)。 在调用 OnSizeRequestOnMeasure 前后,GetSizeRequestMeasure 根据以下属性进行调整:

无限约束

传递给 GetSizeRequest(或 Measure)和 OnSizeRequest(或 OnMeasure)的约束参数可以是无限的(即值为 Double.PositiveInfinity)。 不过,从这些方法返回的 SizeRequest 不得包含无限尺寸。

无限约束表明,请求的尺寸应反映元素的自然尺寸。 垂直 StackLayout 对其子元素调用 GetSizeRequest(或 Measure),设有无限高度约束。 水平堆积布局对其子元素调用 GetSizeRequest(或 Measure),设有无限宽度约束。 AbsoluteLayout 对其子元素调用 GetSizeRequest(或 Measure),设有无限宽度和高度约束。

过程一窥

ExploreChildSize 显示简单布局的约束和尺寸请求信息。

派生自布局<视图>

自定义布局类派生自 Layout<View>。 它有两个责任:

  • OnMeasure 重写为对布局的所有子元素调用 Measure。 返回布局本身的请求尺寸
  • LayoutChildren 重写为对布局的所有子元素调用 Layout

这些重写中的 forforeach 循环应跳过 IsVisible 属性设置为 false 的任何子元素。

不保证调用 OnMeasure。 如果布局的父元素控制布局尺寸(例如,填充页面的布局),则不会调用 OnMeasure。 因此,LayoutChildren 无法依赖在 OnMeasure 调用期间获得的子元素尺寸。 通常情况下,LayoutChildren 本身必须对布局的子元素调用 Measure,你也可以实现某种尺寸缓存逻辑(稍后将进行介绍)。

简单示例

VerticalStackDemo 示例包含简化的 VerticalStack 类及其用法演示。

简化了垂直和水平定位

VerticalStack 必须执行的作业之一发生在 LayoutChildren 重写期间。 此方法使用子元素的 HorizontalOptions 属性,以确定如何在 VerticalStack 中的槽内定位子元素。 可以改为调用静态方法 Layout.LayoutChildIntoBoundingRect。 此方法对子元素调用 Measure,并使用它的 HorizontalOptionsVerticalOptions 属性在指定矩形中定位子元素。

失效

更改元素属性通常会影响此元素在布局中的显示方式。 必须让布局无效,才能触发新布局。

VisualElement 定义了受保护方法 InvalidateMeasure,它通常由更改会影响元素尺寸的任何可绑定属性的属性更改处理程序调用。 InvalidateMeasure 方法触发 MeasureInvalidated 事件。

Layout 类定义了类似的受保护方法,它名为 InvalidateLayout 且是 Layout 衍生物,应对任何影响子元素定位和尺寸的更改调用它。

编码布局的一些规则

  1. Layout<T> 衍生物定义的属性应由可绑定属性支持,且属性更改处理程序应调用 InvalidateLayout

  2. 定义附加的可绑定属性的 Layout<T> 衍生物应将 OnAdded 重写为将属性更改处理程序添加到子元素中,并将 OnRemoved 重写为删除此处理程序。 此处理程序应检查这些附加的可绑定属性是否有更改,并通过调用 InvalidateLayout 来响应。

  3. 实现子元素大小缓存的 Layout<T> 派生项应替代 InvalidateLayoutOnChildMeasureInvalidated,并在调用这些方法时清除缓存。

带属性的布局

Xamarin.FormsBook.Toolkit 中的 WrapLayout 类假设所有子元素都相同,并将子元素从一行(或列)换到下一行。 它定义了 Orientation 属性(像 StackLayout 一样),定义了 ColumnSpacingRowSpacing 属性(像 Grid 一样),并缓存了子元素尺寸。

PhotoWrap 示例将 WrapLayout 置于用于显示图库照片的 ScrollView 中。

不允许使用不受约束的尺寸!

Xamarin.FormsBook.Toolkit 库中的 UniformGridLayout 旨在在自身内部显示所有子元素。 因此,它无法处理不受约束的尺寸;如果遇到,就会抛出异常。

PhotoGrid 示例展示了 UniformGridLayout

Triple screenshot of Photo Grid

重叠子元素

Layout<T> 衍生物可能会与其子元素重叠。 不过,子元素按其在 Children 集合中的顺序呈现,而不是按其 Layout 方法的调用顺序。

Layout 类定义了两个方法,可便于在集合中移动子元素:

对于重叠子元素,从视觉上看,集合末尾的子元素会显示在集合开头的子元素之上。

Xamarin.FormsBook.Toolkit 库中的 OverlapLayout 类定义了附加属性来指明呈现顺序,这样它的一个子元素就能显示在其他子元素之上。 StudentCardFile 示例对此进行了展示:

Triple screenshot of Student Card File Grid

更多附加的可绑定属性

Xamarin.FormsBook.Toolkit 库中的 CartesianLayout 类定义了附加的可绑定属性,以指定两个 Point 值和粗细值,并控制 BoxView 元素,使其类似于线条。

UnitCube 示例使用此类绘制 3D 立方体。

Layout 和 LayoutTo

Layout<T> 衍生物可以调用 LayoutTo(而不是 Layout),以为布局添加动画效果。 AnimatedCartesianLayout 类可实现此目的,AnimatedUnitCube 示例对此进行了展示。