Пользовательские макеты
Пользовательский интерфейс многоплатформенного приложения .NET (.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 вызовом, так как платформа может потребоваться выполнить некоторые спекулятивные измерения перед упорядочением представлений.
Подходы к пользовательскому макету
Существует два основных подхода к созданию пользовательского макета:
- Создайте пользовательский тип макета, который обычно является подклассом существующего типа макета или LayoutCreateLayoutManager() в пользовательском типе макета. Затем предоставьте реализацию, содержащую логику 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 методы ArrangeChildren из HorizontalStackLayoutManager класса. Эти методы определяют логику для реализации пользовательского макета.
Измерение размера макета
Цель 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(Double, Double) перечисляет все видимые дочерние элементы в макете, вызывая метод для каждого дочернего IView.Measure элемента. Затем он возвращает общий размер макета с учетом ограничений и значений Padding свойств.Spacing Метод 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 и использовать его для замены диспетчера макетов по умолчанию .NET MAUI для существующего макета собственной 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
от класса MAUI GridLayoutManager .NET и переопределяет его Measure метод. Этот пользовательский диспетчер макетов гарантирует, что во время выполнения RowDefinitions Grid содержит достаточно строк для учета каждого Grid.Row
присоединенного свойства, заданного в дочернем представлении. Без этого изменения RowDefinitions необходимо указать значение Grid во время разработки.
Внимание
При изменении поведения существующего диспетчера макетов не забудьте убедиться, что метод вызывается base.Measure
из реализации 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.Row
присоединенное Grid свойство в дочерних представлениях, но не задать 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>
Фабрика диспетчера макетов использует пользовательский диспетчер макетов, чтобы убедиться, что в этом примере отображается правильно, несмотря на RowDefinitions то, что Grid свойство не задано: