在中建立自定義版面配置 Xamarin.Forms

Xamarin.Forms 會定義五個版面配置類別 – StackLayout、AbsoluteLayout、RelativeLayout、Grid 和 FlexLayout,且每個配置類別都會以不同的方式排列其子系。 不過,有時候必須使用 未提供的 Xamarin.Forms版面配置來組織頁面內容。 本文說明如何撰寫自定義版面配置類別,並示範區分方向的 WrapLayout 類別,將子系水準排列在頁面,然後將後續子系的顯示換行至其他數據列。

在 中 Xamarin.Forms,所有版面配置類別都衍生自 類別, Layout<T> 並將泛型型別限制為 View 及其衍生型別。 接著,類別 Layout<T> 衍生自 Layout 類別,其提供定位和重設大小子項目的機制。

每個視覺元素都會負責判斷自己的慣用大小,這稱為 要求 的大小。 PageLayoutLayout<View> 衍生類型負責判斷其子系或子系相對於本身的位置和大小。 因此,配置牽涉到父子式關聯性,其中父系會決定其子系的大小,但會嘗試容納子系所要求的大小。

必須徹底瞭解 Xamarin.Forms 版面配置和失效迴圈,才能建立自定義版面配置。 現在將討論這些迴圈。

版面配置

版面配置從可視化樹狀結構頂端開始,並透過可視化樹狀結構的所有分支繼續進行,以包含頁面上的每個視覺元素。 屬於其他元素之父系的項目負責重設大小,並將其子系相對於自己的位置。

類別 VisualElementMeasure 定義測量配置作業之元素的方法,以及 Layout 指定專案將在其中轉譯之矩形區域的方法。 當應用程式啟動並顯示第一頁時,會先由呼叫所組成的Measure版面配置週期,然後在 Layout 對象上Page啟動呼叫:

  1. 在配置週期期間,每個父元素都會負責在其子系上呼叫 Measure 方法。
  2. 測量子系之後,每個父元素都會負責在其子系上呼叫 Layout 方法。

此循環可確保頁面上的每個視覺項目都會收到 和 Layout 方法的呼叫Measure。 下圖顯示此程式:

Xamarin.Forms 版面配置週期

注意

請注意,如果某個變更會影響配置,則配置週期也可能發生在可視化樹狀結構的子集上。 這包括要加入或移除集合中的專案,例如 中的 StackLayout、專案的屬性變更 IsVisible ,或專案大小變更。

具有 ContentChildren 屬性的每個Xamarin.Forms類別都有可LayoutChildren覆寫的方法。 衍生自 Layout<View> 的自定義版面配置類別必須覆寫這個方法,並確保 Measure 在所有元素的子系上呼叫 和 Layout 方法,以提供所需的自定義配置。

此外,衍生自 LayoutLayout<View> 的每個類別都必須覆寫 OnMeasure 方法,也就是配置類別藉由呼叫 Measure 其子系的方法來判斷它所需的大小。

注意

元素會根據 條件約束來決定其大小,指出元素父代內元素可用的空間量。 傳遞至 MeasureOnMeasure 方法的條件約束範圍可以從 0 到 Double.PositiveInfinity。 當專案收到具有Measure非無限自變數之方法的呼叫時,元素會受到限制完全限制, 元素會限制為特定大小。 當專案收到至少一個等於Double.PositiveInfinity一個自變數的Measure呼叫時,元素是不受限制部分限制的, 可以視為表示自動重設大小。

失效

失效是頁面上元素變更觸發新版面配置週期的程式。 當元素不再具有正確的大小或位置時,會被視為無效。 例如,如果 FontSizeButton 屬性有所變更, Button 則表示 無效,因為它不再具有正確的大小。 重設大小 Button 之後,可能會對版面配置變更的波紋效果,透過頁面的其餘部分。

元素會藉由叫 InvalidateMeasure 用 方法來使自己失效,通常當元素的 屬性變更時,可能會導致專案的新大小。 這個方法會 MeasureInvalidated 引發 事件,元素的父代會處理以觸發新的版面配置迴圈。

類別會在Layout新增至其Content屬性或Children集合的每個子系上設定事件的處理程式MeasureInvalidated,並在移除子系時中斷連結處理程式。 因此,每當其中一個子系變更大小時,就會警示具有子系之可視化樹狀結構中的每個元素。 下圖說明可視化樹狀結構中元素大小的變更,如何造成樹狀結構的變更:

可視化樹狀結構中的無效

不過,類別 Layout 會嘗試限制子系在頁面版面配置上變更的影響。 如果配置的大小受限,則子大小變更不會影響比可視化樹狀結構中父版面配置更高的任何專案。 不過,配置大小的變更通常會影響版面配置排列其子系的方式。 因此,配置大小的任何變更都會啟動版面配置週期,而且配置將會接收對其 OnMeasureLayoutChildren 方法的呼叫。

類別 Layout 也會定義 InvalidateLayout 方法,其用途 InvalidateMeasure 與方法類似。 InvalidateLayout每當進行會影響配置位置和大小其子系的變更時,都應該叫用 方法。 例如,每當子系加入或從版面配置中移除時,類別 Layout 都會叫 InvalidateLayout 用 方法。

InvalidateLayout可以覆寫 來實作快取,以將配置子系方法的重複調用Measure降到最低。 覆寫 InvalidateLayout 方法時,會在配置中新增或移除子系時,提供 的通知。 同樣地,當其中一個版面配置的子系變更大小時, OnChildMeasureInvalidated 可以覆寫 方法以提供通知。 針對這兩種方法覆寫,自定義配置應該透過清除快取來回應。 如需詳細資訊,請參閱 計算和快取配置數據

建立自定義版面配置

建立自訂版面設定的程式如下所示:

  1. 建立從 Layout<View> 類別衍生的類別。 如需詳細資訊,請參閱 建立 WrapLayout

  2. [選擇性] 針對配置類別上應設定的任何參數,新增可系結屬性所支持的屬性。 如需詳細資訊,請參閱 新增可系結屬性所支持的屬性

  3. OnMeasure覆寫 方法以在所有版面配置子系上叫Measure用 方法,並傳回配置的要求大小。 如需詳細資訊,請參閱 覆寫 OnMeasure 方法

  4. 覆寫 方法, LayoutChildren 以在所有版面配置子系上叫 Layout 用 方法。 無法在配置中的每個子系上叫 Layout 用 方法,會導致子系永遠不會收到正確的大小或位置,因此子系將不會顯示在頁面上。 如需詳細資訊,請參閱 覆寫 LayoutChildren 方法

    注意

    列舉 和 LayoutChildren 覆寫中的OnMeasure子系時,請略過屬性IsVisible設定為 false的任何子系。 這可確保自定義配置不會保留隱藏子系的空間。

  5. [選擇性] 覆寫方法, InvalidateLayout 以在從版面配置新增或移除子系時收到通知。 如需詳細資訊,請參閱 覆寫 InvalidateLayout 方法

  6. [選擇性] 覆寫 OnChildMeasureInvalidated 當其中一個版面配置子系變更大小時要通知的方法。 如需詳細資訊,請參閱 覆寫 OnChildMeasureInvalidated 方法

注意

請注意, OnMeasure 如果版面配置的大小是由其父代控管,而不是其子系,則不會叫用覆寫。 不過,如果其中一個或兩個條件約束都是無限的,或者如果版面配置類別具有非預設值 HorizontalOptionsVerticalOptions 屬性值,則會叫用覆寫。 基於這個理由,覆 LayoutChildren 寫不能依賴在方法呼叫期間取得的 OnMeasure 子大小。 相反地,在叫用 方法之前LayoutLayoutChildren必須先在版面配置的子系上叫Measure用 方法。 或者,可以快取覆寫中取得的子系大小,以避免稍後Measure在覆寫中OnMeasureLayoutChildren叫用,但版面配置類別必須知道何時需要再次取得大小。 如需詳細資訊,請參閱 計算和快取配置數據

然後,可以將配置類別新增至 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,另一個用於 覆寫的 widthLayoutChildrenheight 自變數。 將裝置旋轉成橫向時, 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 值儲存在快取中。

新增可系結屬性所支持的屬性

類別 WrapLayoutColumnSpacing 定義 和 RowSpacing 屬性,其值可用來分隔配置中的資料列和數據行,以及可系結屬性所支援的屬性。 可繫結的屬性會顯示在下列程式代碼範例中:

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 叫用 方法來達成,此方法可用來根據子 HorizontalOptions 系和 VerticalOptions 屬性值,將子系放置在矩形內。 這相當於呼叫子系的 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();
    if (images != null)
    {
        foreach (var photo in images.Photos)
        {
            var image = new Image
            {
                Source = ImageSource.FromUri(new Uri(photo))
            };
            wrapLayout.Children.Add(image);
        }
    }
}

async Task<ImageList> GetImageListAsync()
{
    try
    {
        string requestUri = "https://raw.githubusercontent.com/xamarin/docs-archive/master/Images/stock/small/stock.json";
        string result = await _client.GetStringAsync(requestUri);
        return JsonConvert.DeserializeObject<ImageList>(result);
    }
    catch (Exception ex)
    {
        Debug.WriteLine($"\tERROR: {ex.Message}");
    }

    return null;
}

當包含 WrapLayout 的頁面出現時,範例應用程式會以異步方式存取包含相片清單的遠端 JSON 檔案、為每個相片建立 Image 元素,並將它新增至 WrapLayout。 這會導致下列螢幕擷取畫面中顯示的外觀:

範例應用程式直向螢幕快照

下列螢幕快照顯示 WrapLayout 旋轉至橫向之後的 :

範例 iOS 應用程式橫向螢幕快照Android 應用程式橫向範例螢幕快照範例 UWP 應用程式橫向螢幕快照

每個數據列中的數據行數目取決於相片大小、螢幕寬度,以及每個裝置獨立單位的像素數目。 元素 Image 會以異步方式載入相片,因此類別 WrapLayout 會經常收到其 LayoutChildren 方法的呼叫,因為每個 Image 元素都會根據載入的相片接收新的大小。