自訂版面配置
.NET 多平臺應用程式 UI (.NET MAUI) 定義多個版面配置類別,每個類別都會以不同的方式排列其子系。 版面配置可以視為具有規則和屬性的檢視清單,這些規則和屬性會定義如何在版面配置中排列這些檢視。 版面設定範例包括 Grid、 AbsoluteLayout和 VerticalStackLayout。
.NET MAUI 版面配置類別衍生自抽象 Layout 類。 此類別會將跨平臺配置和度量委派給配置管理員類別。 類別 Layout 也包含可 CreateLayoutManager() 覆寫的方法,衍生的配置可用來指定版面配置管理員。
每個版面配置管理員類別都會實作 ILayoutManager 介面,指定 Measure 必須提供 和 ArrangeChildren 實作:
- 實作 Measure 會呼叫 IView.Measure 配置中的每個檢視,並傳回指定條件約束的配置總大小。
- 實作 ArrangeChildren 會決定每個檢視應該放在版面配置界限內的位置,並呼叫 Arrange 每個檢視及其適當界限。 傳回值是版面配置的實際大小。
.NET MAUI 的配置具有預先定義的版面配置管理員來處理其配置。 不過,有時候必須使用 .NET MAUI 未提供的版面配置來組織頁面內容。 這可以藉由產生自己的自定義配置來達成,這需要您瞭解 .NET MAUI 跨平臺配置程序的運作方式。
版面配置程式
.NET MAUI 的跨平臺版面配置程式建置在每個平臺上的原生版面配置程式之上。 一般而言,版面配置程式是由原生版面配置系統起始。 跨平台進程會在版面配置或內容控件因原生版面配置系統測量或排列而起始它時執行。
注意
每個平臺會以稍微不同的方式處理版面配置。 不過,.NET MAUI 的跨平臺版面配置程序的目標是盡可能與平台無關。
下圖顯示原生版面設定系統起始版面配置測量時的程式:
所有 .NET MAUI 配置在每個平臺上都有單一支持檢視:
- 在 Android 上,此支援檢視是
LayoutViewGroup
。 - 在 iOS 和 Mac Catalyst 上,此備份檢視是
LayoutView
。 - 在 Windows 上,此備份檢視是
LayoutPanel
。
當平臺的原生配置系統要求測量其中一個支持檢視時,支援檢視會呼叫 Layout.CrossPlatformMeasure 方法。 這是從原生版面配置系統傳遞至 .NET MAUI 版面配置系統的控件點。 Layout.CrossPlatformMeasure 會呼叫配置管理員的方法 Measure 。 此方法負責藉由在版面配置中的每個檢視上呼叫 IView.Measure 來測量子檢視。 檢視會測量其原生控件,並根據該度量更新其 DesiredSize 屬性。 這個值會以 方法的結果 CrossPlatformMeasure
傳回至備份檢視。 支持檢視會執行它需要執行的任何內部處理,並將其測量大小傳回至平臺。
下圖顯示原生版面設定系統起始版面設定排列時的程式:
當平臺的原生配置系統要求其中一個支持檢視的排列或配置時,支持檢視會呼叫 Layout.CrossPlatformArrange 方法。 這是從原生版面配置系統傳遞至 .NET MAUI 版面配置系統的控件點。 Layout.CrossPlatformArrange 會呼叫配置管理員的方法 ArrangeChildren 。 此方法負責判斷每個檢視應該放在版面配置界限內的位置,並呼叫 Arrange 每個檢視來設定其位置。 配置的大小會以 方法的結果 CrossPlatformArrange
傳回至備份檢視。 支持檢視會執行它需要執行的任何內部處理,並將實際大小傳回平臺。
注意
ILayoutManager.Measure 可能會在呼叫之前 ArrangeChildren 多次呼叫,因為平臺可能需要先執行一些推測性測量,再排列檢視。
自定義版面配置方法
建立自訂版面配置有兩個主要方法:
- 建立自定義版面配置類型,通常是現有版面配置類型或的 Layout子類別,並在您的自定義版面配置類型中覆寫 CreateLayoutManager() 。 然後,提供 ILayoutManager 包含自定義配置邏輯的實作。 如需詳細資訊,請參閱 建立自定義版面配置類型。
- 建立實作 的型別,以修改現有版面配置類型 ILayoutManagerFactory的行為。 然後,使用此版面配置管理員處理站,將現有版面配置的 .NET MAUI 預設版面配置管理員取代為包含自定義版面配置邏輯的實 ILayoutManager 作。 如需詳細資訊,請參閱 修改現有版面配置的行為。
建立自定義版面配置類型
建立自訂版面設定類型的程式是:
建立類別,以子類別化現有的版面配置類型或 Layout 類別,並在您的自定義版面配置類型中覆寫 CreateLayoutManager() 。 如需詳細資訊,請參閱 子類別配置。
建立衍生自現有版面配置管理員的版面配置管理員類別,或直接實作 ILayoutManager 介面。 在版面配置管理員類別中,您應該:
- 覆寫或實作 方法, Measure 以根據配置的條件約束來計算配置的總大小。
- 覆寫或實作 方法, ArrangeChildren 以調整和放置配置中的所有子系。
如需詳細資訊,請參閱 建立版面配置管理員。
將自定義版面配置類型新增至 ,並將子系新增至 Page版面配置,以取用您的自定義版面配置類型。 如需詳細資訊,請參閱 取用版面配置類型。
方向敏感 HorizontalWrapLayout
是用來示範此程式。 HorizontalWrapLayout
與 中的 類似 HorizontalStackLayout ,它會水平排列頁面的子系。 不過,它會在遇到容器右邊緣時,將子系的顯示包裝到新的數據列
注意
此 範例 會定義可用來瞭解如何產生自定義版面配置的其他自定義版面配置。
子類別配置
若要建立自定義版面配置類型,您必須先將現有版面配置類型或 Layout 類別子類別子類別。 然後,在您的版面配置類型中覆寫 CreateLayoutManager() ,並傳回配置類型配置管理員的新實例:
using Microsoft.Maui.Layouts;
public class HorizontalWrapLayout : HorizontalStackLayout
{
protected override ILayoutManager CreateLayoutManager()
{
return new HorizontalWrapLayoutManager(this);
}
}
HorizontalWrapLayout
衍生自 HorizontalStackLayout 以使用其版面配置功能。 .NET MAUI 版面配置會將跨平臺配置和度量委派給版面配置管理員類別。 因此,覆寫會 CreateLayoutManager() 傳回 類別的新實例 HorizontalWrapLayoutManager
,也就是下一節所討論的配置管理員。
建立版面配置管理員
配置管理員類別可用來執行自定義版面配置類型的跨平臺配置和度量。 它應該衍生自現有的版面配置管理員,或應該直接實作 ILayoutManager 介面。 HorizontalWrapLayoutManager
衍生自 HorizontalStackLayoutManager ,使其可以使用其基礎功能,並存取其繼承階層中的成員:
using Microsoft.Maui.Layouts;
using HorizontalStackLayoutManager = Microsoft.Maui.Layouts.HorizontalStackLayoutManager;
public class HorizontalWrapLayoutManager : HorizontalStackLayoutManager
{
HorizontalWrapLayout _layout;
public HorizontalWrapLayoutManager(HorizontalWrapLayout horizontalWrapLayout) : base(horizontalWrapLayout)
{
_layout = horizontalWrapLayout;
}
public override Size Measure(double widthConstraint, double heightConstraint)
{
}
public override Size ArrangeChildren(Rect bounds)
{
}
}
建 HorizontalWrapLayoutManager
構函式會將類型的實例 HorizontalWrapLayout
儲存在欄位中,以便在整個版面配置管理員中存取它。 配置管理員也會覆寫 Measure 類別的 HorizontalStackLayoutManager 和 ArrangeChildren 方法。 這些方法是您將定義實作自定義版面配置的邏輯。
測量版面配置大小
實作 ILayoutManager.Measure 的目的是要計算版面配置的總大小。 它應該藉由在版面配置中的每個子系上呼叫 IView.Measure 來執行這項操作。 然後,它應該使用此數據來計算並傳回配置的總大小,因為其條件約束。
下列範例顯示 Measure 類別的 HorizontalWrapLayoutManager
實作:
public override Size Measure(double widthConstraint, double heightConstraint)
{
var padding = _layout.Padding;
widthConstraint -= padding.HorizontalThickness;
double currentRowWidth = 0;
double currentRowHeight = 0;
double totalWidth = 0;
double totalHeight = 0;
for (int n = 0; n < _layout.Count; n++)
{
var child = _layout[n];
if (child.Visibility == Visibility.Collapsed)
{
continue;
}
var measure = child.Measure(double.PositiveInfinity, heightConstraint);
// Will adding this IView put us past the edge?
if (currentRowWidth + measure.Width > widthConstraint)
{
// Keep track of the width so far
totalWidth = Math.Max(totalWidth, currentRowWidth);
totalHeight += currentRowHeight;
// Account for spacing
totalHeight += _layout.Spacing;
// Start over at 0
currentRowWidth = 0;
currentRowHeight = measure.Height;
}
currentRowWidth += measure.Width;
currentRowHeight = Math.Max(currentRowHeight, measure.Height);
if (n < _layout.Count - 1)
{
currentRowWidth += _layout.Spacing;
}
}
// Account for the last row
totalWidth = Math.Max(totalWidth, currentRowWidth);
totalHeight += currentRowHeight;
// Account for padding
totalWidth += padding.HorizontalThickness;
totalHeight += padding.VerticalThickness;
// Ensure that the total size of the layout fits within its constraints
var finalWidth = ResolveConstraints(widthConstraint, Stack.Width, totalWidth, Stack.MinimumWidth, Stack.MaximumWidth);
var finalHeight = ResolveConstraints(heightConstraint, Stack.Height, totalHeight, Stack.MinimumHeight, Stack.MaximumHeight);
return new Size(finalWidth, finalHeight);
}
方法 Measure
會列舉配置中所有可見的子系,並叫用 IView.Measure 每個子系上的方法。 然後,它會傳回配置的總大小,並考慮 和屬性的條件約束和Spacing值Padding。 呼叫 ResolveConstraints 方法,以確保配置的總大小符合其條件約束。
重要
在實作 ILayoutManager.Measure 中列舉子系時,請略過屬性 Visibility 設定為 Collapsed的任何子系。 這可確保自定義版面配置不會保留隱藏子系的空間。
在版面配置中排列子系
實作 ArrangeChildren 的目的是要調整配置中所有子系的大小和位置。 若要判斷每個子系應該放在配置界限內的位置,它應該在每個子系上呼叫 Arrange 其適當的界限。 然後,它應該會傳回值,代表配置的實際大小。
警告
無法在配置中的每個子系上叫 ArrangeChildren 用 方法,會導致子系永遠不會收到正確的大小或位置,因此子系不會顯示在頁面上。
下列範例顯示 ArrangeChildren 類別的 HorizontalWrapLayoutManager
實作:
public override Size ArrangeChildren(Rect bounds)
{
var padding = Stack.Padding;
double top = padding.Top + bounds.Top;
double left = padding.Left + bounds.Left;
double currentRowTop = top;
double currentX = left;
double currentRowHeight = 0;
double maxStackWidth = currentX;
for (int n = 0; n < _layout.Count; n++)
{
var child = _layout[n];
if (child.Visibility == Visibility.Collapsed)
{
continue;
}
if (currentX + child.DesiredSize.Width > bounds.Right)
{
// Keep track of our maximum width so far
maxStackWidth = Math.Max(maxStackWidth, currentX);
// Move down to the next row
currentX = left;
currentRowTop += currentRowHeight + _layout.Spacing;
currentRowHeight = 0;
}
var destination = new Rect(currentX, currentRowTop, child.DesiredSize.Width, child.DesiredSize.Height);
child.Arrange(destination);
currentX += destination.Width + _layout.Spacing;
currentRowHeight = Math.Max(currentRowHeight, destination.Height);
}
var actual = new Size(maxStackWidth, currentRowTop + currentRowHeight);
// Adjust the size if the layout is set to fill its container
return actual.AdjustForFill(bounds, Stack);
}
方法 ArrangeChildren
會列舉版面配置中所有可見的子系,以調整其大小,並將其放置在版面配置中。 它會藉由 Arrange 叫用每個具有適當界限的子系,以考慮 Padding 基礎配置的 和 Spacing 來執行此工作。 然後,它會傳回版面配置的實際大小。 呼叫 AdjustForFill 方法,以確保大小會考慮設定是否將其 HorizontalLayoutAlignment 和 VerticalLayoutAlignment 屬性設定為 LayoutOptions.Fill。
重要
在實作 ArrangeChildren 中列舉子系時,請略過屬性 Visibility 設定為 Collapsed的任何子系。 這可確保自定義版面配置不會保留隱藏子系的空間。
取用版面配置類型
類別 HorizontalWrapLayout
可以藉由將類別放在衍生類型中 Page 來取用:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:layouts="clr-namespace:CustomLayoutDemos.Layouts"
x:Class="CustomLayoutDemos.Views.HorizontalWrapLayoutPage"
Title="Horizontal wrap layout">
<ScrollView Margin="20">
<layouts:HorizontalWrapLayout Spacing="20">
<Image Source="img_0074.jpg"
WidthRequest="150" />
<Image Source="img_0078.jpg"
WidthRequest="150" />
<Image Source="img_0308.jpg"
WidthRequest="150" />
<Image Source="img_0437.jpg"
WidthRequest="150" />
<Image Source="img_0475.jpg"
WidthRequest="150" />
<Image Source="img_0613.jpg"
WidthRequest="150" />
<!-- More images go here -->
</layouts:HorizontalWrapLayout>
</ScrollView>
</ContentPage>
控制項可以視需要新增至 HorizontalWrapLayout
。 在此範例中,當包含 HorizontalWrapLayout
的頁面出現時, Image 會顯示控件:
每個數據列的數據行數目取決於影像大小、頁面寬度,以及每個裝置獨立單位的像素數目:
注意
將 包裝 HorizontalWrapLayout
在 中 ScrollView,支援卷動。
修改現有版面配置的行為
在某些情況下,您可能想要變更現有版面配置類型的行為,而不需要建立自定義版面配置類型。 在這些案例中,您可以建立實作 ILayoutManagerFactory 的型別,並將其用來將現有版面配置的默認版面配置管理員取代為您自己的 ILayoutManager 實作。 這可讓您為現有的版面配置定義新的版面配置管理員,例如提供的 Grid自定義版面配置管理員。 這適用於您想要將新行為新增至版面配置,但不想更新應用程式中現有廣泛使用配置類型的案例。
使用版面設定管理員處理站來修改現有版面配置行為的程式為:
- 建立衍生自其中一個 .NET MAUI 版面配置管理員類型的版面配置管理員。 如需詳細資訊,請參閱 建立自定義版面配置管理員。
- 建立實作的型別 ILayoutManagerFactory。 如需詳細資訊,請參閱 建立版面配置管理員處理站。
- 向應用程式的服務提供者註冊您的版面配置管理員處理站。 如需詳細資訊,請參閱 註冊版面配置管理員處理站。
建立自定義版面配置管理員
版面配置管理員可用來執行版面配置的跨平臺配置和度量。 若要變更現有版面配置的行為,您應該建立衍生自版面配置管理員的自定義版面配置管理員:
using Microsoft.Maui.Layouts;
public class CustomGridLayoutManager : GridLayoutManager
{
public CustomGridLayoutManager(IGridLayout layout) : base(layout)
{
}
public override Size Measure(double widthConstraint, double heightConstraint)
{
EnsureRows();
return base.Measure(widthConstraint, heightConstraint);
}
void EnsureRows()
{
if (Grid is not Grid grid)
{
return;
}
// Find the maximum row value from the child views
int maxRow = 0;
foreach (var child in grid)
{
maxRow = Math.Max(grid.GetRow(child), maxRow);
}
// Add more rows if we need them
for (int n = grid.RowDefinitions.Count; n <= maxRow; n++)
{
grid.RowDefinitions.Add(new RowDefinition(GridLength.Star));
}
}
}
在此範例中, CustomGridLayoutManager
衍生自 .NET MAUI 的 GridLayoutManager 類別,並覆寫其 Measure 方法。 此自定義版面配置管理員可確保 在運行時間 RowDefinitions ,針對 Grid 包含足夠的數據列來考慮子檢視中設定的每個 Grid.Row
附加屬性。 若未進行這項修改, RowDefinitions 則必須在設計時間指定 的 Grid 。
重要
修改現有版面配置管理員的行為時,別忘了確定您從實Measure作呼叫 base.Measure
方法。
建立版面配置管理員處理站
應在版面配置管理員處理站中建立自定義版面配置管理員。 這是藉由建立實作 介面的類型 ILayoutManagerFactory 來達成此目的:
using Microsoft.Maui.Layouts;
public class CustomLayoutManagerFactory : ILayoutManagerFactory
{
public ILayoutManager CreateLayoutManager(Layout layout)
{
if (layout is Grid)
{
return new CustomGridLayoutManager(layout as IGridLayout);
}
return null;
}
}
在此範例中, CustomGridLayoutManager
如果版面配置是 Grid,則會傳回 實例。
註冊版面配置管理員處理站
設定管理員處理站應該在類別中 MauiProgram
向您的應用程式服務提供者註冊:
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});
// Setup a custom layout manager so the default manager for the Grid can be replaced.
builder.Services.Add(new ServiceDescriptor(typeof(ILayoutManagerFactory), new CustomLayoutManagerFactory()));
return builder.Build();
}
}
然後,當應用程式轉譯時 Grid ,它會使用自定義版面配置管理員,以確保 在運行時間 RowDefinitions , Grid 針對 包含足夠的數據列來考慮子檢視中設定的每個 Grid.Row
附加屬性。
下列範例顯示 , Grid 會在子檢視中設定 Grid.Row
附加屬性,但未設定 RowDefinitions 屬性:
<Grid>
<Label Text="This Grid demonstrates replacing the LayoutManager for an existing layout type." />
<Label Grid.Row="1"
Text="In this case, it's a LayoutManager for Grid which automatically adds enough rows to accommodate the rows specified in the child views' attached properties." />
<Label Grid.Row="2"
Text="Notice that the Grid doesn't explicitly specify a RowDefinitions collection." />
<Label Grid.Row="3"
Text="In MauiProgram.cs, an instance of an ILayoutManagerFactory has been added that replaces the default GridLayoutManager. The custom manager will automatically add the necessary RowDefinitions at runtime." />
<Label Grid.Row="5"
Text="We can even skip some rows, and it will add the intervening ones for us (notice the gap between the previous label and this one)." />
</Grid>
設定管理員處理站會使用自定義版面配置管理員,以確保 Grid 此範例中的 顯示正確,儘管 RowDefinitions 未設定 屬性: