尺寸和定位
布局过程从可视树的顶部开始,然后通过可视树的所有分支继续进行,以包含页面上的每个可视元素。 作为其他元素的父母的元素负责相对于他们自己的孩子的大小调整和定位。 这要求父元素调用子元素中的某些公共方法。 这些公共方法通常会导致调用每个元素中的其他方法,要设置的属性以及要触发的事件。
也许布局中涉及的最重要的公共方法被命名(非常恰当)布局。 此方法由VisualElement定义,并由派生自VisualElement的每个类继承:
public void Layout(Rectangle bounds)
Layout方法指定元素的两个特征:
- 呈现元素的矩形区域(由Rectangle值的Width和Height属性指示); 和
- 元素左上角相对于其父左上角的位置(X和Y属性)。
当应用程序启动并且需要显示第一页时,第一个布局调用是指向Page对象,“宽度”和“高度”属性指示屏幕的大小或页面占用的屏幕区域。从第一个Layout调用开始,Layout调用通过可视树有效传播:作为其他元素(Page,Layout和Layout 衍生物)的父元素的每个元素负责在其子元素上调用Layout方法,导致页面上的每个可视元素都调用其Layout方法。 (你会很快看到它是如何工作的。)
整个过程称为布局循环,如果您将手机侧向转动,则布局周期
使用Page对象从可视树的顶部开始。如果某些更改会影响布局,则布局周期也可能发生在可视树的子集上。这些更改包括从集合中添加或删除的项目(例如ListView或StackLayout或其他Layout类中的项目,元素的IsVisible属性的更改或元素大小的更改(出于某种原因) )。
在VisualElement内部,Layout方法导致设置元素的五个属性。这些属性都由VisualElement定义:
- Bounds类型是 Rectangle
- X类型是 double
- Y类型是double
- Width类型是 double
- Height类型是 double
这些属性都是同步的。 VisualElement的X,Y,Width和Height属性始终与Bounds矩形的X,Y,Width和Height属性值相同。这些属性指示元素的实际渲染大小及其相对于其父级左上角的位置。
这五个属性都没有公共集访问器。对于外部代码,这些属性是getonly。
在元素的第一个布局调用之前,X和Y属性的值为0,但Width和Height属性的“mock”值为-1,表示尚未设置属性。只有在布局周期发生后,这些属性的有效值才可用。在执行构成可视树的元素的构造函数期间,有效值不可用。
X,Y,Width和Height属性都由可绑定属性支持,因此它们可以是数据绑定的源。 Bounds属性不受可绑定属性的支持,也不会触发PropertyChanged事件。不要将Bounds用作数据绑定源。
对Layout的调用也会触发对SizeAllocated方法的调用,该方法由VisualElement定义,如下所示:
protected void SizeAllocated(double width, double height)
这两个参数与Bounds矩形的Width和Height属性相同。 SizeAllocated方法调用受保护的虚方法名称OnSizeAllocated:
protected virtual void OnSizeAllocated(double width, double height)
在OnSizeAllocated方法返回并且大小已从其先前值更改后,VisualElement将触发SizeChanged事件,其定义如下:
public event EventHandler SizeChanged;
这表示元素的大小已设置或随后已更改。正如您在前面的章节中所看到的,当您需要实现一些特定于大小的处理时,SizeChanged事件是访问Bounds属性或Width和Height属性以获取页面或任何元素的有效大小的绝佳机会。这页纸。通过触发SizeChanged事件完成对Layout方法的调用。
作为SizeChanged事件的替代方法,应用程序可以覆盖ContentPage派生中的OnSizeAllocated以获取页面的新大小。 (如果这样做,请务必调用OnSizeAllocated的基类实现。)您会发现,当元素的大小实际上没有变化时,有时会调用OnSizeAllocated。 SizeChanged事件仅在大小更改时触发,并且对于应用程序级别的特定于大小的处理更好。
OnSizeAllocated方法未定义为虚拟,因此应用程序可以覆盖它,但允许Xamarin.Forms中的类覆盖它。只有两个类重写OnSizeAllocated来执行自己的专门处理,但它们是非常重要的类:
- Page
- Layout
这些是所有Xamarin.Forms元素的基类,它们充当Xamarin.Forms可视树中其他元素的父级。 (尽管ListView和TableView似乎也有子节点,但这些子节点的布局是在这些视图的平台实现中处理的。)
从Page和Layout派生的一些类具有View类型的Content属性。 这些类是ContentPage,ContentView,Frame和ScrollView。 Content属性是单个子项。 从Page(MasterDetailPage,TabbedPage和CarouselPage)派生的其他类有多个子节点。 从Layout 派生的类具有IList 类型的Children proprty; 这些类是StackLayout,AbsoluteLayout,RelativeLayout和Grid。
Page和Layout类具有并行结构,以OverSizeAllocated方法的覆盖开始。 这两个类都定义了从OnSizeAllocated覆盖调用的以下方法:
protected void UpdateChildrenLayout()
两个版本的UpdateChildrenLayout都调用名为LayoutChildren的方法。 在Page和Layout中,此方法的定义略有不同。 在Page中,LayoutChildren方法定义为virtual:
protected virtual void LayoutChildren(double x, double y, double width, double height)
在Layout中,它被定义为抽象:
protected abstract void LayoutChildren(double x, double y, double width, double height);
每个具有Content或Children属性的Xamarin.Forms类都具有可覆盖的LayoutChildren方法。当您编写自己的类派生自Layout (这是本章的主要目标)时,您将覆盖LayoutChildren以提供布局“子”的自定义组织。
LayoutChildren覆盖的责任是在所有元素的子元素上调用Layout方法,这些元素通常是设置为元素的View对象的Content属性或元素中的View对象的子集合。这是布局中最重要的部分。
正如您所记得的,对Layout方法的调用会导致设置Bounds,X,Y,Width和Height属性,并调用SizeAllocated和OnSizeAllocated。如果元素是Layout de rivative,则OnSizeAllocated调用UpdateChildrenLayout和LayoutChildren。然后LayoutChildren在其子项上调用Layout。这就是布局调用从可视树的顶部传播到页面上的所有分支和每个元素的方式。
Page和Layout都定义了LayoutChanged事件:
public event EventHandler LayoutChang
UpdateChildrenLayout方法通过触发此事件来结束,但前提是至少有一个子节点具有新的Bounds属性。
您已经看到Page和Layout类都覆盖了OnSizeAllocated方法,并且都定义了UpdateChildrenLayout和LayoutChildren方法以及LayoutChanged事件。 Page和Layout类还有另一个相似之处:它们都定义了Padding属性。 此填充自动反映在LayoutChildren的参数中。
例如,请考虑以下页面定义:
<ContentPage __ Padding="20">
<ContentView Padding="15">
<Label Text="Sample text" />
</ContentView>
</ContentPage>
假设纵向模式下的屏幕测量360乘640.OconmentsPage调用其Layout方法,边界矩形等于(0,0,360,640)。 这开始了布局周期。
虽然ContentPage中的Layout方法的参数为(0,0,360,640),但该页面中的LayoutChildren调用的Padding属性调整为20.宽度和高度均减少40(每边20个) 并且x和y参数增加20,因此LayoutChildren参数为(20,20,320,600)。 这是相对于页面的矩形,其中Con tentPage可以定位其子项。
ContentPage中的LayoutChildren方法调用其子窗体(ContentView)中的Layout方法,为ContentView提供页面可用的整个空间减去页面上的填充。 此Layout调用的bounds矩形参数是(20,20,320,600),它将ContentView 20单元的左上角定位在ContentPage的左上角的右下方。
在ContentView中对LayoutChildren覆盖的调用反映了该布局区域,但是由Padding设置15减少,因此ContentView中LayoutChildren覆盖的参数为(15,15,290,570)。 此LayoutChildren方法使用该值调用Label中的Layout方法。
现在让我们做一点改变:
<ContentPage __ Padding="20">
<ContentView Padding="15"
VerticalOptions="Center">
<Label Text="Sample text" />
</ContentView>
</ContentPage>
ContentPage中的LayoutChildren覆盖现在需要做一些不同的事情。 它不能简单地在ContentView上使用自己的大小减去填充来调用Layout。 它必须调用ContentView中的Layout方法,使ContentView在其可用空间内垂直居中。
但是怎么样? 要使ContentView相对于自身垂直居中,ContentPage必须知道ContentView的高度。 但ContentView的高度取决于Label的高度,并且该高度取决于文本,也可能取决于可能在Label上设置的各种字体属性。 此外,Label能够将文本包装到多行,并且Label无法知道它需要多少行而不知道可用的水平空间。
此问题意味着涉及更多步骤。