在中建立自定義版面配置 Xamarin.Forms
Xamarin.Forms 會定義五個版面配置類別 – StackLayout、AbsoluteLayout、RelativeLayout、Grid 和 FlexLayout,且每個配置類別都會以不同的方式排列其子系。 不過,有時候必須使用 未提供的 Xamarin.Forms版面配置來組織頁面內容。 本文說明如何撰寫自定義版面配置類別,並示範區分方向的 WrapLayout 類別,將子系水準排列在頁面,然後將後續子系的顯示換行至其他數據列。
在 中 Xamarin.Forms,所有版面配置類別都衍生自 類別, Layout<T>
並將泛型型別限制為 View
及其衍生型別。 接著,類別 Layout<T>
衍生自 Layout
類別,其提供定位和重設大小子項目的機制。
每個視覺元素都會負責判斷自己的慣用大小,這稱為 要求 的大小。 Page
、 Layout
和 Layout<View>
衍生類型負責判斷其子系或子系相對於本身的位置和大小。 因此,配置牽涉到父子式關聯性,其中父系會決定其子系的大小,但會嘗試容納子系所要求的大小。
必須徹底瞭解 Xamarin.Forms 版面配置和失效迴圈,才能建立自定義版面配置。 現在將討論這些迴圈。
版面配置
版面配置從可視化樹狀結構頂端開始,並透過可視化樹狀結構的所有分支繼續進行,以包含頁面上的每個視覺元素。 屬於其他元素之父系的項目負責重設大小,並將其子系相對於自己的位置。
類別 VisualElement
會 Measure
定義測量配置作業之元素的方法,以及 Layout
指定專案將在其中轉譯之矩形區域的方法。 當應用程式啟動並顯示第一頁時,會先由呼叫所組成的Measure
版面配置週期,然後在 Layout
對象上Page
啟動呼叫:
- 在配置週期期間,每個父元素都會負責在其子系上呼叫
Measure
方法。 - 測量子系之後,每個父元素都會負責在其子系上呼叫
Layout
方法。
此循環可確保頁面上的每個視覺項目都會收到 和 Layout
方法的呼叫Measure
。 下圖顯示此程式:
注意
請注意,如果某個變更會影響配置,則配置週期也可能發生在可視化樹狀結構的子集上。 這包括要加入或移除集合中的專案,例如 中的 StackLayout
、專案的屬性變更 IsVisible
,或專案大小變更。
具有 Content
或 Children
屬性的每個Xamarin.Forms類別都有可LayoutChildren
覆寫的方法。 衍生自 Layout<View>
的自定義版面配置類別必須覆寫這個方法,並確保 Measure
在所有元素的子系上呼叫 和 Layout
方法,以提供所需的自定義配置。
此外,衍生自 Layout
或 Layout<View>
的每個類別都必須覆寫 OnMeasure
方法,也就是配置類別藉由呼叫 Measure
其子系的方法來判斷它所需的大小。
注意
元素會根據 條件約束來決定其大小,指出元素父代內元素可用的空間量。 傳遞至 Measure
和 OnMeasure
方法的條件約束範圍可以從 0 到 Double.PositiveInfinity
。 當專案收到具有Measure
非無限自變數之方法的呼叫時,元素會受到限制或完全限制, 元素會限制為特定大小。 當專案收到至少一個等於Double.PositiveInfinity
一個自變數的Measure
呼叫時,元素是不受限制或部分限制的, 可以視為表示自動重設大小。
失效
失效是頁面上元素變更觸發新版面配置週期的程式。 當元素不再具有正確的大小或位置時,會被視為無效。 例如,如果 FontSize
的 Button
屬性有所變更, Button
則表示 無效,因為它不再具有正確的大小。 重設大小 Button
之後,可能會對版面配置變更的波紋效果,透過頁面的其餘部分。
元素會藉由叫 InvalidateMeasure
用 方法來使自己失效,通常當元素的 屬性變更時,可能會導致專案的新大小。 這個方法會 MeasureInvalidated
引發 事件,元素的父代會處理以觸發新的版面配置迴圈。
類別會在Layout
新增至其Content
屬性或Children
集合的每個子系上設定事件的處理程式MeasureInvalidated
,並在移除子系時中斷連結處理程式。 因此,每當其中一個子系變更大小時,就會警示具有子系之可視化樹狀結構中的每個元素。 下圖說明可視化樹狀結構中元素大小的變更,如何造成樹狀結構的變更:
不過,類別 Layout
會嘗試限制子系在頁面版面配置上變更的影響。 如果配置的大小受限,則子大小變更不會影響比可視化樹狀結構中父版面配置更高的任何專案。 不過,配置大小的變更通常會影響版面配置排列其子系的方式。 因此,配置大小的任何變更都會啟動版面配置週期,而且配置將會接收對其 OnMeasure
和 LayoutChildren
方法的呼叫。
類別 Layout
也會定義 InvalidateLayout
方法,其用途 InvalidateMeasure
與方法類似。 InvalidateLayout
每當進行會影響配置位置和大小其子系的變更時,都應該叫用 方法。 例如,每當子系加入或從版面配置中移除時,類別 Layout
都會叫 InvalidateLayout
用 方法。
InvalidateLayout
可以覆寫 來實作快取,以將配置子系方法的重複調用Measure
降到最低。 覆寫 InvalidateLayout
方法時,會在配置中新增或移除子系時,提供 的通知。 同樣地,當其中一個版面配置的子系變更大小時, OnChildMeasureInvalidated
可以覆寫 方法以提供通知。 針對這兩種方法覆寫,自定義配置應該透過清除快取來回應。 如需詳細資訊,請參閱 計算和快取配置數據。
建立自定義版面配置
建立自訂版面設定的程式如下所示:
建立從
Layout<View>
類別衍生的類別。 如需詳細資訊,請參閱 建立 WrapLayout。[選擇性] 針對配置類別上應設定的任何參數,新增可系結屬性所支持的屬性。 如需詳細資訊,請參閱 新增可系結屬性所支持的屬性。
OnMeasure
覆寫 方法以在所有版面配置子系上叫Measure
用 方法,並傳回配置的要求大小。 如需詳細資訊,請參閱 覆寫 OnMeasure 方法。覆寫 方法,
LayoutChildren
以在所有版面配置子系上叫Layout
用 方法。 無法在配置中的每個子系上叫Layout
用 方法,會導致子系永遠不會收到正確的大小或位置,因此子系將不會顯示在頁面上。 如需詳細資訊,請參閱 覆寫 LayoutChildren 方法。注意
列舉 和
LayoutChildren
覆寫中的OnMeasure
子系時,請略過屬性IsVisible
設定為false
的任何子系。 這可確保自定義配置不會保留隱藏子系的空間。[選擇性] 覆寫方法,
InvalidateLayout
以在從版面配置新增或移除子系時收到通知。 如需詳細資訊,請參閱 覆寫 InvalidateLayout 方法。[選擇性] 覆寫
OnChildMeasureInvalidated
當其中一個版面配置子系變更大小時要通知的方法。 如需詳細資訊,請參閱 覆寫 OnChildMeasureInvalidated 方法。
注意
請注意, OnMeasure
如果版面配置的大小是由其父代控管,而不是其子系,則不會叫用覆寫。 不過,如果其中一個或兩個條件約束都是無限的,或者如果版面配置類別具有非預設值 HorizontalOptions
或 VerticalOptions
屬性值,則會叫用覆寫。 基於這個理由,覆 LayoutChildren
寫不能依賴在方法呼叫期間取得的 OnMeasure
子大小。 相反地,在叫用 方法之前Layout
,LayoutChildren
必須先在版面配置的子系上叫Measure
用 方法。 或者,可以快取覆寫中取得的子系大小,以避免稍後Measure
在覆寫中OnMeasure
LayoutChildren
叫用,但版面配置類別必須知道何時需要再次取得大小。 如需詳細資訊,請參閱 計算和快取配置數據。
然後,可以將配置類別新增至 Page
來取用配置類別,並將子系新增至配置。 如需詳細資訊,請參閱 取用 WrapLayout。
建立 WrapLayout
範例應用程式示範一個區分方向的 WrapLayout
類別,會水平排列其子系跨頁面,然後將後續子系的顯示包裝至其他數據列。
類別 WrapLayout
會根據子系的大小上限,為每個子系配置相同的空間量,稱為 數據格大小。 小於儲存格大小的子系可以根據其 HorizontalOptions
和 VerticalOptions
屬性值,放置在儲存格內。
類別 WrapLayout
定義會顯示在下列程式代碼範例中:
public class WrapLayout : Layout<View>
{
Dictionary<Size, LayoutData> layoutDataCache = new Dictionary<Size, LayoutData>();
...
}
計算和快取版面配置數據
結構 LayoutData
會將子系集合的相關數據儲存在數個屬性中:
VisibleChildCount
– 配置中可見的子係數目。CellSize
– 所有子系的大小上限,已調整為版面配置的大小。Rows
– 資料列數目。Columns
– 資料行數目。
欄位 layoutDataCache
用來儲存多個 LayoutData
值。 當應用程式啟動時,會將兩LayoutData
個物件快取到目前方向的字典中layoutDataCache
–一個用於覆寫的條件約束自變數OnMeasure
,另一個用於 覆寫的 width
LayoutChildren
和 height
自變數。 將裝置旋轉成橫向時, 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
會 ColumnSpacing
定義 和 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
對象,同時考慮 RowSpacing
和 ColumnSpacing
屬性值。 如需 方法的詳細資訊 GetLayoutData
,請參閱 計算和快取配置數據。
重要
和 Measure
OnMeasure
方法絕對不應該藉由傳回 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
,請覆寫 ShouldInvalidateOnChildAdded
和 ShouldInvalidateOnChildRemoved
方法,並傳回 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
旋轉至橫向之後的 :
每個數據列中的數據行數目取決於相片大小、螢幕寬度,以及每個裝置獨立單位的像素數目。 元素 Image
會以異步方式載入相片,因此類別 WrapLayout
會經常收到其 LayoutChildren
方法的呼叫,因為每個 Image
元素都會根據載入的相片接收新的大小。