Compartilhar via


Xamarin.Forms Visualização de caixa

BoxView renderiza um retângulo simples de uma 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 usam BoxView para renderizar elementos gráficos. O BoxView pode ser dimensionado para se assemelhar a uma linha de uma largura e espessura específicas e, em seguida, girado por qualquer ângulo usando a Rotation propriedade.

Embora BoxView possa imitar gráficos simples, convém investigar Usando o SkiaSharp para Xamarin.Forms obter requisitos gráficos mais sofisticados.

Definindo 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 alfabeticamente de AliceBlue até YellowGreen.

A CornerRadius propriedade é do tipo CornerRadius, a propriedade pode ser definida como um único double valor de raio BoxViewde canto uniforme ou uma CornerRadius estrutura definida por quatro double valores que são aplicados à parte superior esquerda, superior direita, inferior esquerda e inferior direita do .

As WidthRequest propriedades e HeightRequest só desempenham uma função se o BoxView não estiver restrito no layout. Esse é o caso quando o contêiner de layout precisa saber o tamanho da criança, por exemplo, quando é BoxView filho de uma célula de tamanho automático no Grid layout. A BoxView também não é restrito quando suas HorizontalOptions propriedades e VerticalOptions são definidas como valores diferentes de LayoutOptions.Fill. Se o BoxView não estiver restrito, mas as WidthRequest propriedades e HeightRequest não estiverem definidas, a largura ou 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 tem sua HorizontalOptions propriedade definida para algo diferente de LayoutOptions.Fill, então a dimensão horizontal também é irrestrita. Também é possível que o StackLayout próprio tenha uma dimensão horizontal irrestrita, caso em que o BoxView mesmo também será horizontalmente irrestrito.

O exemplo exibe um quadrado de uma polegada sem restrições 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 e HorizontalOptions forem removidas da BoxView tag ou estiverem definidas como Fill, o BoxView se tornará restrito pelo tamanho da página e se expandirá para preencher a página.

Um 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 vinculável anexada. O AbsoluteLayout é discutido no artigo AbsoluteLayout.

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

Renderização de 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 elementos visuais do programa são definidos no arquivo MainPage.xaml , que contém vários Label elementos e BoxView 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. Esta marcação consiste em vários tipos de elementos decorativos BoxView utilizados 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 os quais são atribuídos 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 cadeia de caracteres de texto colocando o Label e BoxView em um StackLayout que tem seu HorizontalOptions valor definido como algo diferente de Fill. A largura do é então governada StackLayout pela largura do Label, que então impõe essa largura ao BoxView. Ao 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 texto mais longas ou em um parágrafo.

Também é possível usar um BoxView elemento para hr se assemelhar a um HTML (regra horizontal). Basta deixar que a largura do BoxView seja determinada pelo seu recipiente 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 uma horizontal StackLayout. Neste caso, a altura do BoxView é a mesma que a altura do 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:

ListView Cores

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 elementos 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 que é definido como o modelo de dados do ListView. Este modelo inclui uma BoxView propriedade cuja Color está vinculada à Color propriedade do NamedColor objeto.

Jogando o Jogo da Vida subclassificando 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 Wikipédia 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;
        }
    }
}

LifeCellAdiciona mais três propriedades a BoxView: As propriedades e Row armazenam Col 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 do BoxView para preto se a célula estiver viva e branco 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 traduz o Tapped evento do reconhecedor de gestos 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. Estes incluem 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 exemplo cria 210 BoxView elementos para simular os pontos de uma exibição de matriz de pontos 5 por 7 à moda antiga. Você pode ler a hora no modo retrato ou paisagem, mas ela é maior no modo 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 terminam 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 propriedade dos BoxView 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 para valores fracionários.

Como todas as posições e tamanhos são relativos ao tamanho total do , o SizeChanged manipulador da AbsoluteLayoutpágina só precisa 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 na 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 do BoxView, mas BoxView os elementos também são capazes de realizar um relógio analógico:

BoxView Relógio

Todos os elementos visuais no programa de exemplo são filhos de um AbsoluteLayoutarquivo . 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 posicionamento de todos os BoxView elementos ocorre no SizeChanged 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 AbsoluteLayoutseleção. 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 até a posição 12:00. No final do método, a AnchorY propriedade é definida como 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;
    }
}

A segunda mão é tratada de forma um pouco diferente: uma função de flexibilização de animação é aplicada para fazer o movimento parecer mecânico em vez de suave. Em cada carrapato, a segunda mão recua um pouco e depois ultrapassa seu destino. Esse pouco de código acrescenta muito ao realismo do movimento.