Xamarin.Forms中提供了StackLayout、AbsoluteLayout、RelativeLayout、Grid
四个布局类,借助这四个类可以方便的管理程序的布局。当现有布局类不能满足我们的功能时,可以通过自定义布局实现想要的效果。 在讲解布局时提到过StackLayout等继承Layout<View>类,可以管理多个子视图。如果继承Layout类,只能管理单一子视图,如ContentView、Frame、ScrollView等。
布局概述
布局类是如何管理子视图的(确定子视图的大小和位置)?
先看一个简单示例,HorizontalOptions和VerticalOptions属性默认值为LayoutOptions.Fill。
显示效果,从红色外边框看出Frame大小为Page大小:
对Frame属性做如下修改:
对应显示效果
HorizontalOptions="Center" VerticalOptions="Center"
属性用来确定子视图大小,Frame请求大小为内部Label大小,再通过指定WidthRequest="1000"
明确Frame的宽度。从效果图看出布局分配给子视图的大小由子视图请求决定但又不能超出布局大小。
布局(父元素)如何才能知道子元素的大小?子元素应提供一个公共方法由父元素调用,这个方法为Layout
,Layout方法定义在VisualElement
中。
Layout方法接收一个Rectangle类型参数bounds。
查看Bounds属性定义
Bounds属性的Set操作声明为private,且通过Set内代码看出Bounds的值即元素的显示位置(X,Y)和大小(Width,Height)。
一个页面显示时首先会调用Page对象的Layout方法,Layout方法内在通过视图元素树(visual tree)依次调用子元素的Layout方法,这个过程称为布局的生命周期。旋转手机时会重新开始这个布局周期,这个布局周期也可以发生在元素树的一个子集内,如添加或移除子视图等操作都会调用父元素的Layout方法。
视图的X,Y,Width和Height等属性都是在布局的生命周期执行后被赋予一个有效值,视图的构造函数内无法获取有效值。X、Y的默认值为0,Width和Height的默认值为-1。
执行Layout方法会一次调用SetSize->SizeAllocated方法。SizeAllocated方法接收的width和htight参数为Bounds属性的Width和Heiht值,SizeAllocated方法内部又调用一个虚方法OnSizeAllocated
。OnSizeAllocated执行完,此时视图的大小已经发生变化,同时出发SizeChanged事件。
OnSizeAllocated方法用virtual关键字声明,但是只允许Page和Layout两个类重写该方法进行特殊处理。
As an alternative to the SizeChanged event, it is possible for an application to override OnSizeAllocated in a ContentPage derivative to obtain the new size of the page. (If you do so, be sure to call the base class implementation of OnSizeAllocated.) You’ll find that OnSizeAllocated is sometimes called when the element’s size doesn’t actually change. The SizeChanged event is fired only when the size changes, and it’s better for size-specific handling on the application level.
Page类内OnSizeAllocated方法实现(Layout内相同),调用UpdateChildrenLayout:
查看UpdateChildrenLayout方法定义,UpdateChildrenLayout方法中调用LayoutChildren方法,并在方法结束前触发LayoutChanged
事件。
LayoutChildren方法在Page和Layout中有了不同的处理,Page类中LayoutChildren方法定义为virtual方法,Layout类中LayoutChildren定义为abstract方法。
Page类中LayoutChildren的定义:
Layout类中LayoutChildren的定义:
LayoutChildren是一个很重要的方法。查看Page的LayoutChildren方法代码实现循环所有子元素进入LayoutChildIntoBoundingRegion
方法。
LayoutChildIntoBoundingRegion是一个静态方法,方法内部判断当前元素是否为视图,传入元素不是视图(即布局)则调用子布局的Layout方法,此时再次回到Layout方法,从而完成布局生命周期的调用过程。当我们继承Layout<View>编写自定义布局时应实现该方法以管理子视图。
Layout传值到LayoutChildren时应考虑Padding属性值。如下布局所示,在360*640的设备上,Page的Layout方法接收到的X,Y,Width,Height为(0, 0, 360, 640),此时没有设置Padding 值传到LayoutChildren方法接收的值同样是(0, 0, 360, 640),LayoutChildren方法内调用ContentView的Layout方法,所以ContentView的Layout方法接收到Bound的X,Y,Width和Height值不变(0, 0, 360, 640)。在ContentView的Layout方法传值到LayoutChildren方法此时Padding值为15,所以Width和Height减少30(上下左右各减少15)并且X和Y的值增加15。LayoutChildren接收到的值为(15,15,330,610),LayoutChildren方法内调用Label的Layout,此时接收到的Bounds属性对应的X,Y,Width和Height分别为(15,15,330,610)。
在Page的LayoutChildIntoBoundingRegion方法中,传入child是一个View则调用子视图的Measure
(过时方法为GetSizeRequest)方法计算子视图需要的大小,(此时child的Bounds属性或Width和Height属性还没有赋予有效值)。
调用子元素的Layout方法前会调用Measure方法,Layout方法的参数取决Measure返回的结果。
widthConstraint 和 heightConstraint表示宽高的约束值,表示父元素可以提供多少可用空间用来布局子元素。Measure方法内计算出所有子元素所需的大小与widthConstraint和heightConstraint比较,取较小值创建SizeRequest类型实例返回,SizeRequest包含Minimum和Request两个Size类型属性。Request表示元素需要的大小,Minimum表示元素的最小大小。
通常Minimum和Request两个属性赋予相同值,Xamarin.Forms中ListView和TabeView两个元素对应的Minimum和Request值不同,Minimum的值为(40,40)。然而Minimum的值并没什么用。
Measure内部调用已经过时的GetSizeRequest方法。
GetSizeRequest会调用OnMeasure方法,OnMeasure定义为虚方法。OnMeasure方法也是我们自定义布局时必须要重写的方法。
布局重绘
当元素的某些属性改变时会影响元素的大小,此时会引起元素的重绘。VisualElement定义一个InvalidateMeasure
方法重绘元素,InvalidateMeasure是一个受保护的方法。自定义View时有某个BindableProperty类型属性可能改变元素大小,应该在该属性的propertyChanged内调用InvalidateMeasure方法。
AnchorX, AnchorY, Rotation, RotationX, RotationY, Scale, TranslationX, TranslationY等属性影响的是元素的渲染大小并不能改变元素的布局大小,值的改变不会触发MeasureInvalidated事件。Bounds, X, Y, Width,Height等属性在Layout方法内被调用,这几个属性改变时同样不会调用InvalidateMeasure方法。
InvalidateMeasure方法会触发MeasureInvalidated
事件,以通知外部元素布局发生改变,通常是父元素处理MeasureInvalidated事件。
Layout类提供了一个类似的方法InvalidateLayout
,当布局内属性发生改变影响子元素的大小和位置时应调用InvalidateLayout方法,如Layout<T>中Children属性的added和removed操作会调用InvalidateLayout方法。
如果想阻止增加或移除子元素时InvalidateLayout方法的调用可以重写ShouldInvalidateOnChildAdded和ShouldInvalidateOnChildRemoved方法,返回false即可。代码片段截图看出,还将OnChildMeasureInvalidated
方法作为参数实例化EventHandler监听view的MeasureInvalidated事件,实际编码时可以重写OnChildMeasureInvalidated方法接收MeasureInvalidated触发的通知。
Layout<T>类定义了OnAdded和OnRemoved两个虚方法,分别在重写Element类的OnChildAdded和OnChildRemoved方法时调用,自定义布局时可以根据自己需要重写这两个方法。
Grid类重写OnAdded方法,添加子元素PropertyChanged监听示例(重写OnRemoved卸载监听,附加属性改变时调用InvalidateLayout方法,有兴趣可查看OnItemPropertyChanged内部实现):
当我们必须重新绘制布局时,该调用InvalidateLayout还是InvalidateMeasure?调用InvalidateLayout重新开始一个布局周期!!!InvalidateMeasure方法适合改变了布局的大小且不影响子布局排列时调用。
In most cases, the layout should call InvalidateLayout. This guarantees that the layout gets a call to its LayoutChildren method even if the layout is fully constrained in size. If the layout calls InvalidateMeasure, then a new layout pass will be generated only if the layout is not fully constrained in size. If the layout is constrained in size, then a call to InvalidateMeasure will do nothing.
自定义布局示例
通过继承Layout<T>实现瀑布流布局(只是简单示例)。如果想限制布局内子元素的类型可以T来指定,如Layout<Label>。但更多时候是继承Layout<View>。
重写LayoutChildren和OnMeasure方法,方法内循环所有子元素(忽略IsVisible属性为false的子元素)。LayoutChildren方法内调用Xamarin.Forms提供的LayoutChildIntoBoundingRegion(代替子元素的Layout 方法)方法简化编码,不必考虑LayoutOptions的值。
public class FlowLayout : Layout<View>
{
public static readonly BindableProperty ColumnSpacingProperty =
BindableProperty.Create(
"ColumnSpacing",
typeof(double),
typeof(FlowLayout),
6.0,
propertyChanged: (bindable, oldvalue, newvalue) =>
{
((FlowLayout)bindable).InvalidateLayout();
});
public static readonly BindableProperty RowSpacingProperty =
BindableProperty.Create(
"RowSpacing",
typeof(double),
typeof(FlowLayout),
6.0,
propertyChanged: (bindable, oldvalue, newvalue) =>
{
((FlowLayout)bindable).InvalidateLayout();
});
public double ColumnSpacing
{
set { SetValue(ColumnSpacingProperty, value); }
get { return (double)GetValue(ColumnSpacingProperty); }
}
public double RowSpacing
{
set { SetValue(RowSpacingProperty, value); }
get { return (double)GetValue(RowSpacingProperty); }
}
protected override void LayoutChildren(double x, double y, double width, double height)
{
double xChild = 0, yChild = 0;
//循环子视图
foreach (View child in Children)
{
if (!child.IsVisible)
{
continue;
}
SizeRequest childSizeRequest = child.Measure(width, height);
// Initialize child position and size.
var childWidth = childSizeRequest.Request.Width;
var childHeight = childSizeRequest.Request.Height;
if (xChild + childWidth > width)
{
xChild = 0;
yChild += childHeight + RowSpacing;
}
//判断HorizontalOptions和VerticalOptions的值计算子视图 x,y,width,height的值
//switch (child.HorizontalOptions.Alignment)
//{
// case LayoutAlignment.Start:
// break;
// case LayoutAlignment.Center:
// //xChild += (width - childWidth) / 2;
// break;
// case LayoutAlignment.End:
// //xChild += (width - childWidth);
// break;
// case LayoutAlignment.Fill:
// //childWidth = width;
// break;
//}
// Layout the child.
LayoutChildIntoBoundingRegion(child, new Rectangle(xChild, yChild, width, childHeight));
xChild = xChild + childWidth + ColumnSpacing;
}
}
/// <summary>
/// 计算出显示子元素所需大小
/// </summary>
/// <returns>The measure.</returns>
/// <param name="widthConstraint">Width constraint.</param>
/// <param name="heightConstraint">Height constraint.</param>
protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint)
{
var size = new Size();
var width = 0.0;
//循环子视图
foreach (View child in Children)
{
if (!child.IsVisible)
{
continue;
}
// Get the child's requested size.
SizeRequest childSizeRequest = child.Measure(widthConstraint, Double.PositiveInfinity);
// Initialize child position and size.
var childWidth = childSizeRequest.Request.Width;
var childHeight = childSizeRequest.Request.Height;
if (width + childWidth >= widthConstraint)
{
size.Height = size.Height + childHeight + RowSpacing;
}
else
{
width = width + childWidth + ColumnSpacing;
size.Width = Math.Max(size.Width, width);
}
}
//widthConstraint或heightConstraint的值可能为Double.PositiveInfinity,
//但是OnMeasure不能返回Double.PositiveInfinity所以不能有如下代码
//return new SizeRequest(new Size(widthConstraint, heightConstraint));
return new SizeRequest(size);
}
#region 测试时编写的无关代码
protected override bool ShouldInvalidateOnChildAdded(View child)
{
return false;
}
protected override bool ShouldInvalidateOnChildRemoved(View child)
{
return base.ShouldInvalidateOnChildRemoved(child);
}
protected override void OnAdded(View view)
{
base.OnAdded(view);
//根据自己需要编码
}
protected override void OnRemoved(View view)
{
base.OnRemoved(view);
//根据自己需要编码
}
protected override void OnChildMeasureInvalidated()
{
base.OnChildMeasureInvalidated();
}
#endregion
}
XAML使用示例:
运行效果:
更多自定义布局介绍情查看《Creating Mobile Apps with Xamarin.Forms》