Crear un diseño personalizado en Xamarin.Forms
Xamarin.Forms define cinco clases de diseño: StackLayout, AbsoluteLayout, RelativeLayout, Grid y FlexLayout, y cada uno organiza sus elementos secundarios de una manera diferente. Sin embargo, a veces es necesario organizar el contenido de la página mediante un diseño que no proporciona Xamarin.Forms. En este artículo se explica cómo escribir una clase de diseño personalizada y se muestra una clase de WrapLayout que distingue la orientación y organiza sus elementos secundarios horizontalmente en la página y, a continuación, ajusta la presentación de elementos secundarios posteriores a filas adicionales.
En Xamarin.Forms, todas las clases de diseño derivan de la clase Layout<T>
y restringen el tipo genérico a View
y sus tipos derivados. A su vez, la clase Layout<T>
deriva de la clase Layout
, que proporciona el mecanismo para posicionar y cambiar el tamaño de los elementos secundarios.
Cada elemento visual es responsable de determinar su propio tamaño preferido, que se conoce como el tamaño solicitado. Los tipos derivados Page
, Layout
y Layout<View>
son responsables de determinar la ubicación y el tamaño de sus elementos secundarios en relación con ellos mismos. Por lo tanto, el diseño implica una relación primario-secundario, en la que el elemento primario determina cuál debe ser el tamaño de sus elementos secundarios, pero intentará acomodar el tamaño solicitado del elemento secundario.
Se requiere una comprensión exhaustiva de los ciclos de diseño e invalidación de Xamarin.Forms para crear un diseño personalizado. Ahora se tratarán estos ciclos.
Layout
El diseño comienza en la parte superior del árbol visual con una página y continúa por todas sus ramas para abarcar todos los elementos visuales de una página. Los elementos que son elementos primarios de otros son responsables de dimensionar y posicionar sus elementos secundarios con respecto a ellos mismos.
La clase VisualElement
define un método Measure
que mide un elemento para las operaciones de diseño y un método Layout
que especifica el área rectangular en la que se representará el elemento. Cuando se inicia una aplicación y se muestra la primera página, se muestra un ciclo de diseño que consta primero de llamadas Measure
y, a continuación, de llamadas Layout
, se inicia en el objeto Page
:
- Durante el ciclo de diseño, todos los elementos primarios son responsables de llamar al método
Measure
en sus elementos secundarios. - Una vez medidos los elementos secundarios, cada elemento primario es responsable de llamar al método
Layout
en sus elementos secundarios.
Este ciclo garantiza que todos los elementos visuales de la página reciban llamadas a los métodos Measure
y Layout
. El proceso se muestra en el diagrama siguiente:
Nota:
Tenga en cuenta que los ciclos de diseño también pueden producirse en un subconjunto del árbol visual si hay algún cambio que afecte al diseño. Esto incluye los elementos que se agregan o quitan de una colección como en StackLayout
, un cambio en la propiedad IsVisible
de un elemento o un cambio en el tamaño de un elemento.
Todas las clases Xamarin.Formsque tienen una propiedad Content
o Children
tienen un método reemplazable LayoutChildren
. Las clases de diseño personalizadas que derivan de Layout<View>
deben invalidar este método y asegurarse de que se llama a los métodos Measure
y Layout
en todos los elementos secundarios del elemento, para proporcionar el diseño personalizado deseado.
Además, todas las clases que derivan de Layout
o Layout<View>
deben invalidar el método OnMeasure
, que es donde una clase de diseño determina el tamaño que necesita realizando llamadas a los métodos Measure
de sus elementos secundarios.
Nota:
Los elementos determinan su tamaño en función de las restricciones, lo que indica la cantidad de espacio disponible para un elemento dentro del elemento primario del elemento. Las restricciones que pasaron a los métodos Measure
y OnMeasure
pueden oscilar entre o y Double.PositiveInfinity
. Un elemento está restringido o totalmente restringido cuando recibe una llamada a su método Measure
con argumentos no infinitos. El elemento está restringido a un tamaño determinado. Un elemento no está restringido o está parcialmente restringido cuando recibe una llamada a su método Measure
con al menos un argumento igual a Double.PositiveInfinity
. Se puede considerar que la restricción infinita indica el ajuste automático.
Invalidación
La invalidación es el proceso por el que un cambio en un elemento de una página desencadena un nuevo ciclo de diseño. Los elementos se consideran no válidos cuando ya no tienen el tamaño ni la posición correctos. Por ejemplo, si la propiedad FontSize
de un Button
cambia, se dice que Button
no es válido porque ya no tendrá el tamaño correcto. El cambio de tamaño de Button
puede tener un efecto ondulado de los cambios en el diseño a través del resto de una página.
Los elementos se invalidan invocando el método InvalidateMeasure
, generalmente cuando cambia una propiedad del elemento que podría dar lugar a un nuevo tamaño del elemento. Este método desencadena el evento MeasureInvalidated
, que el elemento primario controla para desencadenar un nuevo ciclo de diseño.
La clase Layout
establece un controlador para el evento MeasureInvalidated
en cada elemento secundario agregado a su propiedad Content
o colección Children
y desasocia el controlador cuando se quita el elemento secundario. Por lo tanto, todos los elementos del árbol visual que tienen elementos secundarios reciben alertas cada vez que uno de sus elementos secundarios cambia el tamaño. En el diagrama siguiente se muestra cómo un cambio en el tamaño de un elemento del árbol visual puede provocar cambios que ondulan el árbol:
Sin embargo, la clase Layout
intenta restringir el impacto de un cambio en el tamaño de un elemento secundario en el diseño de una página. Si el diseño está restringido, un cambio de tamaño en un elemento secundario no afecta a nada mayor que al diseño de un elemento primario en el árbol visual. Sin embargo, normalmente un cambio de tamaño de un diseño afecta a cómo el diseño organiza sus elementos secundarios. Por lo tanto, cualquier cambio en el tamaño de un diseño iniciará un ciclo de diseño para el diseño y el diseño recibirá llamadas a sus métodos OnMeasure
y LayoutChildren
.
La clase Layout
también define un método InvalidateLayout
que tiene un propósito similar al método InvalidateMeasure
. Se debe invocar el método InvalidateLayout
cada vez que se realice un cambio que afecte a la forma en que el diseño coloca y ajusta el tamaño de sus elementos secundarios. Por ejemplo, la clase Layout
invoca el método InvalidateLayout
cada vez que se agrega o quita un elemento secundario de un diseño.
El InvalidateLayout
se puede invalidar para implementar una memoria caché con el fin de minimizar las invocaciones repetitivas de los métodos de Measure
de los elementos secundarios del diseño. Al invalidar el método InvalidateLayout
, se proporcionará una notificación de cuándo se agregan o quitan elementos secundarios del diseño. Del mismo modo, el método OnChildMeasureInvalidated
se puede invalidar para proporcionar una notificación cuando uno de los elementos secundarios del diseño cambia el tamaño. En ambas invalidaciones del método, un diseño personalizado debería responder borrando la memoria caché. Para obtener más información, consulte Cálculo y almacenamiento en caché de datos de diseño.
Crear un diseño personalizado
El proceso para crear un diseño personalizado es el siguiente:
Cree una clase que se derive de la clase
Layout<View>
. Para obtener más información, consulte Crear un WrapLayout.[opcional] Agregue propiedades, guardadas en propiedades enlazables, para los parámetros que se deben establecer en la clase de diseño. Para obtener más información, consulte Add Properties Backed by Bindable Properties (Agregar propiedades con copia en propiedades enlazables).
Invalide el método
OnMeasure
para invocar el métodoMeasure
en todos los elementos secundarios del diseño y devuelva un tamaño solicitado para el diseño. Para obtener más información, consulte Invalidación del método OnMeasure.Invalide el método
LayoutChildren
para invocar el métodoLayout
en todos los elementos secundarios del diseño. Si no se invoca el métodoLayout
en cada elemento secundario de un diseño, el elemento secundario nunca recibirá un tamaño o una posición correctos y, por tanto, el elemento secundario no estará visible en la página. Para obtener más información, consulte Invalidación del método LayoutChildren.Nota:
Al enumerar elementos secundarios en las invalidaciones
OnMeasure
yLayoutChildren
, omita cualquier elemento secundario cuya propiedad deIsVisible
esté establecida enfalse
. Esto garantizará que el diseño personalizado no deje espacio para elementos secundarios invisibles.[opcional] Invalide el método
InvalidateLayout
que se va a notificar cuando se agregan o quitan elementos secundarios del diseño. Para obtener más información, consulte Invalidación del método InvalidateLayout.[opcional] Invalide el método
OnChildMeasureInvalidated
que se va a notificar cuando uno de los elementos secundarios del diseño cambia el tamaño. Para obtener más información, consulte Invalidación del método OnChildMeasureInvalidated.
Nota:
Tenga en cuenta que la invalidación de OnMeasure
no se invocará si el tamaño del diseño se rige por su elemento primario, en lugar de por sus elementos secundarios. Sin embargo, se invocará la invalidación si una o ambas restricciones son infinitas o si la clase de diseño tiene valores de propiedad HorizontalOptions
o VerticalOptions
no predeterminados. Por esta razón, la invalidación de LayoutChildren
no puede basarse en los tamaños secundarios obtenidos durante la llamada al método OnMeasure
. En su lugar, LayoutChildren
debe invocar el método Measure
en los elementos secundarios del diseño, antes de invocar el método Layout
. Como alternativa, el tamaño de los elementos secundarios obtenidos en la invalidación de OnMeasure
se puede almacenar en caché para evitar invocaciones Measure
posteriores en la invalidación LayoutChildren
, pero la clase de diseño deberá saber cuándo deben obtenerse de nuevo los tamaños. Para obtener más información, consulte Cálculo y almacenamiento en caché de datos de diseño.
A continuación, la clase de diseño se puede consumir agregándola a un Page
y agregando elementos secundarios al diseño. Para obtener más información, consulte Consumo de WrapLayout.
Crear un WrapLayout
La aplicación de ejemplo muestra una clase WrapLayout
que diferencia la orientación y organiza sus elementos secundarios horizontalmente en la página y, a continuación, ajusta la presentación de elementos secundarios posteriores a filas adicionales.
La clase WrapLayout
asigna la misma cantidad de espacio para cada elemento secundario, conocido como tamaño de celda, en función del tamaño máximo de los elementos secundarios. Los elementos secundarios menores que el tamaño de celda se pueden colocar dentro de la celda en función de sus valores de propiedad HorizontalOptions
y VerticalOptions
.
La definición de clase WrapLayout
se muestra en el siguiente ejemplo de código:
public class WrapLayout : Layout<View>
{
Dictionary<Size, LayoutData> layoutDataCache = new Dictionary<Size, LayoutData>();
...
}
Cálculo y almacenamiento en caché de datos de diseño
La estructura LayoutData
almacena datos sobre una colección de elementos secundarios en una serie de propiedades:
VisibleChildCount
: el número de elementos secundarios que están visibles en el diseño.CellSize
: el tamaño máximo de todos los elementos secundarios, ajustado al tamaño del diseño.Rows
: el número de filas.Columns
: el número de columnas.
El campo layoutDataCache
se usa para almacenar varios valores LayoutData
. Cuando se inicia la aplicación, dos objetos LayoutData
se almacenarán en caché en el diccionario layoutDataCache
para la orientación actual: uno para los argumentos de restricción a la invalidación de OnMeasure
y otro para los argumentos width
y height
para la invalidación de LayoutChildren
. Al girar el dispositivo en orientación horizontal, se volverán a invocar las invalidaciones de OnMeasure
y LayoutChildren
, lo que hará que otros dos objetos LayoutData
se almacenen en caché en el diccionario. Sin embargo, al devolver el dispositivo a la orientación vertical, no se requieren más cálculos porque layoutDataCache
ya tiene los datos necesarios.
En el ejemplo de código siguiente se muestra el método GetLayoutData
, que calcula las propiedades del LayoutData
estructurado basado en un tamaño determinado:
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;
}
El método GetLayoutData
realiza las siguientes acciones:
- Determina si un valor calculado
LayoutData
ya está en la memoria caché y lo devuelve si está disponible. - De lo contrario, enumera todos los elementos secundarios, invocando el método
Measure
en cada elemento secundario con un ancho y alto infinitos, y determina el tamaño máximo del elemento secundario. - Siempre que haya al menos un elemento secundario visible, calcula el número de filas y columnas necesarias y, a continuación, calcula el tamaño de una celda para los elementos secundarios en función de las dimensiones del
WrapLayout
. Tenga en cuenta que el tamaño de la celda suele ser ligeramente más ancho que el tamaño máximo del elemento secundario, pero que también podría ser más pequeño siWrapLayout
no es lo suficientemente ancho para el elemento secundario más ancho o lo suficientemente alto como para el elemento secundario más alto. - Almacena el nuevo valor
LayoutData
en la memoria caché.
Agregar propiedades guardadas en propiedades enlazables
La clase WrapLayout
define las propiedades ColumnSpacing
y RowSpacing
, cuyos valores se usan para separar las filas y columnas del diseño, y que están guardadas por propiedades enlazables. Las propiedades enlazables se muestran en el ejemplo de código siguiente:
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();
});
El controlador modificado por propiedades de cada propiedad enlazable invoca la invalidación del método InvalidateLayout
para desencadenar un nuevo pase de diseño en el WrapLayout
. Para obtener más información, consulte Invalidación del método InvalidateLayoute Invalidación del método OnChildMeasureInvalidated.
Invalidación del método OnMeasure
La invalidación de OnMeasure
se muestra en el ejemplo de código siguiente:
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);
}
La invalidación invoca el método GetLayoutData
y construye un objeto SizeRequest
a partir de los datos devueltos, a la vez que tiene en cuenta los valores de propiedad RowSpacing
y ColumnSpacing
. Para obtener más información sobre el método GetLayoutData
, consulte Cálculo y almacenamiento en caché de datos de diseño.
Importante
Los métodos Measure
y OnMeasure
nunca deben solicitar una dimensión infinita devolviendo un valor SizeRequest
con una propiedad establecida en Double.PositiveInfinity
. Sin embargo, al menos uno de los argumentos de restricción para OnMeasure
puede ser Double.PositiveInfinity
.
Invalidación del método LayoutChildren
La invalidación de LayoutChildren
se muestra en el ejemplo de código siguiente:
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;
}
}
}
La invalidación comienza con una llamada al método GetLayoutData
y, a continuación, enumera todos los elementos secundarios para ajustar el tamaño y colocarlos dentro de la celda de cada elemento secundario. Esto se logra invocando el método LayoutChildIntoBoundingRegion
, que se usa para colocar un elemento secundario dentro de un rectángulo en función de sus valores de propiedad HorizontalOptions
y VerticalOptions
. Esto equivale a realizar una llamada al método Layout
del elemento secundario.
Nota:
Tenga en cuenta que el rectángulo pasado al método LayoutChildIntoBoundingRegion
incluye todo el área en la que puede residir el elemento secundario.
Para obtener más información sobre el método GetLayoutData
, consulte Cálculo y almacenamiento en caché de datos de diseño.
Invalidación del método InvalidateLayout
La invalidación de InvalidateLayout
se invoca cuando se agregan o quitan elementos secundarios del diseño, o cuando una de las propiedades WrapLayout
cambia el valor, como se muestra en el ejemplo de código siguiente:
protected override void InvalidateLayout()
{
base.InvalidateLayout();
layoutInfoCache.Clear();
}
La invalidación invalida el diseño y descarta toda la información de diseño almacenada en caché.
Nota:
Para detener la clase Layout
invocando el método InvalidateLayout
cada vez que se agrega o quita un elemento secundario de un diseño, invalide los métodos ShouldInvalidateOnChildAdded
y ShouldInvalidateOnChildRemoved
y devuelva false
. Después, la clase de diseño puede implementar un proceso personalizado cuando se agregan o quitan elementos secundarios.
Invalidación del método OnChildMeasureInvalidated
La invalidación de OnChildMeasureInvalidated
se invoca cuando uno de los elementos secundarios del diseño cambia el tamaño y se muestra en el ejemplo de código siguiente:
protected override void OnChildMeasureInvalidated()
{
base.OnChildMeasureInvalidated();
layoutInfoCache.Clear();
}
La invalidación invalida el diseño del elemento secundario y descarta toda la información de diseño almacenada en caché.
Consumo de WrapLayout
La clase WrapLayout
se puede consumir colocándola en un tipo derivado dePage
, como se muestra en el siguiente ejemplo de código XAML:
<ContentPage ... xmlns:local="clr-namespace:ImageWrapLayout">
<ScrollView Margin="0,20,0,20">
<local:WrapLayout x:Name="wrapLayout" />
</ScrollView>
</ContentPage>
El código de C# equivalente se muestra a continuación:
public class ImageWrapLayoutPageCS : ContentPage
{
WrapLayout wrapLayout;
public ImageWrapLayoutPageCS()
{
wrapLayout = new WrapLayout();
Content = new ScrollView
{
Margin = new Thickness(0, 20, 0, 20),
Content = wrapLayout
};
}
...
}
A continuación, se pueden agregar elementos secundarios a WrapLayout
según sea necesario. En el ejemplo de código siguiente se muestran elementos Image
que se agregan al 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;
}
Cuando aparece la página que contiene el WrapLayout
, la aplicación de ejemplo accede de forma asincrónica a un archivo JSON remoto que contiene una lista de fotos, crea un elemento Image
para cada foto y lo agrega a WrapLayout
. El resultado es el aspecto que se muestra en las capturas de pantalla siguientes:
Las capturas de pantalla siguientes muestran el WrapLayout
una vez que se ha girado a la orientación horizontal:
El número de columnas de cada fila depende del tamaño de la foto, el ancho de la pantalla y el número de píxeles por unidad independiente del dispositivo. Los elementos Image
cargan asincrónicamente las fotos y, por tanto, la clase WrapLayout
recibirá llamadas frecuentes a su método LayoutChildren
, ya que cada elemento Image
recibe un nuevo tamaño basado en la foto cargada.