Criar um layout personalizado em Xamarin.Forms
Xamarin.Forms define cinco classes de layout – StackLayout, AbsoluteLayout, RelativeLayout, Grid e FlexLayout, e cada uma organiza seus filhos de uma maneira diferente. No entanto, às vezes é necessário organizar o conteúdo da página usando um layout não fornecido pelo Xamarin.Forms. Este artigo explica como escrever uma classe de layout personalizada e demonstra uma classe WrapLayout sensível à orientação que organiza seus filhos horizontalmente na página e, em seguida, quebra a exibição de filhos subsequentes em linhas adicionais.
No Xamarin.Forms, todas as classes de layout derivam da Layout<T>
classe e restringem o tipo genérico a View
e seus tipos derivados. Por sua vez, a Layout<T>
classe deriva da Layout
classe, que fornece o mecanismo para posicionar e dimensionar elementos filho.
Cada elemento visual é responsável por determinar seu próprio tamanho preferido, que é conhecido como o tamanho solicitado . Page
, Layout
e Layout<View>
tipos derivados são responsáveis por determinar a localização e o tamanho de seus filhos, ou filhos, em relação a si mesmos. Portanto, o layout envolve uma relação pai-filho, onde o pai determina qual deve ser o tamanho de seus filhos, mas tentará acomodar o tamanho solicitado da criança.
Uma compreensão completa do layout e dos Xamarin.Forms ciclos de invalidação é necessária para criar um layout personalizado. Esses ciclos serão agora discutidos.
Layout
O layout começa na parte superior da árvore visual com uma página e prossegue por todos os ramos da árvore visual para abranger todos os elementos visuais em uma página. Elementos que são pais de outros elementos são responsáveis por dimensionar e posicionar seus filhos em relação a si mesmos.
A VisualElement
classe define um Measure
método que mede um elemento para operações de layout e um Layout
método que especifica a área retangular em que o elemento será renderizado. Quando um aplicativo é iniciado e a primeira página é exibida, um ciclo de layout que consiste primeiro em Measure
chamadas e, em seguida, em chamadas Layout
, começa no Page
objeto:
- Durante o ciclo de layout, cada elemento pai é responsável por chamar o
Measure
método em seus filhos. - Depois que os filhos são medidos, cada elemento pai é responsável por chamar o
Layout
método em seus filhos.
Esse ciclo garante que cada elemento visual na página receba chamadas para os Measure
métodos e Layout
. O processo é mostrado no diagrama a seguir:
Observação
Observe que os ciclos de layout também podem ocorrer em um subconjunto da árvore visual se algo for alterado para afetar o layout. Isso inclui itens que estão sendo adicionados ou removidos de uma coleção, como em um StackLayout
, uma alteração na IsVisible
propriedade de um elemento ou uma alteração no tamanho de um elemento.
Cada Xamarin.Forms classe que tem uma Content
ou uma Children
propriedade tem um método substituível LayoutChildren
. As classes de layout personalizadas derivadas de Layout<View>
devem substituir esse método e garantir que os Measure
métodos e Layout
sejam chamados em todos os filhos do elemento, para fornecer o layout personalizado desejado.
Além disso, cada classe que deriva de Layout
ou Layout<View>
deve substituir o OnMeasure
método, que é onde uma classe de layout determina o tamanho que precisa ser fazendo chamadas para os Measure
métodos de seus filhos.
Observação
Os elementos determinam seu tamanho com base em restrições, que indicam quanto espaço está disponível para um elemento dentro do pai do elemento. As restrições passadas para os Measure
métodos e OnMeasure
podem variar de 0 a Double.PositiveInfinity
. Um elemento é restringido, ou totalmente restringido, quando recebe uma chamada para seu Measure
método com argumentos não infinitos - o elemento é restrito a um tamanho específico. Um elemento é irrestrito, ou parcialmente constrangido, quando recebe uma chamada para seu Measure
método com pelo menos um argumento igual a Double.PositiveInfinity
– a restrição infinita pode ser pensada como indicando autodimensionamento.
Invalidação
Invalidação é o processo pelo qual uma alteração em um elemento em uma página dispara um novo ciclo de layout. Os elementos são considerados inválidos quando não têm mais o tamanho ou a posição correta. Por exemplo, se a FontSize
propriedade de um Button
for alterada, o Button
é dito ser inválido porque ele não terá mais o tamanho correto. O redimensionamento do Button
pode então ter um efeito cascata de alterações no layout através do resto de uma página.
Os elementos invalidam-se invocando o InvalidateMeasure
método, geralmente quando uma propriedade do elemento é alterada que pode resultar em um novo tamanho do elemento. Esse método dispara o MeasureInvalidated
evento, que o pai do elemento manipula para disparar um novo ciclo de layout.
A Layout
classe define um manipulador para o MeasureInvalidated
evento em cada filho adicionado à sua Content
propriedade ou Children
coleção e desanexa o manipulador quando o filho é removido. Portanto, cada elemento da árvore visual que tem filhos é alertado sempre que um de seus filhos muda de tamanho. O diagrama a seguir ilustra como uma alteração no tamanho de um elemento na árvore visual pode causar alterações que ondulam a árvore:
No entanto, a Layout
classe tenta restringir o impacto de uma alteração no tamanho de uma criança no layout de uma página. Se o layout for restrito ao tamanho, uma alteração de tamanho filho não afetará nada maior do que o layout pai na árvore visual. No entanto, geralmente uma alteração no tamanho de um layout afeta como o layout organiza seus filhos. Portanto, qualquer alteração no tamanho de um layout iniciará um ciclo de layout para o layout, e o layout receberá chamadas para seus OnMeasure
e LayoutChildren
métodos.
A Layout
classe também define um InvalidateLayout
método que tem uma finalidade semelhante ao InvalidateMeasure
método. O InvalidateLayout
método deve ser chamado sempre que uma alteração for feita que afete como o layout posiciona e dimensiona seus filhos. Por exemplo, a Layout
classe invoca o InvalidateLayout
método sempre que um filho é adicionado ou removido de um layout.
O InvalidateLayout
pode ser substituído para implementar um cache para minimizar invocações repetitivas dos Measure
métodos dos filhos do layout. A substituição do InvalidateLayout
método fornecerá uma notificação de quando as crianças são adicionadas ou removidas do layout. Da mesma forma, o OnChildMeasureInvalidated
método pode ser substituído para fornecer uma notificação quando um dos filhos do layout muda de tamanho. Para ambas as substituições de método, um layout personalizado deve responder limpando o cache. Para obter mais informações, consulte Calcular e armazenar dados de layout em cache.
Criar um layout personalizado
O processo para criar um layout personalizado é o seguinte:
Crie uma classe que derive da classe
Layout<View>
. Para obter mais informações, consulte Criar um WrapLayout.[opcional] Adicione propriedades, com suporte de propriedades vinculáveis, para quaisquer parâmetros que devam ser definidos na classe de layout. Para obter mais informações, consulte Adicionar propriedades com suporte de propriedades vinculáveis.
Substitua o
OnMeasure
método para invocar oMeasure
método em todos os filhos do layout e retorne um tamanho solicitado para o layout. Para obter mais informações, consulte Substituir o método OnMeasure.Substitua o
LayoutChildren
método para chamar oLayout
método em todos os filhos do layout. A falha em invocar oLayout
método em cada filho em um layout resultará em que a criança nunca receberá um tamanho ou posição correta e, portanto, a criança não se tornará visível na página. Para obter mais informações, consulte Substituir o método LayoutChildren.Observação
Ao enumerar filhos no
OnMeasure
eLayoutChildren
substitui, ignore qualquer filho cujaIsVisible
propriedade esteja definida comofalse
. Isso garantirá que o layout personalizado não deixe espaço para crianças invisíveis.[opcional] Substitua o
InvalidateLayout
método a ser notificado quando crianças forem adicionadas ou removidas do layout. Para obter mais informações, consulte Substituir o método InvalidateLayout.[opcional] Substitua o
OnChildMeasureInvalidated
método a ser notificado quando um dos filhos do layout mudar de tamanho. Para obter mais informações, consulte Substituir o método OnChildMeasureInvalidated .
Observação
Observe que a OnMeasure
substituição não será invocada se o tamanho do layout for governado por seu pai, em vez de seus filhos. No entanto, a substituição será invocada se uma ou ambas as restrições forem infinitas ou se a classe de layout tiver valores de propriedade ou não padrão HorizontalOptions
VerticalOptions
. Por esse motivo, a LayoutChildren
substituição não pode depender de tamanhos filho obtidos durante a chamada do OnMeasure
método. Em vez disso, LayoutChildren
deve invocar o Measure
método nos filhos do layout, antes de invocar o Layout
método. Como alternativa, o tamanho dos filhos obtidos na substituição pode ser armazenado em OnMeasure
cache para evitar invocações posteriores Measure
na LayoutChildren
substituição, mas a classe de layout precisará saber quando os tamanhos precisam ser obtidos novamente. Para obter mais informações, consulte Calcular e armazenar dados de layout em cache.
A classe de layout pode ser consumida adicionando-a a um Page
, e adicionando filhos ao layout. Para obter mais informações, consulte Consumir o WrapLayout.
Criar um WrapLayout
O aplicativo de exemplo demonstra uma classe sensível WrapLayout
à orientação que organiza seus filhos horizontalmente na página e, em seguida, encapsula a exibição de filhos subsequentes em linhas adicionais.
A WrapLayout
classe aloca a mesma quantidade de espaço para cada criança, conhecida como tamanho da célula, com base no tamanho máximo das crianças. Crianças menores que o tamanho da célula podem ser posicionadas dentro da célula com base em seus HorizontalOptions
valores e VerticalOptions
propriedades.
A WrapLayout
definição de classe é mostrada no exemplo de código a seguir:
public class WrapLayout : Layout<View>
{
Dictionary<Size, LayoutData> layoutDataCache = new Dictionary<Size, LayoutData>();
...
}
Calcular e armazenar em cache dados de layout
A LayoutData
estrutura armazena dados sobre uma coleção de filhos em várias propriedades:
VisibleChildCount
– o número de crianças que estão visíveis no layout.CellSize
– o tamanho máximo de todas as crianças, ajustado ao tamanho do layout.Rows
– o número de linhas.Columns
– o número de colunas.
O layoutDataCache
campo é usado para armazenar vários LayoutData
valores. Quando o aplicativo for iniciado, dois LayoutData
objetos serão armazenados em cache no layoutDataCache
dicionário para a orientação atual – um para os argumentos de restrição para a OnMeasure
substituição e outro para os width
argumentos e height
para a LayoutChildren
substituição. Ao girar o dispositivo para a orientação paisagem, a OnMeasure
substituição e a substituição serão novamente invocadas, o LayoutChildren
que resultará em outros dois LayoutData
objetos sendo armazenados em cache no dicionário. No entanto, ao retornar o dispositivo para a orientação retrato, não são necessários cálculos adicionais porque o layoutDataCache
já tem os dados necessários.
O exemplo de código a seguir mostra o GetLayoutData
método, que calcula as propriedades do estruturado LayoutData
com base em um tamanho específico:
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;
}
O GetLayoutData
método executa as seguintes operações:
- Ele determina se um valor calculado
LayoutData
já está no cache e o retorna se estiver disponível. - Caso contrário, ele enumera todas as crianças, invocando o
Measure
método em cada criança com uma largura e altura infinitas, e determina o tamanho máximo da criança. - Desde que haja pelo menos um filho visível, ele calcula o número de linhas e colunas necessárias e, em seguida, calcula um tamanho de célula para os filhos com base nas dimensões do
WrapLayout
. Observe que o tamanho da célula geralmente é um pouco maior do que o tamanho máximo da criança, mas que também pode ser menor se aWrapLayout
criança não for larga o suficiente para a criança mais larga ou alta o suficiente para a criança mais alta. - Ele armazena o novo
LayoutData
valor no cache.
Adicionar propriedades com suporte de propriedades vinculáveis
A WrapLayout
classe define ColumnSpacing
e RowSpacing
propriedades, cujos valores são usados para separar as linhas e colunas no layout e que são apoiadas por propriedades vinculáveis. As propriedades vinculáveis são mostradas no exemplo de código a seguir:
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();
});
O manipulador de propriedade alterada de cada propriedade vinculável invoca a InvalidateLayout
substituição de método para disparar uma nova passagem de layout no WrapLayout
. Para obter mais informações, consulte Substituir o método InvalidateLayout e Substituir o método OnChildMeasureInvalidated .
Substituir o método OnMeasure
A OnMeasure
substituição é mostrada no exemplo de código a seguir:
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);
}
A substituição invoca o GetLayoutData
método e constrói um SizeRequest
objeto a partir dos dados retornados, ao mesmo tempo em que leva em conta os RowSpacing
valores e ColumnSpacing
propriedade. Para obter mais informações sobre o método, consulte Calcular e armazenar dados de GetLayoutData
layout em cache.
Importante
Os Measure
métodos e OnMeasure
nunca devem solicitar uma dimensão infinita retornando um SizeRequest
valor com uma propriedade definida como Double.PositiveInfinity
. No entanto, pelo menos um dos argumentos de restrição para OnMeasure
pode ser Double.PositiveInfinity
.
Substituir o método LayoutChildren
A LayoutChildren
substituição é mostrada no exemplo de código a seguir:
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;
}
}
}
A substituição começa com uma chamada para o GetLayoutData
método e, em seguida, enumera todos os filhos para dimensioná-los e posicioná-los dentro da célula de cada filho. Isso é obtido invocando o LayoutChildIntoBoundingRegion
método, que é usado para posicionar uma criança dentro de um retângulo com base em seus HorizontalOptions
valores e VerticalOptions
propriedade. Isso equivale a fazer uma chamada para o método da Layout
criança.
Observação
Observe que o retângulo passado para o LayoutChildIntoBoundingRegion
método inclui toda a área em que a criança pode residir.
Para obter mais informações sobre o método, consulte Calcular e armazenar dados de GetLayoutData
layout em cache.
Substituir o método InvalidateLayout
A InvalidateLayout
substituição é invocada quando os filhos são adicionados ou removidos do layout, ou quando uma das propriedades altera o WrapLayout
valor, conforme mostrado no exemplo de código a seguir:
protected override void InvalidateLayout()
{
base.InvalidateLayout();
layoutInfoCache.Clear();
}
A substituição invalida o layout e descarta todas as informações de layout armazenadas em cache.
Observação
Para interromper a Layout
classe invocando o InvalidateLayout
método sempre que um filho for adicionado ou removido de um layout, substitua os ShouldInvalidateOnChildAdded
métodos e ShouldInvalidateOnChildRemoved
e retorne false
. A classe de layout pode implementar um processo personalizado quando os filhos são adicionados ou removidos.
Substitua o método OnChildMeasureInvalidated
A OnChildMeasureInvalidated
substituição é invocada quando um dos filhos do layout altera de tamanho e é mostrada no exemplo de código a seguir:
protected override void OnChildMeasureInvalidated()
{
base.OnChildMeasureInvalidated();
layoutInfoCache.Clear();
}
A substituição invalida o layout filho e descarta todas as informações de layout armazenadas em cache.
Consumir o WrapLayout
A WrapLayout
classe pode ser consumida colocando-a em um Page
tipo derivado, conforme demonstrado no exemplo de código XAML a seguir:
<ContentPage ... xmlns:local="clr-namespace:ImageWrapLayout">
<ScrollView Margin="0,20,0,20">
<local:WrapLayout x:Name="wrapLayout" />
</ScrollView>
</ContentPage>
O código C# equivalente é mostrado abaixo:
public class ImageWrapLayoutPageCS : ContentPage
{
WrapLayout wrapLayout;
public ImageWrapLayoutPageCS()
{
wrapLayout = new WrapLayout();
Content = new ScrollView
{
Margin = new Thickness(0, 20, 0, 20),
Content = wrapLayout
};
}
...
}
As crianças podem então ser adicionadas ao conforme WrapLayout
necessário. O exemplo de código a WrapLayout
seguir mostra Image
elementos que estão sendo adicionados ao :
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;
}
Quando a página que contém o WrapLayout
é exibida, o aplicativo de exemplo acessa de forma assíncrona um arquivo JSON remoto que contém uma lista de fotos, cria um Image
elemento para cada foto e o adiciona ao WrapLayout
. Isso resulta na aparência mostrada nas capturas de tela seguir:
As capturas de tela a seguir mostram o depois que ele foi girado para a WrapLayout
orientação paisagem:
O número de colunas em cada linha depende do tamanho da foto, da largura da tela e do número de pixels por unidade independente do dispositivo. Os Image
elementos carregam as fotos de forma assíncrona e, portanto, a WrapLayout
classe receberá chamadas frequentes para seu LayoutChildren
método à medida que cada Image
elemento recebe um novo tamanho com base na foto carregada.