Xamarin.Forms 用戶界面——控件——佈局——創建自定義佈局

創建自定義佈局

PDF用於離線使用
示例代碼:
相關文章:
相關API:
有關的影片:

讓我們知道你對此的感受

最後更新:4個月前

Xamarin.Forms定義了四個佈局類 - StackLayout,AbsoluteLayout,RelativeLayout和Grid,並且每個佈局類別都以不同的方式排列它們的子類。但是,有時需要使用Xamarin.Forms不提供的佈局來組織頁面內容。本文介紹瞭如何編寫自定義佈局類,並演示了一種方向敏感的WrapLayout類,它將子級別橫跨頁面排列,然後將後續子項的顯示包裝到其他行。

概觀

在Xamarin.Forms中,所有佈局類都從類派生,Layout<T>並將泛型類型限制View爲其派生類型。反過來,這個Layout<T>階級從課堂中得到Layout,它提供了定位和調整兒童元素大小的機制。

每個視覺元素負責確定自己的首選大小,這被稱爲請求的大小。PageLayoutLayout<View>衍生類型負責確定他們的孩子或孩子相對於自己的位置和大小。因此,佈局涉及父子關係,其中父級確定其子級的大小應該是多少,但會嘗試容納所請求的小孩大小。

創建自定義佈局需要徹底瞭解Xamarin.Forms佈局和無效循環。現在將討論這些週期。

佈局

佈局從頁面的視覺樹頂部開始,並通過視覺樹的所有分支進行,以包含頁面上的每個視覺元素。父母對其他因素的要素負責相對於自己的尺寸和定位他們的孩子。

VisualElement類定義了一個Measure測量用於佈局操作的元素,和一種方法Layout,用於指定矩形區域的元件將在被渲染方法。當應用程序啓動並顯示第一個頁面時,首先調用一個佈局循環Measure,然後Layout調用,從該Page對象開始:

  1. 在佈局循環中,每個父元素都負責調用Measure其子對象的方法。
  2. 在孩子被測量之後,每個父元素都負責調用Layout孩子的方法。

這個循環保證了每個頁面上的視覺元素接收到來電MeasureLayout方法。該過程如下圖所示:

請注意,如果某些更改影響佈局,佈局週期也可能發生在視覺樹的子集上。這包括被添加或刪除的項目從集合諸如在StackLayout,在一個變化IsVisible的元件,或在元件的尺寸變化的屬性。

每個具有一個Content或一個Children屬性的Xamarin.Forms類具有可覆蓋的LayoutChildren方法。派生的自定義佈局類Layout<View>必須覆蓋此方法,並確保在所有元素的子項上調用MeasureLayout方法,以便提供所需的自定義佈局。

另外,派生Layout或者Layout<View>必須覆蓋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<View>類的類。有關詳細信息,請參閱創建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類,將其子級別橫跨頁面排列,然後將後續子項的顯示包裝到其他行。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章