Partilhar via


Xamarin.Forms Visualização da caixa

BoxView renderiza um retângulo simples de largura, altura e cor especificadas. Você pode usar BoxView para decoração, gráficos rudimentares e para interação com o usuário através do toque.

Porque Xamarin.Forms não tem um sistema de gráficos vetoriais embutido, o BoxView ajuda a compensar. Alguns dos programas de exemplo descritos neste artigo são usados BoxView para renderizar gráficos. O BoxView pode ser dimensionado para se assemelhar a uma linha de largura e espessura específicas e, em seguida, girado por qualquer ângulo usando a Rotation propriedade.

Embora BoxView possa imitar gráficos simples, você pode querer investigar o uso do SkiaSharp em Xamarin.Forms para requisitos gráficos mais sofisticados.

Configurando a cor e o tamanho do BoxView

Normalmente, você definirá as seguintes propriedades de BoxView:

A Color propriedade é do tipo Color; a propriedade pode ser definida como qualquer Color valor, incluindo os 141 campos estáticos somente leitura de cores nomeadas que variam em ordem alfabética de AliceBlue a YellowGreen.

A CornerRadius propriedade é do tipo CornerRadius; a propriedade pode ser definida como um único double valor de raio de canto uniforme ou uma CornerRadius estrutura definida por quatro double valores que são aplicados ao canto superior esquerdo, superior direito, inferior esquerdo e inferior direito do BoxView.

As WidthRequest propriedades e HeightRequest só desempenham uma função se o BoxView for irrestrito no layout. Esse é o caso quando o contêiner de layout precisa saber o tamanho do filho, por exemplo, quando o BoxView é filho de uma célula dimensionada automaticamente no Grid layout. A BoxView também não é restringido quando suas HorizontalOptions propriedades e VerticalOptions são definidas com valores diferentes de LayoutOptions.Fill. Se o BoxView for irrestrito, mas as WidthRequest propriedades e HeightRequest não estiverem definidas, a largura ou a altura serão definidas como valores padrão de 40 unidades ou cerca de 1/4 de polegada em dispositivos móveis.

As WidthRequest propriedades e HeightRequest serão ignoradas se o BoxView for restrito no layout, caso em que o contêiner de layout impõe seu próprio tamanho ao BoxView.

A BoxView pode ser restringido em uma dimensão e irrestrito na outra. Por exemplo, se o BoxView é filho de uma vertical StackLayout, a dimensão vertical do BoxView é irrestrita e sua dimensão horizontal é geralmente restrita. Mas há exceções para essa dimensão horizontal: Se o BoxView tiver sua HorizontalOptions propriedade definida como algo diferente de LayoutOptions.Fill, então a dimensão horizontal também será irrestrita. Também é possível que o StackLayout próprio tenha uma dimensão horizontal irrestrita, caso em que também BoxView será horizontalmente irrestrito.

O exemplo exibe um quadrado de uma polegada irrestrito BoxView no centro de sua página:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:BasicBoxView"
             x:Class="BasicBoxView.MainPage">

    <BoxView Color="CornflowerBlue"
             CornerRadius="10"
             WidthRequest="160"
             HeightRequest="160"
             VerticalOptions="Center"
             HorizontalOptions="Center" />

</ContentPage>

Eis o resultado:

BoxView básico

Se as VerticalOptions propriedades and HorizontalOptions forem removidas da BoxView tag ou definidas como Fill, o se BoxView tornará restrito pelo tamanho da página e se expandirá para preencher a página.

A BoxView também pode ser filho de um AbsoluteLayout. Nesse caso, o local e o tamanho do BoxView são definidos usando a LayoutBounds propriedade associável anexada. O AbsoluteLayout é discutido no artigo AbsoluteLayout.

Você verá exemplos de todos esses casos nos programas de exemplo a seguir.

Renderizando decorações de texto

Você pode usar o BoxView para adicionar algumas decorações simples em suas páginas na forma de linhas horizontais e verticais. A amostra demonstra isso. Todos os visuais do programa são definidos no arquivo MainPage.xaml , que contém vários Label e BoxView elementos no StackLayout mostrado aqui:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:TextDecoration"
             x:Class="TextDecoration.MainPage">
    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness">
            <On Platform="iOS" Value="0, 20, 0, 0" />
        </OnPlatform>
    </ContentPage.Padding>

    <ContentPage.Resources>
        <ResourceDictionary>
            <Style TargetType="BoxView">
                <Setter Property="Color" Value="Black" />
            </Style>
        </ResourceDictionary>
    </ContentPage.Resources>

    <ScrollView Margin="15">
        <StackLayout>

            ···

        </StackLayout>
    </ScrollView>
</ContentPage>

Todas as marcações a seguir são filhas do StackLayout. Essa marcação consiste em vários tipos de elementos decorativos BoxView usados com o Label elemento:

Decoração de texto

O cabeçalho elegante na parte superior da página é obtido com um AbsoluteLayout cujos filhos são quatro BoxView elementos e um Label, todos atribuídos a locais e tamanhos específicos:

<AbsoluteLayout>
    <BoxView AbsoluteLayout.LayoutBounds="0, 10, 200, 5" />
    <BoxView AbsoluteLayout.LayoutBounds="0, 20, 200, 5" />
    <BoxView AbsoluteLayout.LayoutBounds="10, 0, 5, 65" />
    <BoxView AbsoluteLayout.LayoutBounds="20, 0, 5, 65" />
    <Label Text="Stylish Header"
           FontSize="24"
           AbsoluteLayout.LayoutBounds="30, 25, AutoSize, AutoSize"/>
</AbsoluteLayout>

No arquivo XAML, o AbsoluteLayout é seguido por um Label texto formatado que descreve o AbsoluteLayout.

Você pode sublinhar uma string de texto colocando o Label e em um StackLayout que tenha seu HorizontalOptions valor definido como algo diferente de FillBoxView . A largura do StackLayout é então governada pela largura do Label, que então impõe essa largura ao BoxView. A BoxView é atribuída apenas uma altura explícita:

<StackLayout HorizontalOptions="Center">
    <Label Text="Underlined Text"
           FontSize="24" />
    <BoxView HeightRequest="2" />
</StackLayout>

Você não pode usar essa técnica para sublinhar palavras individuais em cadeias de caracteres de texto mais longas ou em um parágrafo.

Também é possível usar um BoxView elemento HTML hr (regra horizontal). Basta deixar a largura do BoxView ser determinada por seu contêiner pai, que neste caso é o StackLayout:

<BoxView HeightRequest="3" />

Finalmente, você pode desenhar uma linha vertical em um lado de um parágrafo de texto colocando o BoxView e o Label em um horizontal StackLayout. Neste caso, a BoxView altura do é igual à altura de StackLayout, que é governada pela altura do Label:

<StackLayout Orientation="Horizontal">
    <BoxView WidthRequest="4"
             Margin="0, 0, 10, 0" />
    <Label>

        ···

    </Label>
</StackLayout>

Listando cores com BoxView

O BoxView é conveniente para exibir cores. Este programa usa um ListView para listar todos os campos públicos estáticos somente leitura da Xamarin.FormsColor estrutura:

Cores do ListView

O programa de exemplo inclui uma classe chamada NamedColor. O construtor estático usa reflexão para acessar todos os campos da Color estrutura e criar um NamedColor objeto para cada um. Eles são armazenados na propriedade estática All :

public class NamedColor
{
    // Instance members.
    private NamedColor()
    {
    }

    public string Name { private set; get; }

    public string FriendlyName { private set; get; }

    public Color Color { private set; get; }

    public string RgbDisplay { private set; get; }

    // Static members.
    static NamedColor()
    {
        List<NamedColor> all = new List<NamedColor>();
        StringBuilder stringBuilder = new StringBuilder();

        // Loop through the public static fields of the Color structure.
        foreach (FieldInfo fieldInfo in typeof(Color).GetRuntimeFields ())
        {
            if (fieldInfo.IsPublic &&
                fieldInfo.IsStatic &&
                fieldInfo.FieldType == typeof (Color))
            {
                // Convert the name to a friendly name.
                string name = fieldInfo.Name;
                stringBuilder.Clear();
                int index = 0;

                foreach (char ch in name)
                {
                    if (index != 0 && Char.IsUpper(ch))
                    {
                        stringBuilder.Append(' ');
                    }
                    stringBuilder.Append(ch);
                    index++;
                }

                // Instantiate a NamedColor object.
                Color color = (Color)fieldInfo.GetValue(null);

                NamedColor namedColor = new NamedColor
                {
                    Name = name,
                    FriendlyName = stringBuilder.ToString(),
                    Color = color,
                    RgbDisplay = String.Format("{0:X2}-{1:X2}-{2:X2}",
                                               (int)(255 * color.R),
                                               (int)(255 * color.G),
                                               (int)(255 * color.B))
                };

                // Add it to the collection.
                all.Add(namedColor);
            }
        }
        all.TrimExcess();
        All = all;
    }

    public static IList<NamedColor> All { private set; get; }
}

Os visuais do programa são descritos no arquivo XAML. A propriedade do ListView é definida como a propriedade estáticaNamedColor.All, o que significa que o ListView exibe todos os objetos individuaisNamedColor:ItemsSource

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:ListViewColors"
             x:Class="ListViewColors.MainPage">
    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness">
            <On Platform="iOS" Value="10, 20, 10, 0" />
            <On Platform="Android, UWP" Value="10, 0" />
        </OnPlatform>
    </ContentPage.Padding>

    <ListView SeparatorVisibility="None"
              ItemsSource="{x:Static local:NamedColor.All}">
        <ListView.RowHeight>
            <OnPlatform x:TypeArguments="x:Int32">
                <On Platform="iOS, Android" Value="80" />
                <On Platform="UWP" Value="90" />
            </OnPlatform>
        </ListView.RowHeight>

        <ListView.ItemTemplate>
            <DataTemplate>
                <ViewCell>
                    <ContentView Padding="5">
                        <Frame OutlineColor="Accent"
                               Padding="10">
                            <StackLayout Orientation="Horizontal">
                                <BoxView Color="{Binding Color}"
                                         WidthRequest="50"
                                         HeightRequest="50" />
                                <StackLayout>
                                    <Label Text="{Binding FriendlyName}"
                                           FontSize="22"
                                           VerticalOptions="StartAndExpand" />
                                    <Label Text="{Binding RgbDisplay, StringFormat='RGB = {0}'}"
                                           FontSize="16"
                                           VerticalOptions="CenterAndExpand" />
                                </StackLayout>
                            </StackLayout>
                        </Frame>
                    </ContentView>
                </ViewCell>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</ContentPage>

Os NamedColor objetos são formatados pelo ViewCell objeto definido como o modelo de dados do ListView. Esse modelo inclui uma BoxView propriedade cujo Color está associada à Color propriedade do NamedColor objeto.

Jogando o jogo da vida por subclasse BoxView

O Jogo da Vida é um autômato celular inventado pelo matemático John Conway e popularizado nas páginas da Scientific American na década de 1970. Uma boa introdução é fornecida pelo artigo da Wikipedia Conway's Game of Life.

O Xamarin.Forms programa de exemplo define uma classe chamada LifeCell que deriva de BoxView. Esta classe encapsula a lógica de uma célula individual no Jogo da Vida:

class LifeCell : BoxView
{
    bool isAlive;

    public event EventHandler Tapped;

    public LifeCell()
    {
        BackgroundColor = Color.White;

        TapGestureRecognizer tapGesture = new TapGestureRecognizer();
        tapGesture.Tapped += (sender, args) =>
        {
            Tapped?.Invoke(this, EventArgs.Empty);
        };
        GestureRecognizers.Add(tapGesture);
    }

    public int Col { set; get; }

    public int Row { set; get; }

    public bool IsAlive
    {
        set
        {
            if (isAlive != value)
            {
                isAlive = value;
                BackgroundColor = isAlive ? Color.Black : Color.White;
            }
        }
        get
        {
            return isAlive;
        }
    }
}

LifeCell Adiciona mais três propriedades a BoxView: As Col propriedades e Row armazenam a posição da célula dentro da grade e a IsAlive propriedade indica seu estado. A IsAlive propriedade também define a Color propriedade de the BoxView como black se a célula estiver viva e white se a célula não estiver viva.

LifeCell também instala um TapGestureRecognizer para permitir que o usuário alterne o estado das células tocando nelas. A classe converte o evento do reconhecedor de gestos Tapped em seu próprio Tapped evento.

O programa GameOfLife também inclui uma LifeGrid classe que encapsula grande parte da lógica do jogo e uma MainPage classe que lida com os visuais do programa. Isso inclui uma sobreposição que descreve as regras do jogo. Aqui está o programa em ação mostrando algumas centenas LifeCell de objetos na página:

Jogo da Vida

Criando um relógio digital

O programa de amostra cria 210 BoxView elementos para simular os pontos de uma exibição de matriz de pontos 5 por 7 antiquada. Você pode ler a hora no modo retrato ou paisagem, mas é maior na paisagem:

Relógio matricial

O arquivo XAML faz pouco mais do que instanciar o AbsoluteLayout usado para o relógio:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:DotMatrixClock"
             x:Class="DotMatrixClock.MainPage"
             Padding="10"
             SizeChanged="OnPageSizeChanged">

    <AbsoluteLayout x:Name="absoluteLayout"
                    VerticalOptions="Center" />
</ContentPage>

Todo o resto ocorre no arquivo code-behind. A lógica de exibição da matriz de pontos é bastante simplificada pela definição de várias matrizes que descrevem os pontos correspondentes a cada um dos 10 dígitos e dois pontos:

public partial class MainPage : ContentPage
{
    // Total dots horizontally and vertically.
    const int horzDots = 41;
    const int vertDots = 7;

    // 5 x 7 dot matrix patterns for 0 through 9.
    static readonly int[, ,] numberPatterns = new int[10, 7, 5]
    {
        {
            { 0, 1, 1, 1, 0}, { 1, 0, 0, 0, 1}, { 1, 0, 0, 1, 1}, { 1, 0, 1, 0, 1},
            { 1, 1, 0, 0, 1}, { 1, 0, 0, 0, 1}, { 0, 1, 1, 1, 0}
        },
        {
            { 0, 0, 1, 0, 0}, { 0, 1, 1, 0, 0}, { 0, 0, 1, 0, 0}, { 0, 0, 1, 0, 0},
            { 0, 0, 1, 0, 0}, { 0, 0, 1, 0, 0}, { 0, 1, 1, 1, 0}
        },
        {
            { 0, 1, 1, 1, 0}, { 1, 0, 0, 0, 1}, { 0, 0, 0, 0, 1}, { 0, 0, 0, 1, 0},
            { 0, 0, 1, 0, 0}, { 0, 1, 0, 0, 0}, { 1, 1, 1, 1, 1}
        },
        {
            { 1, 1, 1, 1, 1}, { 0, 0, 0, 1, 0}, { 0, 0, 1, 0, 0}, { 0, 0, 0, 1, 0},
            { 0, 0, 0, 0, 1}, { 1, 0, 0, 0, 1}, { 0, 1, 1, 1, 0}
        },
        {
            { 0, 0, 0, 1, 0}, { 0, 0, 1, 1, 0}, { 0, 1, 0, 1, 0}, { 1, 0, 0, 1, 0},
            { 1, 1, 1, 1, 1}, { 0, 0, 0, 1, 0}, { 0, 0, 0, 1, 0}
        },
        {
            { 1, 1, 1, 1, 1}, { 1, 0, 0, 0, 0}, { 1, 1, 1, 1, 0}, { 0, 0, 0, 0, 1},
            { 0, 0, 0, 0, 1}, { 1, 0, 0, 0, 1}, { 0, 1, 1, 1, 0}
        },
        {
            { 0, 0, 1, 1, 0}, { 0, 1, 0, 0, 0}, { 1, 0, 0, 0, 0}, { 1, 1, 1, 1, 0},
            { 1, 0, 0, 0, 1}, { 1, 0, 0, 0, 1}, { 0, 1, 1, 1, 0}
        },
        {
            { 1, 1, 1, 1, 1}, { 0, 0, 0, 0, 1}, { 0, 0, 0, 1, 0}, { 0, 0, 1, 0, 0},
            { 0, 1, 0, 0, 0}, { 0, 1, 0, 0, 0}, { 0, 1, 0, 0, 0}
        },
        {
            { 0, 1, 1, 1, 0}, { 1, 0, 0, 0, 1}, { 1, 0, 0, 0, 1}, { 0, 1, 1, 1, 0},
            { 1, 0, 0, 0, 1}, { 1, 0, 0, 0, 1}, { 0, 1, 1, 1, 0}
        },
        {
            { 0, 1, 1, 1, 0}, { 1, 0, 0, 0, 1}, { 1, 0, 0, 0, 1}, { 0, 1, 1, 1, 1},
            { 0, 0, 0, 0, 1}, { 0, 0, 0, 1, 0}, { 0, 1, 1, 0, 0}
        },
    };

    // Dot matrix pattern for a colon.
    static readonly int[,] colonPattern = new int[7, 2]
    {
        { 0, 0 }, { 1, 1 }, { 1, 1 }, { 0, 0 }, { 1, 1 }, { 1, 1 }, { 0, 0 }
    };

    // BoxView colors for on and off.
    static readonly Color colorOn = Color.Red;
    static readonly Color colorOff = new Color(0.5, 0.5, 0.5, 0.25);

    // Box views for 6 digits, 7 rows, 5 columns.
    BoxView[, ,] digitBoxViews = new BoxView[6, 7, 5];

    ···

}

Esses campos são concluídos com uma matriz tridimensional de elementos para armazenar os padrões de BoxView pontos para os seis dígitos.

O construtor cria todos os BoxView elementos para os dígitos e dois-pontos e também inicializa a Color BoxView propriedade dos elementos para os dois-pontos:

public partial class MainPage : ContentPage
{

    ···

    public MainPage()
    {
        InitializeComponent();

        // BoxView dot dimensions.
        double height = 0.85 / vertDots;
        double width = 0.85 / horzDots;

        // Create and assemble the BoxViews.
        double xIncrement = 1.0 / (horzDots - 1);
        double yIncrement = 1.0 / (vertDots - 1);
        double x = 0;

        for (int digit = 0; digit < 6; digit++)
        {
            for (int col = 0; col < 5; col++)
            {
                double y = 0;

                for (int row = 0; row < 7; row++)
                {
                    // Create the digit BoxView and add to layout.
                    BoxView boxView = new BoxView();
                    digitBoxViews[digit, row, col] = boxView;
                    absoluteLayout.Children.Add(boxView,
                                                new Rectangle(x, y, width, height),
                                                AbsoluteLayoutFlags.All);
                    y += yIncrement;
                }
                x += xIncrement;
            }
            x += xIncrement;

            // Colons between the hours, minutes, and seconds.
            if (digit == 1 || digit == 3)
            {
                int colon = digit / 2;

                for (int col = 0; col < 2; col++)
                {
                    double y = 0;

                    for (int row = 0; row < 7; row++)
                    {
                        // Create the BoxView and set the color.
                        BoxView boxView = new BoxView
                            {
                                Color = colonPattern[row, col] == 1 ?
                                            colorOn : colorOff
                            };
                        absoluteLayout.Children.Add(boxView,
                                                    new Rectangle(x, y, width, height),
                                                    AbsoluteLayoutFlags.All);
                        y += yIncrement;
                    }
                    x += xIncrement;
                }
                x += xIncrement;
            }
        }

        // Set the timer and initialize with a manual call.
        Device.StartTimer(TimeSpan.FromSeconds(1), OnTimer);
        OnTimer();
    }

    ···

}

Este programa usa o posicionamento relativo e o recurso de dimensionamento do AbsoluteLayout. A largura e a altura de cada um BoxView são definidas para valores fracionários, especificamente 85% de 1 dividido pelo número de pontos horizontais e verticais. As posições também são definidas com valores fracionários.

Como todas as posições e tamanhos são relativos ao tamanho total do AbsoluteLayout, o SizeChanged manipulador da página precisa apenas definir um HeightRequest dos AbsoluteLayout:

public partial class MainPage : ContentPage
{

    ···

    void OnPageSizeChanged(object sender, EventArgs args)
    {
        // No chance a display will have an aspect ratio > 41:7
        absoluteLayout.HeightRequest = vertDots * Width / horzDots;
    }

    ···

}

A largura do AbsoluteLayout é definida automaticamente porque se estende até a largura total da página.

O código final da classe processa MainPage o retorno de chamada do temporizador e colore os pontos de cada dígito. A definição das matrizes multidimensionais no início do arquivo code-behind ajuda a tornar essa lógica a parte mais simples do programa:

public partial class MainPage : ContentPage
{

    ···

    bool OnTimer()
    {
        DateTime dateTime = DateTime.Now;

        // Convert 24-hour clock to 12-hour clock.
        int hour = (dateTime.Hour + 11) % 12 + 1;

        // Set the dot colors for each digit separately.
        SetDotMatrix(0, hour / 10);
        SetDotMatrix(1, hour % 10);
        SetDotMatrix(2, dateTime.Minute / 10);
        SetDotMatrix(3, dateTime.Minute % 10);
        SetDotMatrix(4, dateTime.Second / 10);
        SetDotMatrix(5, dateTime.Second % 10);
        return true;
    }

    void SetDotMatrix(int index, int digit)
    {
        for (int row = 0; row < 7; row++)
            for (int col = 0; col < 5; col++)
            {
                bool isOn = numberPatterns[digit, row, col] == 1;
                Color color = isOn ? colorOn : colorOff;
                digitBoxViews[index, row, col].Color = color;
            }
    }
}

Criando um relógio analógico

Um relógio matricial pode parecer uma aplicação óbvia de BoxView, mas BoxView os elementos também são capazes de realizar um relógio analógico:

Relógio BoxView

Todos os visuais no programa de exemplo são filhos de um AbsoluteLayout. Esses elementos são dimensionados usando a propriedade anexada LayoutBounds e girados usando a Rotation propriedade.

Os três BoxView elementos para os ponteiros do relógio são instanciados no arquivo XAML, mas não posicionados ou dimensionados:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:BoxViewClock"
             x:Class="BoxViewClock.MainPage">
    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness">
            <On Platform="iOS" Value="0, 20, 0, 0" />
        </OnPlatform>
    </ContentPage.Padding>

    <AbsoluteLayout x:Name="absoluteLayout"
                    SizeChanged="OnAbsoluteLayoutSizeChanged">

        <BoxView x:Name="hourHand"
                 Color="Black" />

        <BoxView x:Name="minuteHand"
                 Color="Black" />

        <BoxView x:Name="secondHand"
                 Color="Black" />
    </AbsoluteLayout>
</ContentPage>

O construtor do arquivo code-behind instancia os 60 BoxView elementos para as marcas de escala ao redor da circunferência do relógio:

public partial class MainPage : ContentPage
{

    ···

    BoxView[] tickMarks = new BoxView[60];

    public MainPage()
    {
        InitializeComponent();

        // Create the tick marks (to be sized and positioned later).
        for (int i = 0; i < tickMarks.Length; i++)
        {
            tickMarks[i] = new BoxView { Color = Color.Black };
            absoluteLayout.Children.Add(tickMarks[i]);
        }

        Device.StartTimer(TimeSpan.FromSeconds(1.0 / 60), OnTimerTick);
    }

    ···

}

O dimensionamento e o SizeChanged posicionamento de todos os BoxView elementos ocorrem no manipulador para o AbsoluteLayout. Uma pequena estrutura interna à classe chamada HandParams descreve o tamanho de cada um dos três ponteiros em relação ao tamanho total do relógio:

public partial class MainPage : ContentPage
{
    // Structure for storing information about the three hands.
    struct HandParams
    {
        public HandParams(double width, double height, double offset) : this()
        {
            Width = width;
            Height = height;
            Offset = offset;
        }

        public double Width { private set; get; }   // fraction of radius
        public double Height { private set; get; }  // ditto
        public double Offset { private set; get; }  // relative to center pivot
    }

    static readonly HandParams secondParams = new HandParams(0.02, 1.1, 0.85);
    static readonly HandParams minuteParams = new HandParams(0.05, 0.8, 0.9);
    static readonly HandParams hourParams = new HandParams(0.125, 0.65, 0.9);

    ···

 }

O SizeChanged manipulador determina o centro e o raio do , e então dimensiona e posiciona os 60 BoxView elementos usados como marcas de AbsoluteLayoutescala. O for loop é concluído definindo a Rotation propriedade de cada um desses BoxView elementos. No final do SizeChanged manipulador, o LayoutHand método é chamado para dimensionar e posicionar os três ponteiros do relógio:

public partial class MainPage : ContentPage
{

    ···

    void OnAbsoluteLayoutSizeChanged(object sender, EventArgs args)
    {
        // Get the center and radius of the AbsoluteLayout.
        Point center = new Point(absoluteLayout.Width / 2, absoluteLayout.Height / 2);
        double radius = 0.45 * Math.Min(absoluteLayout.Width, absoluteLayout.Height);

        // Position, size, and rotate the 60 tick marks.
        for (int index = 0; index < tickMarks.Length; index++)
        {
            double size = radius / (index % 5 == 0 ? 15 : 30);
            double radians = index * 2 * Math.PI / tickMarks.Length;
            double x = center.X + radius * Math.Sin(radians) - size / 2;
            double y = center.Y - radius * Math.Cos(radians) - size / 2;
            AbsoluteLayout.SetLayoutBounds(tickMarks[index], new Rectangle(x, y, size, size));
            tickMarks[index].Rotation = 180 * radians / Math.PI;
        }

        // Position and size the three hands.
        LayoutHand(secondHand, secondParams, center, radius);
        LayoutHand(minuteHand, minuteParams, center, radius);
        LayoutHand(hourHand, hourParams, center, radius);
    }

    void LayoutHand(BoxView boxView, HandParams handParams, Point center, double radius)
    {
        double width = handParams.Width * radius;
        double height = handParams.Height * radius;
        double offset = handParams.Offset;

        AbsoluteLayout.SetLayoutBounds(boxView,
            new Rectangle(center.X - 0.5 * width,
                          center.Y - offset * height,
                          width, height));

        // Set the AnchorY property for rotations.
        boxView.AnchorY = handParams.Offset;
    }

    ···

}

O LayoutHand método dimensiona e posiciona cada mão para apontar diretamente para a posição 12:00. No final do método, a AnchorY propriedade é definida para uma posição correspondente ao centro do relógio. Isso indica o centro de rotação.

Os ponteiros são girados na função de retorno de chamada do temporizador:

public partial class MainPage : ContentPage
{

    ···

    bool OnTimerTick()
    {
        // Set rotation angles for hour and minute hands.
        DateTime dateTime = DateTime.Now;
        hourHand.Rotation = 30 * (dateTime.Hour % 12) + 0.5 * dateTime.Minute;
        minuteHand.Rotation = 6 * dateTime.Minute + 0.1 * dateTime.Second;

        // Do an animation for the second hand.
        double t = dateTime.Millisecond / 1000.0;

        if (t < 0.5)
        {
            t = 0.5 * Easing.SpringIn.Ease(t / 0.5);
        }
        else
        {
            t = 0.5 * (1 + Easing.SpringOut.Ease((t - 0.5) / 0.5));
        }

        secondHand.Rotation = 6 * (dateTime.Second + t);
        return true;
    }
}

O ponteiro dos segundos é tratado de maneira um pouco diferente: uma função de atenuação de animação é aplicada para fazer o movimento parecer mecânico em vez de suave. Em cada tique, o ponteiro dos segundos recua um pouco e depois ultrapassa seu destino. Esse pequeno código acrescenta muito ao realismo do movimento.