Xamarin.Forms自定义CustomLayout时ChildViews属性取值异常问题
问题原因
构造函数中无法获取到ChildViews集合内容是XAML解析机制的默认行为,和属性是否为BindableProperty无关:
- 所有集合类型的XAML属性,解析器不会调用属性的
set访问器做整体赋值,只会先调用get访问器拿到属性初始化时创建的集合实例,再逐个将XAML中声明的子元素通过Add方法加入集合。 - 子元素加入集合的操作,会在控件构造函数执行完成后才触发,构造函数执行的时间点集合必然为空,这就是读不到子元素的核心原因。
正确实现方案
你需要实现自定义流布局的场景,直接继承Layout<View>比继承ContentView更合理,不需要自己额外实现子元素管理、布局触发的基础逻辑,具体实现代码如下:
// 标记ChildViews为XAML默认内容属性,书写XAML时可省略<CustomLayout.ChildViews>层级标签 [ContentProperty(nameof(ChildViews))] public class CustomLayout : Layout<View> { // 使用ObservableCollection监听集合元素增减事件 public ObservableCollection<View> ChildViews { get; } = new ObservableCollection<View>(); public CustomLayout() { // 不要在构造函数中读取ChildViews内容,此时子元素还未被加入集合 ChildViews.CollectionChanged += (s, e) => { // 子元素新增/删除时触发布局重算 InvalidateLayout(); // 这里可以写视图切换的相关逻辑,比如默认只展示第一个子视图 if (e.NewItems != null) { foreach (View child in e.NewItems) { // 新增子元素的初始化逻辑,比如默认隐藏、绑定上下文传递等 } } if (e.OldItems != null) { foreach (View child in e.OldItems) { // 子元素移除时的清理逻辑 } } }; } // 重写测量逻辑:计算所有子元素排布后的总尺寸 protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint) { double totalHeight = 0; double currentLineWidth = 0; double currentLineMaxHeight = 0; foreach (var child in ChildViews.Where(c => c.IsVisible)) { var childSize = child.Measure(widthConstraint, heightConstraint); // 当前行宽度不足时触发换行 if (currentLineWidth + childSize.Request.Width > widthConstraint) { totalHeight += currentLineMaxHeight; currentLineWidth = 0; currentLineMaxHeight = 0; } currentLineWidth += childSize.Request.Width; currentLineMaxHeight = Math.Max(currentLineMaxHeight, childSize.Request.Height); } // 加上最后一行的高度 totalHeight += currentLineMaxHeight; return new SizeRequest(new Size(widthConstraint, totalHeight)); } // 重写布局逻辑:逐个将子元素放到对应坐标位置 protected override void LayoutChildren(double x, double y, double width, double height) { double currentX = x; double currentY = y; double currentLineMaxHeight = 0; foreach (var child in ChildViews.Where(c => c.IsVisible)) { var childSize = child.Measure(width, height); if (currentX + childSize.Request.Width > x + width) { // 换行更新坐标 currentY += currentLineMaxHeight; currentX = x; currentLineMaxHeight = 0; } // 将子元素布局到计算好的位置 LayoutChildIntoBoundingRegion(child, new Rectangle(currentX, currentY, childSize.Request.Width, childSize.Request.Height)); currentX += childSize.Request.Width; currentLineMaxHeight = Math.Max(currentLineMaxHeight, childSize.Request.Height); } } }
对应的XAML调用可以简化为:
<controls:CustomLayout BackgroundColor="Pink" HorizontalOptions="FillAndExpand"> <Frame Height="50" Width="50" BackgroundColor="Yellow"/> <Frame Height="50" Width="50" BackgroundColor="Green"/> <Frame Height="50" Width="50" BackgroundColor="Red"/> <Frame Height="50" Width="50" BackgroundColor="Blue"/> </controls:CustomLayout>
实现提示
- 如果你需要做视图切换功能,不需要动态移除/加入子视图,只需要控制对应子视图的
IsVisible属性即可,性能更高,布局方法中已经自动过滤了不可见的子元素。 - 如果不想从零处理子元素间距、横竖屏适配、不同尺寸子元素兼容等边界问题,可以直接使用社区工具包中已经稳定的流布局控件,已经修复了官方FlexLayout的已知换行缺陷。
- 子元素自身的大小、可见性属性变化时,记得调用
InvalidateLayout()触发重排,否则布局不会自动更新。
内容的提问来源于stack exchange,提问作者Gaurav Katdare
相关产品推荐
相关产品推荐

