Xamarin.Forms 用户界面——控件——布局——创建自定义布局

创建自定义布局

PDF用于离线使用
  • 下载PDF
示例代码:
  • WrapLayout
相关文章:
  • 自定义布局
相关API:
  • 布局
  • 布局
  • VisualElement
有关的影片:
  • 在Xamarin.Forms中创建自定义布局

让我们知道你对此的感受

最后更新:4个月前

Xamarin.Forms定义了四个布局类 - StackLayout,AbsoluteLayout,RelativeLayout和Grid,并且每个布局类别都以不同的方式排列它们的子类。但是,有时需要使用Xamarin.Forms不提供的布局来组织页面内容。本文介绍了如何编写自定义布局类,并演示了一种方向敏感的WrapLayout类,它将子级别横跨页面排列,然后将后续子项的显示包装到其他行。

概观

在Xamarin.Forms中,所有布局类都从类派生,Layout并将泛型类型限制View为其派生类型。反过来,这个Layout阶级从课堂中得到Layout,它提供了定位和调整儿童元素大小的机制。

每个视觉元素负责确定自己的首选大小,这被称为请求的大小。PageLayoutLayout衍生类型负责确定他们的孩子或孩子相对于自己的位置和大小。因此,布局涉及父子关系,其中父级确定其子级的大小应该是多少,但会尝试容纳所请求的小孩大小。

创建自定义布局需要彻底了解Xamarin.Forms布局和无效循环。现在将讨论这些周期。

布局

布局从页面的视觉树顶部开始,并通过视觉树的所有分支进行,以包含页面上的每个视觉元素。父母对其他因素的要素负责相对于自己的尺寸和定位他们的孩子。

VisualElement类定义了一个Measure测量用于布局操作的元素,和一种方法Layout,用于指定矩形区域的元件将在被渲染方法。当应用程序启动并显示第一个页面时,首先调用一个布局循环Measure,然后Layout调用,从该Page对象开始:

  1. 在布局循环中,每个父元素都负责调用Measure其子对象的方法。
  2. 在孩子被测量之后,每个父元素都负责调用Layout孩子的方法。

这个循环保证了每个页面上的视觉元素接收到来电MeasureLayout方法。该过程如下图所示:

请注意,如果某些更改影响布局,布局周期也可能发生在视觉树的子集上。这包括被添加或删除的项目从集合诸如在StackLayout,在一个变化IsVisible的元件,或在元件的尺寸变化的属性。

每个具有一个Content或一个Children属性的Xamarin.Forms类具有可覆盖的LayoutChildren方法。派生的自定义布局类Layout必须覆盖此方法,并确保在所有元素的子项上调用MeasureLayout方法,以便提供所需的自定义布局。

另外,派生Layout或者Layout必须覆盖OnMeasure方法的每个类,这是布局类决定通过调用Measure其子对象的方式所需的大小。

元素基于约束来确定它们的大小,这些约束指示元素的父元素内的元素有多少空间。传递给约束MeasureOnMeasure方法的范围可从0到Double.PositiveInfinity。当元素被接收到具有非无限参数的方法的调用时,元素被约束完全约束Measure - 元素被约束到特定的大小。当一个元素接收到对其方法的调用时,它至少有一个参数等于- 无限约束可以被认为是表示自动调整,这个元素是无约束的部分受限的MeasureDouble.PositiveInfinity

失效

无效是页面上的元素更改触发新的布局循环的过程。当元素不再具有正确的大小或位置时,元素被认为是无效的。例如,如果FontSize某个属性Button发生更改,则说明该属性Button将不再具有正确的大小。调整大小Button可能会通过页面的其余部分产生布局变化的波纹效应。

元素通过调用该InvalidateMeasure方法无效,通常当元素的属性更改时可能会导致元素的新大小。该方法触发MeasureInvalidated事件,元素的父处理器触发新的布局循环。

Layout类设置为一个处理器MeasureInvalidated上添加到它的每一个孩子的事件Content属性或Children集合,当孩子被删除分离的处理程序。因此,每当其中一个子节点改变大小时,每个可视树中的具有子节点的元素就会被提醒。下图说明了视觉树中元素的大小如何改变可能会导致纹理变化的变化:

但是,Layout该类试图限制一个孩子大小的变化对页面布局的影响。如果布局大小受限,则子视图树中的父布局不会影响任何高度的子尺寸更改。但是,通常情况下,布局大小的变化会影响布局如何布置其子项。因此,布局大小的任何更改都将为布局开始布局循环,布局将接收对其OnMeasureLayoutChildren方法的调用。

Layout类也定义一个InvalidateLayout具有类似目的的方法InvalidateMeasure的方法。InvalidateLayout每当进行更改时,都应调用该方法,影响其子项的布局位置和大小。例如,每当将子添加到布局或从布局中移除时,Layout该类将调用该InvalidateLayout方法。

InvalidateLayout可以覆盖来实现高速缓存,以尽量减少重复调用Measure布局的孩子的方法。覆盖该InvalidateLayout方法将提供一个关于什么时候将子添加到布局或从布局中移除的通知。类似地,OnChildMeasureInvalidated当其中一个布局的子代改变大小时,该方法可被覆盖以提供通知。对于这两种方法覆盖,自定义布局应通过清除缓存来进行响应。有关更多信息,请参阅计算和缓存数据。

创建自定义布局

创建自定义布局的过程如下:

  1. 创建一个派生Layout类的类。有关详细信息,请参阅创建WrapLayout。
  2. 可选 ]添加可绑定属性支持的属性,用于布局类中应设置的任何参数。有关详细信息,请参阅添加由绑定属性支持的属性。
  3. 覆盖在所有布局的子项上OnMeasure调用Measure方法的方法,并返回所需的布局大小。有关更多信息,请参阅覆盖OnMeasure方法。
  4. 覆盖在所有布局的子项上LayoutChildren调用Layout方法的方法。未能Layout在布局中调用每个孩子的方法将导致孩子从未收到正确的大小或位置,因此该小孩将不会在页面上显示。有关更多信息,请参阅覆盖LayoutChildren方法。

    当枚举子进程OnMeasureLayoutChildren覆盖时,请跳过其IsVisible属性设置为的任何子级别false。这将确保自定义布局不会为不可见的孩子留出空间。

  5. 可选 ]覆盖将InvalidateLayout子添加到布局或从布局中移除时通知的方法。有关更多信息,请参阅覆盖InvalidateLayout方法。

  6. 可选 ]覆盖OnChildMeasureInvalidated当其中一个布局的子节点更改大小时要通知的方法。有关更多信息,请参阅覆盖OnChildMeasureInvalidated方法。

请注意,OnMeasure如果布局的大小由其父级而不是其子级控制,则不会调用override。但是,如果一个或两个约束是无限的,或者如果布局类具有非默认值HorizontalOptionsVerticalOptions属性值,则将调用override 。因此,LayoutChildren覆盖不能依赖于OnMeasure方法调用期间获得的子大小。而是在调用该方法之前LayoutChildren必须调用Measure布局的子节点上的Layout方法。或者,OnMeasure可以缓存在覆盖中获取的子项的大小,以避免MeasureLayoutChildren覆盖中稍后调用,但布局类将需要知道何时需要再次获取大小。有关详细信息,请参阅计算和缓存布局数据。

然后可以通过将布局类添加到一个Page,并通过将子项添加到布局来消费。有关更多信息,请参阅使用WrapLayout。

创建一个WrapLayout

示例应用程序演示了一个方向敏感WrapLayout类,将其子级别横跨页面排列,然后将后续子项的显示包装到其他行。

根据WrapLayout孩子的最大大小,该类为每个孩子分配相同数量的空间,称为单元格大小。小于细胞大小的小孩可以根据其细胞的数量HorizontalOptionsVerticalOptions属性值定位在细胞内。

所述WrapLayout类的定义示于下面的代码示例:

public class WrapLayout : Layout<View>
{
  Dictionary<Size, LayoutData> layoutDataCache = new Dictionary<Size, LayoutData>();
  ...
}

计算和缓存布局数据

LayoutData结构存储有关的一些属性的子项的集合数据:

  • VisibleChildCount - 在布局中可见的子项数。
  • CellSize - 所有孩子的最大大小,调整到布局的大小。
  • Rows - 行数。
  • Columns - 列数。

layoutDataCache字段用于存储多个LayoutData值。当应用程序启动时,两个LayoutData对象将被缓存到layoutDataCache字典中用于当前方向 - 一个用于OnMeasure覆盖的约束参数,另一个用于覆盖的参数widthheight参数LayoutChildren。当将设备旋转为横向时,将再次调用OnMeasure覆盖和LayoutChildren覆盖,这将导致另外两个LayoutData对象被缓存到字典中。然而,当将设备返回到纵向方向时,不需要进一步的计算,因为layoutDataCache已经具有所需的数据。

以下代码示例显示了基于特定大小GetLayoutData计算LayoutData结构化属性的方法:

LayoutData GetLayoutData(double width, double height)
{
  Size size = new Size(width, height);

  // Check if cached information is available.
  if (layoutDataCache.ContainsKey(size))
  {
    return layoutDataCache[size];
  }

  int visibleChildCount = 0;
  Size maxChildSize = new Size();
  int rows = 0;
  int columns = 0;
  LayoutData layoutData = new LayoutData();

  // Enumerate through all the children.
  foreach (View child in Children)
  {
    // Skip invisible children.
    if (!child.IsVisible)
      continue;

    // Count the visible children.
    visibleChildCount++;

    // Get the child's requested size.
    SizeRequest childSizeRequest = child.Measure(Double.PositiveInfinity, Double.PositiveInfinity);

    // Accumulate the maximum child size.
    maxChildSize.Width = Math.Max(maxChildSize.Width, childSizeRequest.Request.Width);
    maxChildSize.Height = Math.Max(maxChildSize.Height, childSizeRequest.Request.Height);
  }

  if (visibleChildCount != 0)
  {
    // Calculate the number of rows and columns.
    if (Double.IsPositiveInfinity(width))
    {
      columns = visibleChildCount;
      rows = 1;
    }
    else
    {
      columns = (int)((width + ColumnSpacing) / (maxChildSize.Width + ColumnSpacing));
      columns = Math.Max(1, columns);
      rows = (visibleChildCount + columns - 1) / columns;
    }

    // Now maximize the cell size based on the layout size.
    Size cellSize = new Size();

    if (Double.IsPositiveInfinity(width))
      cellSize.Width = maxChildSize.Width;
    else
      cellSize.Width = (width - ColumnSpacing * (columns - 1)) / columns;

    if (Double.IsPositiveInfinity(height))
      cellSize.Height = maxChildSize.Height;
    else
      cellSize.Height = (height - RowSpacing * (rows - 1)) / rows;

    layoutData = new LayoutData(visibleChildCount, cellSize, rows, columns);
  }

  layoutDataCache.Add(size, layoutData);
  return layoutData;
}

GetLayoutData方法执行以下操作:

  • 它确定计算的LayoutData值是否已经在缓存中,并在可用时返回它。
  • 否则,它枚举所有的孩子,调用Measure每个孩子的方法无限宽和高,并确定最大的子大小。
  • 如果至少有一个可见的子项,则它计算所需的行数和列数,然后根据该维数计算子项的单元格大小WrapLayout。请注意,单元格大小通常比最大小孩大小略宽,但如果WrapLayout最宽的小孩不够宽,或者最高的孩子足够高,那么它也可以更小。
  • 它将新LayoutData值存储在缓存中。

添加由绑定属性支持的属性

WrapLayout类定义ColumnSpacingRowSpacing属性,其值用于在布局中的行和列中分离,并且通过绑定属性被备份。可绑定属性显示在以下代码示例中:

public static readonly BindableProperty ColumnSpacingProperty = BindableProperty.Create(
  "ColumnSpacing",
  typeof(double),
  typeof(WrapLayout),
  5.0,
  propertyChanged: (bindable, oldvalue, newvalue) =>
  {
    ((WrapLayout)bindable).InvalidateLayout();
  });

public static readonly BindableProperty RowSpacingProperty = BindableProperty.Create(
  "RowSpacing",
  typeof(double),
  typeof(WrapLayout),
  5.0,
  propertyChanged: (bindable, oldvalue, newvalue) =>
  {
    ((WrapLayout)bindable).InvalidateLayout();
  });

每个可绑定属性的属性更改的处理程序调用InvalidateLayout方法覆盖以触发新的布局传递WrapLayout。有关更多信息,请参阅覆盖InvalidateLayout方法并覆盖OnChildMeasureInvalidated方法。

覆盖OnMeasure方法

OnMeasure倍率显示在下面的代码示例:

protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint)
{
  LayoutData layoutData = GetLayoutData(widthConstraint, heightConstraint);
  if (layoutData.VisibleChildCount == 0)
  {
    return new SizeRequest();
  }

  Size totalSize = new Size(layoutData.CellSize.Width * layoutData.Columns + ColumnSpacing * (layoutData.Columns - 1),
                layoutData.CellSize.Height * layoutData.Rows + RowSpacing * (layoutData.Rows - 1));
  return new SizeRequest(totalSize);
}

覆盖调用该GetLayoutData方法并SizeRequest从返回的数据构造一个对象,同时考虑RowSpacingColumnSpacing属性值。有关该GetLayoutData方法的更多信息,请参阅计算和缓存数据。

⚠️

MeasureOnMeasure方法不应该通过返回的请求的无穷维SizeRequest与属性设置为值Double.PositiveInfinity。但是,至少有一个约束参数OnMeasure可以是Double.PositiveInfinity

覆盖LayoutChildren方法

LayoutChildren倍率显示在下面的代码示例:

protected override void LayoutChildren(double x, double y, double width, double height)
{
  LayoutData layoutData = GetLayoutData(width, height);

  if (layoutData.VisibleChildCount == 0)
  {
    return;
  }

  double xChild = x;
  double yChild = y;
  int row = 0;
  int column = 0;

  foreach (View child in Children)
  {
    if (!child.IsVisible)
    {
      continue;
    }

    LayoutChildIntoBoundingRegion(child, new Rectangle(new Point(xChild, yChild), layoutData.CellSize));
    if (++column == layoutData.Columns)
    {
      column = 0;
      row++;
      xChild = x;
      yChild += RowSpacing + layoutData.CellSize.Height;
    }
    else
    {
      xChild += ColumnSpacing + layoutData.CellSize.Width;
    }
  }
}

覆盖开始于对该GetLayoutData方法的调用,然后枚举所有的孩子的大小并将它们放置在每个孩子的单元格内。这是通过调用LayoutChildIntoBoundingRegion方法来实现的,该方法用于根据其HorizontalOptionsVerticalOptions属性值将一个子项定位在一个矩形内。这相当于调用孩子的Layout方法。

请注意,传递给该LayoutChildIntoBoundingRegion方法的矩形包括小孩所在的整个区域。

有关该GetLayoutData方法的更多信息,请参阅计算和缓存数据。

覆盖InvalidateLayout方法

InvalidateLayout当孩子被添加到布局中或从布局中移除时,或当其中一个WrapLayout属性更改值时,将调用该覆盖,如以下代码示例所示:

protected override void InvalidateLayout()
{
  base.InvalidateLayout();
  layoutInfoCache.Clear();
}

覆盖使布局无效,并丢弃所有缓存的布局信息。

要停止Layout类调用InvalidateLayout每当孩子添加或删除从布局方法,覆盖ShouldInvalidateOnChildAddedShouldInvalidateOnChildRemoved方法,并返回false。然后,当添加或删除子项时,布局类可以实现自定义进程。

覆盖OnChildMeasureInvalidated方法

OnChildMeasureInvalidated当其中一个布局的子节点更改大小时,将调用该覆盖,并显示在以下代码示例中:

protected override void OnChildMeasureInvalidated()
{
  base.OnChildMeasureInvalidated();
  layoutInfoCache.Clear();
}

该覆盖使子版面无效,并丢弃所有缓存的布局信息。

消耗WrapLayout

WrapLayout类可以通过将其放置在被消耗Page派生类型,如下面的XAML代码示例表明:

<ContentPage ... xmlns:local="clr-namespace:ImageWrapLayout">
    <ScrollView Margin="0,20,0,20">
        <local:WrapLayout x:Name="wrapLayout" />
    </ScrollView>
</ContentPage>

等效的C#代码如下所示:

public class ImageWrapLayoutPageCS : ContentPage
{
  WrapLayout wrapLayout;

  public ImageWrapLayoutPageCS()
  {
    wrapLayout = new WrapLayout();

    Content = new ScrollView
    {
      Margin = new Thickness(0, 20, 0, 20),
      Content = wrapLayout
    };
  }
  ...
}

然后可以WrapLayout根据需要将儿童加入。以下代码示例显示Image要添加到的元素WrapLayout

protected override async void OnAppearing()
{
  base.OnAppearing();

  var images = await GetImageListAsync();
  foreach (var photo in images.Photos)
  {
    var image = new Image
    {
      Source = ImageSource.FromUri(new Uri(photo + string.Format("?width={0}&height={0}&mode=max", Device.OnPlatform(240, 240, 120))))
    };
    wrapLayout.Children.Add(image);
  }
}

async Task<ImageList> GetImageListAsync()
{
  var requestUri = "https://docs.xamarin.com/demo/stock.json";
  using (var client = new HttpClient())
  {
    var result = await client.GetStringAsync(requestUri);
    return JsonConvert.DeserializeObject<ImageList>(result);
  }
}

当包含WrapLayout出现的页面时,示例应用程序异步访问包含照片列表的远程JSON文件,Image为每张照片创建一个元素,并将其添加到WrapLayout。这将导致以下屏幕截图中显示的外观:

以下屏幕截图显示了WrapLayout它被旋转到横向:

  

每行中的列数取决于照片大小,屏幕宽度和每个与设备无关的单元的像素数。该Image元件异步加载的照片,并且因此,WrapLayout类将接收到其频繁调用LayoutChildren每个方法Image元件接收基于所加载的照片中的新的大小。

概要

 

本文介绍了如何编写自定义布局类,并演示了一种方向敏感WrapLayout类,将其子级别横跨页面排列,然后将后续子项的显示包装到其他行。

你可能感兴趣的:(Xamarin,xamarin.forms,Xamarin.Forms,跨平台新势力)