Compartir a través de


Diseños personalizados

Examinar ejemplo. Examinar el ejemplo

.NET Multi-Platform App UI (.NET MAUI) define varias clases de diseños en las que cada una organiza sus elementos secundarios de una manera diferente. Un diseño se puede considerar como una lista de vistas con reglas y propiedades que definen cómo organizar esas vistas dentro del diseño. Entre los ejemplos de diseño se pueden incluir Grid, AbsoluteLayout y VerticalStackLayout.

Las clases de diseño de .NET MAUI se derivan de la clase abstracta Layout. Esta clase delega el diseño y la medición multiplataforma en una clase de administrador de diseño. La clase Layout también contiene un método reemplazable CreateLayoutManager() que los diseños derivados pueden usar para especificar el administrador de diseño.

Cada clase del administrador de diseño implementa la interfaz ILayoutManager, que especifica que se deben proporcionar las implementaciones Measure y ArrangeChildren:

  • La implementación de Measure llama a IView.Measure en cada vista del diseño y devuelve el tamaño total de este según las restricciones.
  • La implementación de ArrangeChildren determina dónde se debe colocar cada vista dentro de los límites del diseño y llama a Arrange en cada vista con sus límites adecuados. El valor devuelto es el tamaño real del diseño.

Los diseños de .NET MAUI tienen administradores de diseño predefinidos para controlar su diseño. Sin embargo, a veces es necesario organizar el contenido de la página mediante un diseño que no proporciona .NET MAUI. Esto se puede lograr mediante la generación de su propio diseño personalizado, lo cual requiere que tenga conocimientos sobre cómo funciona el proceso de diseño multiplataforma de .NET MAUI.

Proceso de diseño

El proceso de diseño multiplataforma de .NET MAUI se basa en el proceso de diseño nativo de cada plataforma. Por lo general, el sistema de diseño nativo inicia el proceso de diseño. El proceso multiplataforma se ejecuta cuando un diseño o control de contenido lo inicia como resultado de su medición u organización por parte del sistema de diseño nativo.

Nota:

Cada plataforma controla el diseño de manera ligeramente diferente. Sin embargo, el proceso de diseño multiplataforma de .NET MAUI pretende ser lo más independiente de la plataforma posible.

En el diagrama siguiente se muestra el proceso que se sigue cuando un sistema de diseño nativo inicia la medición del diseño:

Proceso de medición de diseño en .NET MAUI

Todos los diseños de .NET MAUI tienen una sola vista de respaldo en cada plataforma:

  • En Android, esta vista de respaldo es LayoutViewGroup.
  • En iOS y Mac Catalyst, la vista de respaldo es LayoutView.
  • En Windows, es LayoutPanel.

Cuando el sistema de diseño nativo de una plataforma solicita la medición de una de estas vistas de respaldo, esta llama al método Layout.CrossPlatformMeasure. Este es el punto en el que se pasa el control desde el sistema de diseño nativo al sistema de diseño de .NET MAUI. Layout.CrossPlatformMeasure llama al método Measure de los administradores de diseño. Este método es responsable de medir las vistas secundarias llamando a IView.Measure en cada vista del diseño. La vista mide su control nativo y actualiza su propiedad DesiredSize en función de esa medida. Este valor se devuelve a la vista de respaldo como resultado del método CrossPlatformMeasure. La vista de respaldo realiza el procesamiento interno que tiene que hacer y devuelve su tamaño medido a la plataforma.

En el diagrama siguiente se muestra el proceso que se sigue cuando un sistema de diseño nativo inicia la organización del diseño:

El proceso para la disposición del diseño en .NET MAUI

Cuando el sistema de diseño nativo de una plataforma solicita la organización, o diseño, de una de estas vistas de respaldo, esta llama al método Layout.CrossPlatformArrange. Este es el punto en el que se pasa el control desde el sistema de diseño nativo al sistema de diseño de .NET MAUI. Layout.CrossPlatformArrange llama al método ArrangeChildren de los administradores de diseño. Este método es responsable de determinar dónde se debe colocar cada vista dentro de los límites del diseño y llama a Arrange en cada vista para establecer su ubicación. El tamaño del diseño se devuelve a la vista de respaldo como resultado del método CrossPlatformArrange. La vista de respaldo realiza el procesamiento interno que tiene que hacer y devuelve su tamaño real a la plataforma.

Nota:

Se puede llamar a ILayoutManager.Measure varias veces antes de llamar a ArrangeChildren, ya que es posible que una plataforma tenga que realizar algunas mediciones especulativas antes de organizar las vistas.

Enfoques de diseño personalizado

Hay dos enfoques principales para crear un diseño personalizado:

  1. Cree un tipo de diseño personalizado, el cual suele ser una subclase de un tipo de diseño existente o de Layout, e invalide CreateLayoutManager() en el tipo de diseño personalizado. A continuación, proporcione una implementación de ILayoutManager que contenga la lógica de diseño personalizada. Para más información, consulte Creación de un tipo de diseño personalizado.
  2. Modifique el comportamiento de un tipo de diseño existente mediante la creación de un tipo que implemente ILayoutManagerFactory. A continuación, use este generador del administrador de diseños para reemplazar el administrador de diseño predeterminado de .NET MAUI para el diseño existente por su propia implementación de ILayoutManager que contenga la lógica de diseño personalizada. Para más información, consulte Modificación del comportamiento de un diseño existente.

Creación de un tipo de diseño personalizado

El proceso para crear un tipo de diseño personalizado consiste en:

  1. Crear una clase que sea una subclase de un tipo de diseño existente o la clase Layout e invalidar CreateLayoutManager() en el tipo de diseño personalizado. Para más información, consulte Subclase de un diseño.

  2. Crear una clase de administrador de diseño que se derive de un administrador de diseño existente o que implemente la interfaz ILayoutManager directamente. En la clase del administrador de diseño, debe:

    1. Invalidar, o implementar, el método Measure para calcular el tamaño total del diseño según sus restricciones.
    2. Invalidar, o implementar, el método ArrangeChildren para ajustar el tamaño y colocar todos los elementos secundarios dentro del diseño.

    Para más información, consulte Creación de un administrador de diseño.

  3. Para usar el tipo de diseño personalizado, agréguelo a Page y agregue los elementos secundarios al diseño. Para más información, consulte Uso del tipo de diseño.

Se usa un HorizontalWrapLayout con distinción de orientación para mostrar este proceso. HorizontalWrapLayout se parece a HorizontalStackLayout por el hecho de que organiza sus elementos secundarios horizontalmente en la página. Sin embargo, ajusta la presentación de elementos secundarios en una nueva fila cuando encuentra el borde correcto de su contenedor.

Nota:

En el ejemplo se definen diseños personalizados adicionales que se pueden usar para comprender cómo producir un diseño personalizado.

Subclase de un diseño

Para crear un tipo de diseño personalizado, primero debe crear una subclase de un tipo de diseño existente o de la clase Layout. A continuación, invalide CreateLayoutManager() en el tipo de diseño y devuelva una nueva instancia del administrador de diseño para el tipo de diseño:

using Microsoft.Maui.Layouts;

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

HorizontalWrapLayout se deriva de HorizontalStackLayout para usar su funcionalidad de diseño. Los diseños de .NET MAUI delegan el diseño y la medición multiplataforma en una clase de administrador de diseño. Por lo tanto, la invalidación de CreateLayoutManager() devuelve una nueva instancia de la clase HorizontalWrapLayoutManager, que es el administrador de diseño que se describe en la sección siguiente.

Creación de un administrador de diseño

Una clase de administrador de diseño se usa para realizar el diseño y la medición multiplataforma del tipo de diseño personalizado. Debe derivarse de un administrador de diseño existente o debe implementar directamente la interfaz ILayoutManager. HorizontalWrapLayoutManager se deriva de HorizontalStackLayoutManager para que pueda usar su funcionalidad subyacente y acceder a los miembros en su jerarquía de herencia:

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

El constructor HorizontalWrapLayoutManager almacena una instancia del tipo HorizontalWrapLayout en un campo, de modo que se pueda acceder a ella en todo el administrador de diseño. El administrador de diseño también invalida los métodos Measure y ArrangeChildren de la clase HorizontalStackLayoutManager. En estos métodos es donde definirá la lógica para implementar el diseño personalizado.

Medición del tamaño del diseño

El propósito de la implementación ILayoutManager.Measure es calcular el tamaño total del diseño. Para ello, debe llamar a IView.Measure en cada elemento secundario del diseño. A continuación, debe usar estos datos para calcular y devolver el tamaño total del diseño según sus restricciones.

En el siguiente ejemplo se muestra la implementación de Measure de la clase 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);
}

El método Measure(Double, Double) enumera todos los elementos secundarios visibles del diseño, invocando el método IView.Measure en cada elemento secundario. A continuación, devuelve el tamaño total del diseño, teniendo en cuenta las restricciones y valores de las propiedades Padding y Spacing. Se llama al método ResolveConstraints para asegurarse de que el tamaño total del diseño se ajusta a sus restricciones.

Importante

Al enumerar elementos secundarios en la implementación de ILayoutManager.Measure, omita cualquier elemento secundario cuya propiedad Visibility esté establecida en Collapsed. Esto garantiza que el diseño personalizado no dejará espacio para elementos secundarios invisibles.

Organización de elementos secundarios en el diseño

El propósito de la implementación de ArrangeChildren es ajustar el tamaño y colocar todos los elementos secundarios dentro del diseño. Para determinar dónde se debe colocar cada elemento secundario dentro de los límites del diseño, debe llamar a Arrange en cada elemento secundario con sus límites adecuados. A continuación, debe devolver un valor que represente el tamaño real del diseño.

Advertencia

Si no se invoca el método ArrangeChildren en cada elemento secundario del diseño, el elemento secundario no recibirá nunca un tamaño o posición correctos y, por tanto, el elemento secundario no estará visible en la página.

En el siguiente ejemplo se muestra la implementación de ArrangeChildren de la clase 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);
}

El método ArrangeChildren enumera todos los elementos secundarios visibles del diseño para ajustarlos y colocarlos dentro del diseño. Para ello, invoca a Arrange en cada elemento secundario con límites adecuados que tienen en cuenta los valores de Padding y Spacing del diseño subyacente. A continuación, devuelve el tamaño real del diseño. Se llama al método AdjustForFill para asegurarse de que el tamaño tenga en cuenta que el diseño tiene sus propiedades HorizontalLayoutAlignment y VerticalLayoutAlignment establecidas en LayoutOptions.Fill.

Importante

Al enumerar elementos secundarios en la implementación de ArrangeChildren, omita cualquier elemento secundario cuya propiedad Visibility esté establecida en Collapsed. Esto garantiza que el diseño personalizado no dejará espacio para elementos secundarios invisibles.

Uso del tipo de diseño

Para usar la clase HorizontalWrapLayout, colóquela en un tipo derivado de 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>

Los controles se pueden agregar a HorizontalWrapLayout según sea necesario. En este ejemplo, cuando aparece la página que contiene HorizontalWrapLayout, se muestran los controles Image:

Captura de pantalla del diseño de ajuste horizontal en un Equipo Mac con dos columnas.

El número de columnas de cada fila depende del tamaño de la imagen, la anchura de la página y el número de píxeles por unidad independiente del dispositivo:

Captura de pantalla del diseño de ajuste horizontal en un Equipo Mac con cinco columnas.

Nota:

El desplazamiento se admite incluyendo el HorizontalWrapLayout en una ScrollView.

Modificación del comportamiento de un diseño existente

En algunos escenarios, es posible que desee cambiar el comportamiento de un tipo de diseño existente sin tener que crear un tipo de diseño personalizado. En estos escenarios, puede crear un tipo que implemente ILayoutManagerFactory y usarlo para reemplazar el administrador de diseño predeterminado de .NET MAUI para el diseño existente con su propia implementación de ILayoutManager. Esto le permite definir un nuevo administrador de diseño para un diseño existente. Por ejemplo, proporcionar un administrador de diseño personalizado para Grid. Esto puede ser útil para escenarios en los que desea agregar un nuevo comportamiento a un diseño, pero no quiere actualizar el tipo de diseño de mucho uso existente de la aplicación.

El proceso para modificar el comportamiento de un diseño existente, con un generador del administrador de diseños, consiste en:

  1. Crear un administrador de diseño que se derive de uno de los tipos de administrador de diseño de .NET MAUI. Para más información, consulte Creación de un administrador de diseño personalizado.
  2. Crear un tipo que implemente ILayoutManagerFactory. Para más información, consulte Creación de un generador de administrador de diseño.
  3. Registre el generador del administrador de diseño con el proveedor de servicios de la aplicación. Para más información, consulte Registro del generador del administrador de diseño.

Creación de un administrador de diseño personalizado

Un administrador de diseño se usa para realizar el diseño y la medición multiplataforma de un diseño. Para cambiar el comportamiento de un diseño existente, debe crear un administrador de diseño personalizado que se derive del administrador de diseño del diseño:

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

En este ejemplo, CustomGridLayoutManager se deriva de la clase GridLayoutManager de .NET MAUI e invalida su método Measure. Este administrador de diseño personalizado garantiza que, en tiempo de ejecución, los valores de RowDefinitions de Grid incluyen suficientes filas para tener en cuenta cada propiedad Grid.Row adjunta establecida en una vista secundaria. Sin esta modificación, los valores de RowDefinitions de Grid tendrían que especificarse en tiempo de diseño.

Importante

Al modificar el comportamiento de un administrador de diseño existente, no olvide asegurarse de llamar al método base.Measure desde la implementación de Measure.

Creación de un generador de administrador de diseño

El administrador de diseño personalizado debe crearse en un generador de administradores de diseño. Esto se consigue mediante la creación de un tipo que implemente la interfaz 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;
    }
}

En este ejemplo, se devuelve una instancia CustomGridLayoutManager si el diseño es Grid.

Registro del generador del administrador de diseño

El generador del administrador de diseño debe registrarse con el proveedor de servicios de la aplicación en la clase 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();
    }
}

A continuación, cuando la aplicación represente un elemento Grid, usará el administrador de diseño personalizado para asegurarse de que, en tiempo de ejecución, las RowDefinitions de Grid incluyen suficientes filas para tener en cuenta cada propiedad adjunta Grid.Row establecida en las vistas secundarias.

En el ejemplo siguiente se muestra un Grid que establece la propiedad adjunta Grid.Row en las vistas secundarias, pero no establece la propiedad 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>

El generador del administrador de diseño usa el administrador de diseño personalizado para asegurarse de que Grid en este ejemplo se muestra correctamente, a pesar de que la propiedad RowDefinitions no está establecida.

Captura de pantalla de una cuadrícula personalizada mediante un generador del administrador de diseño.