Layouts personalizados

Procurar amostra. Procurar no exemplo

A interface do usuário do .NET MAUI (.NET Multi-Platform App UI) define várias classes de layouts que organizam os respectivos filhos de uma maneira diferente. Um layout pode ser pensado como uma lista de exibições com regras e propriedades que definem como organizar essas exibições dentro do layout. Exemplos de layouts incluem Grid, AbsoluteLayout e VerticalStackLayout.

As classes de layout do .NET MAUI derivam da classe abstrata Layout. Essa classe delega o layout e a medição entre plataformas a uma classe de gerenciador de layout. A classe Layout também contém um método substituível CreateLayoutManager() que layouts derivados podem usar para especificar o gerenciador de layout.

Cada classe do gerenciador de layout implementa a interface ILayoutManager, que especifica que as implementações Measure e ArrangeChildren precisam ser fornecidas:

  • A implementação Measure chama IView.Measure em cada exibição no layout e retorna o tamanho total do layout, dadas as delimitações.
  • A implementação ArrangeChildren determina onde cada exibição deve ser colocada dentro dos limites do layout e chama Arrange em cada exibição com seus limites apropriados. O valor retornado é o tamanho real do layout.

Os layouts do .NET MAUI têm gerenciadores de layout predefinidos para lidar com o layout deles. No entanto, às vezes é necessário organizar o conteúdo da página usando um layout que não é fornecido pelo .NET MAUI. Isso pode ser alcançado produzindo um layout personalizado próprio, o que requer que você tenha uma compreensão de como o processo de layout multiplataforma do .NET MAUI funciona.

Processo de layout

O processo de layout multiplataforma do .NET MAUI se baseia no processo de layout nativo em cada plataforma. Geralmente, o processo de layout é iniciado pelo sistema de layout nativo. O processo multiplataforma é executado quando um layout ou controle de conteúdo o inicia como resultado de ser medido ou organizado pelo sistema de layout nativo.

Observação

Cada plataforma lida com o layout de maneira ligeiramente diferente. No entanto, o processo de layout multiplataforma do .NET MAUI visa ser o mais independente de plataforma possível.

O diagrama a seguir mostra o processo quando um sistema de layout nativo inicia a medição de layout:

O processo de medição de layout no .NET MAUI

Todos os layouts do .NET MAUI têm apenas uma exibição de suporte em cada plataforma:

  • No Android, essa exibição de suporte é LayoutViewGroup.
  • No iOS e no Mac Catalyst, essa exibição de suporte é LayoutView.
  • No Windows, essa modo de exibição de suporte é LayoutPanel.

Quando o sistema de layout nativo de uma plataforma solicita a medição de uma dessas exibições de suporte, a exibição de suporte chama o método Layout.CrossPlatformMeasure. Este é o ponto em que o controle é passado do sistema de layout nativo para o sistema de layout do .NET MAUI. Layout.CrossPlatformMeasure chama o método Measure dos gerenciadores de layout. Esse método é responsável por medir exibições filho chamando IView.Measure em cada exibição no layout. A exibição mede o controle nativo dela e atualiza a propriedade DesiredSize dela com base nessa medição. Esse valor é retornado para a exibição de suporte como resultado do método CrossPlatformMeasure. A exibição de suporte executa qualquer processamento interno que precise fazer e retorna o tamanho medido dela para a plataforma.

O seguinte diagrama mostra o processo quando um sistema de layout nativo inicia a medição de layout:

O processo de organização de layout no .NET MAUI

Quando o sistema de layout nativo de uma plataforma solicita a disposição ou layout de uma dessas exibições de suporte, a exibição de suporte chama o método Layout.CrossPlatformArrange. Este é o ponto em que o controle é passado do sistema de layout nativo para o sistema de layout do .NET MAUI. Layout.CrossPlatformArrange chama o método ArrangeChildren dos gerenciadores de layout. Esse método é responsável por determinar onde cada exibição deve ser colocada dentro dos limites do layout e chama Arrange em cada exibição para definir o local dela. O tamanho do layout é retornado para a exibição de suporte como resultado do método CrossPlatformArrange. A exibição de suporte executa qualquer processamento interno que precise fazer e retorna o tamanho real para a plataforma.

Observação

ILayoutManager.Measure pode ser chamado várias vezes antes de ArrangeChildren ser chamado, porque uma plataforma pode precisar realizar algumas medições especulativas antes de organizar exibições.

Abordagens de layout personalizado

Há duas abordagens principais para criar um layout personalizado:

  1. Crie um tipo de layout personalizado, que geralmente é uma subclasse de um tipo de layout existente ou de Layout, e substitua CreateLayoutManager() no seu tipo de layout personalizado. Em seguida, forneça uma implementação de ILayoutManager que contenha sua lógica de layout personalizado. Para obter mais informações, confira Criar um tipo de layout personalizado.
  2. Modifique o comportamento de um tipo de layout existente criando um tipo que implemente ILayoutManagerFactory. Em seguida, use essa fábrica do gerenciador de layout para substituir o gerenciador de layout padrão do .NET MAUI para o layout existente com sua implementação de ILayoutManager que contém sua lógica de layout personalizado. Para obter mais informações, consulte Modificar o comportamento de um layout existente.

Criar um tipo de layout personalizado

O processo para criar um tipo de layout personalizado é:

  1. Crie uma classe que seja uma subclasse de um tipo de layout existente ou a classe Layout e substitua CreateLayoutManager() em seu tipo de layout personalizado. Para obter mais informações, consulte Criar uma subclasse de um layout.

  2. Crie uma classe de gerenciador de layout que derive de um gerenciador de layout existente ou que implemente a interface ILayoutManager diretamente. Em sua classe de gerenciador de layout, você deve:

    1. Substitua ou implemente o método Measure para calcular o tamanho total do layout, dadas as delimitações dele.
    2. Substitua ou implemente o método ArrangeChildren para dimensionar e posicionar todos os filhos dentro do layout.

    Para obter mais informações, confira Criar um gerenciador de layout.

  3. Consuma seu tipo de layout personalizado adicionando-o a um Pagee adicionando filhos ao layout. Para obter mais informações, confira Consumir o tipo de layout.

Um HorizontalWrapLayout que diferencia a orientação é usado para demonstrar esse processo. HorizontalWrapLayout é semelhante a um HorizontalStackLayout, na medida em que organiza os filhos dele horizontalmente em toda a página. No entanto, ele quebra a exibição dos filhos para uma nova linha quando encontra a borda direita do contêiner dele

Observação

O exemplo define layouts personalizados adicionais que podem ser usados para entender como produzir um layout personalizado.

Criar uma subclasse de um layout

Para criar um tipo de layout personalizado, você precisa primeiro criar uma subclasse de um tipo de layout existente ou da classe Layout. Em seguida, substitua CreateLayoutManager() no seu tipo de layout e retorne uma nova instância do gerenciador de layout para o seu tipo de layout:

using Microsoft.Maui.Layouts;

public class HorizontalWrapLayout : HorizontalStackLayout
{
    protected override ILayoutManager CreateLayoutManager()
    {
        return new HorizontalWrapLayoutManager(this);
    }
}

HorizontalWrapLayout deriva de HorizontalStackLayout para usar a respectiva funcionalidade de layout. Os layouts do .NET MAUI delegam o layout e a medição multiplataforma a uma classe de gerenciador de layout. Assim, a substituição CreateLayoutManager() retorna uma nova instância da classe HorizontalWrapLayoutManager, que é o gerenciador de layout discutido na próxima seção.

Criar um gerenciador de layout

Uma classe de gerenciador de layout é usada para executar o layout e a medição multiplataforma para seu tipo de layout personalizado. Ela deve derivar de um gerenciador de layout existente ou deve implementar diretamente a interface ILayoutManager. HorizontalWrapLayoutManager deriva de HorizontalStackLayoutManager para que ele possa usar a própria funcionalidade subjacente e acessar membros na hierarquia de herança dele:

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)
    {
    }
}

O construtor HorizontalWrapLayoutManager armazena uma instância do tipo HorizontalWrapLayout em um campo, para que ele possa ser acessado em todo o gerenciador de layout. O gerenciador de layout também substitui os métodos Measure e ArrangeChildren da classe HorizontalStackLayoutManager. Esses métodos são onde você definirá a lógica para implementar seu layout personalizado.

Medir o tamanho do layout

O objetivo da implementação de ILayoutManager.Measure é calcular o tamanho total do layout. Ele deve fazer isso chamando IView.Measure em cada filho no layout. Em seguida, ele deve usar esses dados para calcular e retornar o tamanho total do layout, dadas as delimitações dele.

O seguinte exemplo mostra a implementação Measure da classe 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);
}

O método Measure(Double, Double) enumera todos os filhos visíveis no layout, invocando o método IView.Measure em cada filho. Em seguida, ele retorna o tamanho total do layout, levando em conta as delimitações e os valores das propriedades Padding e Spacing. O método ResolveConstraints é chamado para garantir que o tamanho total do layout esteja dentro das delimitações dele.

Importante

Ao enumerar filhos na implementação ILayoutManager.Measure, ignore qualquer filho cuja propriedade Visibility esteja definida como Collapsed. Isso garante que o layout personalizado não deixe espaço para filhos invisíveis.

Organizar filhos no layout

O objetivo da implementação ArrangeChildren é dimensionar e posicionar todos os filhos dentro do layout. Para determinar onde cada filho deve ser colocado dentro dos limites do layout, ele deve chamar Arrange em cada filho com os limites apropriados dele. Em seguida, ele deve retornar um valor que representa o tamanho real do layout.

Aviso

A falha em invocar o método ArrangeChildren em cada filho no layout fará com que o filho nunca receba um tamanho ou posição correta e, portanto, o filho não ficará visível na página.

O seguinte exemplo mostra a implementação ArrangeChildren da classe 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);
}

O método ArrangeChildren enumera todos os filhos visíveis no layout para dimensioná-los e posicioná-los dentro do layout. Ele faz isso invocando Arrange em cada filho com limites apropriados, que levam em conta o Padding e o Spacing do layout subjacente. Em seguida, ele retorna o tamanho real do layout. O método AdjustForFill é chamado para garantir que o tamanho leve em conta se o layout tem as propriedades HorizontalLayoutAlignment e VerticalLayoutAlignment dele definidas como LayoutOptions.Fill.

Importante

Ao enumerar filhos na implementação ArrangeChildren, ignore qualquer filho cuja propriedade Visibility esteja definida como Collapsed. Isso garante que o layout personalizado não deixe espaço para filhos invisíveis.

Consumir o tipo de layout

A classe HorizontalWrapLayout pode ser consumida colocando-a em um tipo derivado 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>

Os controles podem ser adicionados ao HorizontalWrapLayout conforme necessário. Neste exemplo, quando a página que contém o HorizontalWrapLayout é exibida, os controles Image são exibidos:

Captura de tela do layout de quebra horizontal em um Mac com duas colunas.

O número de colunas em cada linha depende do tamanho da imagem, da largura da página e do número de pixels por unidade independente do dispositivo:

Captura de tela do layout de quebra horizontal em um Mac com cinco colunas.

Observação

O suporte à rolagem é fornecido encapsulando o HorizontalWrapLayout em um ScrollView.

Modificar o comportamento de um layout existente

Em alguns cenários, convém alterar o comportamento de um tipo de layout existente sem precisar criar um tipo de layout personalizado. Para esses cenários, você pode criar um tipo que implementa ILayoutManagerFactory e usá-lo para substituir o gerenciador de layout padrão do .NET MAUI para o layout existente com a sua implementação de ILayoutManager. Isso permite que você defina um novo gerenciador de layout para um layout existente, como fornecer um gerenciador de layout personalizado para Grid. Isso pode ser útil para cenários em que você deseja adicionar um novo comportamento a um layout, mas não deseja atualizar o tipo de um layout existente amplamente usado em seu aplicativo.

O processo para modificar o comportamento de um layout existente, com uma fábrica do gerenciador de layout, é:

  1. Crie um gerenciador de layout que derive de um dos tipos de gerenciador de layout do .NET MAUI. Para obter mais informações, confira Criar um gerenciador de layout personalizado.
  2. Crie um tipo que implementa ILayoutManagerFactory. Para obter mais informações, confira Criar uma fábrica de gerenciador de layout.
  3. Registre sua fábrica do gerenciador de layout com o provedor de serviços do aplicativo. Para obter mais informações, confira Registrar a sua fábrica do gerenciador de layout.

Criar um gerenciador de layout personalizado

Um gerenciador de layout é usado para executar o layout e a medição multiplataforma para um layout. Para alterar o comportamento de um layout existente, você deve criar um gerenciador de layout personalizado que deriva do gerenciador de layout para o layout:

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));
        }
    }
}

Neste exemplo, CustomGridLayoutManager deriva da classe GridLayoutManager do .NET MAUI e substitui o método Measure dela. Esse gerenciador de layout personalizado garante que, em runtime, o RowDefinitions para o Grid inclua linhas suficientes para contabilizar cada conjunto de propriedades anexadas Grid.Row em uma exibição filho. Sem essa modificação, o RowDefinitions para o Grid precisaria ser especificado em tempo de design.

Importante

Ao modificar o comportamento de um gerenciador de layout existente, não se esqueça de chamar o método base.Measure de sua implementação Measure.

Criar uma fábrica de gerenciador de layout

O gerenciador de layout personalizado deve ser criado em uma fábrica de gerenciador de layout. Isso é realizado criando um tipo que implementa a interface 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;
    }
}

Neste exemplo, uma instância de CustomGridLayoutManager será retornada se o layout for um Grid.

Registrar a fábrica do gerenciador de layout

A fábrica do gerenciador de layout deve ser registrada com o provedor de serviços do seu aplicativo em sua classe 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();
    }
}

Em seguida, quando o aplicativo renderizar um Grid, ele usará o gerenciador de layout personalizado para garantir que, em tempo de execução, o RowDefinitions para o Grid inclua linhas suficientes para contabilizar cada conjunto de propriedades anexadas Grid.Row em exibições filho.

O seguinte exemplo mostra um Grid que define a propriedade anexada Grid.Row em exibições filho, mas não define a propriedade 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>

A fábrica do gerenciador de layout usa o gerenciador de layout personalizado para garantir que o Grid neste exemplo seja exibido corretamente, apesar de a propriedade RowDefinitions não estar definida:

Captura de tela de uma Grade personalizada usando uma fábrica do gerenciador de layout.