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, encapsula a exibição de filhos subsequentes em linhas adicionais.
Em 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 tamanho solicitado . Page
, Layout
e Layout<View>
os tipos derivados são responsáveis por determinar a localização e o tamanho de seu filho, ou filhos, em relação a si mesmos. Portanto, o layout envolve uma relação pai-filho, em que o pai determina qual deve ser o tamanho de seus filhos, mas tentará acomodar o tamanho solicitado do filho.
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 discutidos agora.
Layout
O layout começa na parte superior da árvore visual com uma página e prossegue por todas as ramificações 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 Layout
, chamadas, é iniciado 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 forem medidos, cada elemento pai será 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 and 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 mudar 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
propriedade ou tem um Children
método substituível LayoutChildren
. As classes de layout personalizado derivadas de Layout<View>
devem substituir esse método e garantir que os Measure
métodos and Layout
sejam chamados em todos os filhos do elemento, para fornecer o layout personalizado desejado.
Além disso, cada classe que deriva ou Layout
Layout<View>
deve substituir o OnMeasure
método, que é onde uma classe de layout determina o tamanho que ela precisa ter 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 é restrito, ou totalmente restrito, 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 restrito, quando recebe uma chamada para seu Measure
método com pelo menos um argumento igual a Double.PositiveInfinity
– a restrição infinita pode ser considerada como indicando autossínculo.
Invalidação
A invalidação é o processo pelo qual uma alteração em um elemento em uma página aciona um novo ciclo de layout. Os elementos são considerados inválidos quando não têm mais o tamanho ou a posição corretos. Por exemplo, se a FontSize
propriedade de um Button
for alterada, o Button
será considerado inválido porque não terá mais o tamanho correto. Redimensionar o Button
pode ter um efeito cascata de mudanças no layout no restante de uma página.
Os elementos se invalidam invocando o InvalidateMeasure
método, geralmente quando uma propriedade do elemento é alterada, o que pode resultar em um novo tamanho do elemento. Esse método aciona o MeasureInvalidated
evento, que o pai do elemento manipula para acionar 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 na á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 se propagam na á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 tiver restrição de 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 a forma 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
métodos and LayoutChildren
.
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 invocado sempre que for feita uma alteração que afete a forma 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. Substituir o InvalidateLayout
método fornecerá uma notificação de quando os filhos forem adicionados ou removidos 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 em cache dados de layout.
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, apoiadas por propriedades associáveis, para quaisquer parâmetros que devam ser definidos na classe de layout. Para obter mais informações, consulte Adicionar propriedades apoiadas por propriedades associá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 chamá-loLayout
em todos os filhos do layout. A falha em invocar oLayout
método em cada filho em um layout resultará no filho nunca recebendo um tamanho ou posição corretos e, portanto, o filho não ficará visível na página. Para obter mais informações, consulte Substituir o método LayoutChildren.Observação
Ao enumerar filhos nas
OnMeasure
substituições eLayoutChildren
, 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 as 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 OnChildMeasureInvalided.
Observação
Observe que a OnMeasure
substituição não será invocada se o tamanho do layout for regido 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 não padrão HorizontalOptions
ou VerticalOptions
de propriedade. Por esse motivo, a LayoutChildren
substituição não pode depender de tamanhos filho obtidos durante a chamada de 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 em cache dados de layout.
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 filho, conhecida como tamanho da célula, com base no tamanho máximo dos filhos. Crianças menores que o tamanho da célula podem ser posicionadas dentro da célula com base em seus HorizontalOptions
valores de propriedade e VerticalOptions
.
A WrapLayout
definição de classe é mostrada no seguinte exemplo de código:
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 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 LayoutChildren
substituição serão invocadas novamente, o 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, nenhum cálculo adicional é necessário porque o já layoutDataCache
possui os dados necessários.
O exemplo de código a seguir mostra o GetLayoutData
método, que calcula LayoutData
as propriedades da estrutura 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 todos os filhos, invocando o
Measure
método em cada filho com largura e altura infinitas e determina o tamanho máximo do filho. - 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 também pode ser menor se nãoWrapLayout
for largo o suficiente para a criança mais larga ou alto o suficiente para a criança mais alta. - Ele armazena o novo
LayoutData
valor no cache.
Adicionar propriedades apoiadas por propriedades associá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 associáveis. As propriedades associáveis são mostradas no seguinte exemplo de código:
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 associável invoca a substituição de InvalidateLayout
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 seguinte exemplo de código:
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 valores de RowSpacing
propriedade and ColumnSpacing
. Para obter mais informações sobre o método, consulte Calcular e armazenar em cache dados de GetLayoutData
layout.
Importante
Os Measure
métodos and 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 seguinte exemplo de código:
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 um filho dentro de um retângulo com base em seus HorizontalOptions
valores de propriedade e VerticalOptions
. Isso é equivalente 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 em cache dados de GetLayoutData
layout.
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 impedir que a Layout
classe invoque o InvalidateLayout
método sempre que um filho for adicionado ou removido de um layout, substitua os ShouldInvalidateOnChildAdded
métodos and ShouldInvalidateOnChildRemoved
e retorne false
. A classe de layout pode implementar um processo personalizado quando os filhos são adicionados ou removidos.
Substituir o método OnChildMeasureInvalidated
A OnChildMeasureInvalidated
substituição é invocada quando um dos filhos do layout muda 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 WrapLayout
conforme necessário. O exemplo de código a seguir mostra Image
elementos sendo adicionados ao 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;
}
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 WrapLayout
depois que ele foi girado para a 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 classe receberá chamadas frequentes para seu LayoutChildren
método à medida que WrapLayout
cada Image
elemento recebe um novo tamanho com base na foto carregada.