Xamarin.Forms BoxView

BoxView representa un rectángulo simple de un ancho, alto y color especificados. Puede usar BoxView para la decoración, gráficos rudimentarios y para la interacción con el usuario a través de la función táctil.

Dado que Xamarin.Forms no tiene un sistema de gráficos vectoriales integrado, BoxView ayuda a compensar. Algunos de los programas de ejemplo descritos en este artículo usan BoxView para representar gráficos. BoxView Puede tener un tamaño similar a una línea de un ancho y un grosor específicos y, a continuación, girar por cualquier ángulo mediante la propiedad Rotation.

Aunque BoxView puede imitar gráficos simples, es posible que quiera investigar el uso de SkiaSharp en Xamarin.Forms para obtener requisitos gráficos más sofisticados.

Establecer el color y el tamaño de BoxView

Normalmente, establecerá las siguientes propiedades de BoxView:

  • Color para establecer su color.
  • CornerRadius para establecer su radio de redondeo.
  • WidthRequest para establecer el ancho de BoxView en unidades independientes del dispositivo.
  • HeightRequest para establecer el alto de BoxView.

La propiedad Color es de tipo Color; la propiedad se puede establecer en cualquier valor Color, incluidos los 141 campos estáticos de solo lectura de colores con nombre que van alfabéticamente de AliceBlue a YellowGreen.

La propiedad CornerRadius es de tipo CornerRadius; la propiedad se puede establecer en un único valor double de radio de esquina uniforme o una estructura CornerRadius definida por cuatro valores double que se aplican a la parte superior izquierda, superior derecha, inferior izquierda e inferior derecha de BoxView.

Las propiedades WidthRequest y HeightRequest solo desempeñan un rol si BoxView está sin restricciones en el diseño. Este es el caso cuando el contenedor de diseño necesita conocer el tamaño del elemento secundario, por ejemplo, cuando BoxView es un elemento secundario de una celda de tamaño automático en el diseño Grid. Un BoxView tampoco tiene restricciones cuando sus propiedades HorizontalOptions y VerticalOptions se establecen en valores distintos de LayoutOptions.Fill. Si BoxView no está restringido, pero las propiedades WidthRequest y HeightRequest no se establecen, el ancho o alto se establecen en valores predeterminados de 40 unidades o aproximadamente de 1/4 pulgadas en dispositivos móviles.

Las propiedades WidthRequest y HeightRequest se omiten si BoxView está restringido en el diseño, en cuyo caso el contenedor de diseño impone su propio tamaño en el BoxView.

Una vista BoxView se puede restringir en una dimensión y sin restricciones en la otra. Por ejemplo, si BoxView es un elemento secundario de una vertical StackLayout, la dimensión vertical de BoxView no está restringida y su dimensión horizontal suele estar restringida. Pero hay excepciones para esa dimensión horizontal: si BoxView tiene su propiedad HorizontalOptions establecida en algo distinto de LayoutOptions.Fill, la dimensión horizontal tampoco está restringida. También es posible que el propio StackLayout tenga una dimensión horizontal sin restricciones, en cuyo caso el BoxView también estará horizontalmente sin restricciones.

La muestra presenta un cuadrado de una pulgada sin restricciones BoxView en el centro de su 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>

Este es el resultado:

Basic BoxView

Si las propiedades VerticalOptions y HorizontalOptions se quitan de la etiqueta BoxView o se establecen en Fill, el BoxView se restringe por el tamaño de la página y se expande para rellenar la página.

BoxView también puede ser un elemento secundario de AbsoluteLayout. En ese caso, tanto la ubicación como el tamaño del BoxView se establecen mediante la propiedad enlazable adjunta LayoutBounds. El AbsoluteLayout describe en el artículo AbsoluteLayout.

Verá ejemplos de todos estos casos en los programas de ejemplo siguientes.

Representación de decoraciones de texto

Puede usar BoxView para agregar algunas decoraciones simples en sus páginas en forma de líneas horizontales y verticales. En el ejemplo se muestra esto. Todos los objetos visuales del programa se definen en el archivo MainPage.xaml, que contiene varios elementos Label y BoxView en el StackLayout que se muestra aquí:

<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>

Todo el marcado siguiente son elementos secundarios de StackLayout. Este marcado consta de varios tipos de elementos decorativos BoxView usados con el elemento Label:

Decoración de texto

El encabezado elegante de la parte superior de la página se logra con un AbsoluteLayout cuyos elementos secundarios son cuatro elementos BoxView y un Label, todos los cuales se asignan ubicaciones y tamaños 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>

En el archivo XAML, AbsoluteLayout va seguido de un Label con texto con formato que describe el AbsoluteLayout.

Puede subrayar una cadena de texto escribiendo tanto Label como BoxView en un StackLayout entre llaves que tenga su valor HorizontalOptions establecido en un valor distinto de Fill. A continuación, el ancho de StackLayout se rige por el ancho de Label, que a continuación impone ese ancho en BoxView. Solo BoxView se asigna un alto explícito:

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

Esta técnica no se puede usar para subrayar palabras individuales dentro de cadenas de texto más largas o un párrafo.

También es posible usar un BoxView para que se parezca a un elemento HTML hr (regla horizontal). Simplemente deje que el ancho de BoxView determinado por su contenedor primario, que en este caso es el StackLayout:

<BoxView HeightRequest="3" />

Por último, puede dibujar una línea vertical en un lado de un párrafo de texto; para ello, incluya entre llaves tanto BoxView como Label en un StackLayout horizontal. En este caso, el alto de BoxView es el mismo que el alto de StackLayout, que se rige por el alto de Label:

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

        ···

    </Label>
</StackLayout>

Mostrar colores con BoxView

El BoxView es conveniente para mostrar colores. Este programa usa un ListView para enumerar todos los campos de solo lectura estáticos públicos de la estructura Xamarin.FormsColor:

Colores de ListView

El programa de ejemplo incluye una clase denominada NamedColor. El constructor estático usa la reflexión para tener acceso a todos los campos de la estructura Color y crear un objeto NamedColor para cada uno. Estos se almacenan en la propiedad 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; }
}

Los objetos visuales del programa se describen en el archivo XAML. La propiedad ItemsSource de ListView se establece en la propiedad estática NamedColor.All, lo que significa que ListView muestra todos los objetos individuales NamedColor:

<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>

Los objetos NamedColor tienen el formato ViewCell del objeto que se establece como la plantilla de datos de ListView. Esta plantilla incluye un BoxView cuya propiedad Color está enlazada a la propiedad Color del objeto NamedColor.

Jugar al Juego de la vida por subclases de BoxView

El Juego de la vida es un autómata celular inventado por el matemático John Conway y popularizado en las páginas de la revista Scientific American en los años 70. El artículo de Wikipedia del Juego de vida de Conway proporciona una buena introducción.

El programa de ejemplo Xamarin.Forms define una clase denominada LifeCell que deriva de BoxView. Esta clase encapsula la lógica de una célula individual en el Juego de 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 agrega tres propiedades más a BoxView: las propiedades Col y Row almacenan la posición de la celda dentro de la cuadrícula y la propiedad IsAlive indica su estado. La propiedad IsAlive también establece la propiedad Color de BoxView en negro si la celda está activa y blanca si la celda no está activa.

LifeCell también instala un TapGestureRecognizer para permitir al usuario alternar el estado de las celdas pulsando en ellas. La clase traduce el evento Tapped del reconocedor de gestos en su propio evento Tapped.

El programa GameOfLife también incluye una clase LifeGrid que encapsula gran parte de la lógica del juego y una clase MainPage que controla los objetos visuales del programa. Estas incluyen una superposición que describe las reglas del juego. Este es el programa en acción mostrando un par de cientos de objetos LifeCell en la página:

Juego de vida

Creación de un reloj digital

El programa de ejemplo crea 210 BoxView elementos para simular los puntos de una pantalla de matriz de puntos de 5 a 7 de moda antigua. Puede leer la hora en modo vertical u horizontal, pero es mayor en horizontal:

Reloj de matriz de puntos

El archivo XAML hace poco más de crear una instancia del objeto AbsoluteLayout usado para el reloj:

<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 lo demás se produce en el archivo de código subyacente. La lógica de un visualización de matriz se simplifica considerablemente mediante la definición de varias matrices que describen los puntos correspondientes a cada uno de los 10 dígitos y dos puntos:

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];

    ···

}

Estos campos concluyen con una matriz tridimensional de elementos BoxView para almacenar los patrones de puntos para los seis dígitos.

El constructor crea todos los elementos BoxView para los dígitos y los dos puntos, e inicializa también la propiedad Color de los elementos BoxView para los dos puntos:

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 la característica de posicionamiento y ajuste de tamaño relativos de AbsoluteLayout. El ancho y alto de cada BoxView se establecen en valores fraccionarios, específicamente el 85 % de 1 dividido por el número de puntos horizontales y verticales. Las posiciones también se establecen en valores fraccionarios.

Dado que todas las posiciones y tamaños son relativas al tamaño total de AbsoluteLayout, el controlador SizeChanged de la página solo necesita establecer un HeightRequest de 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;
    }

    ···

}

El ancho de AbsoluteLayout se establece automáticamente porque se extiende al ancho completo de la página.

El código final de la clase MainPage procesa la devolución de llamada del temporizador y colorea los puntos de cada dígito. La definición de las matrices multidimensionales al principio del archivo de código subyacente ayuda a que esta lógica sea la parte más sencilla del 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;
            }
    }
}

Crear un reloj analógico

Un reloj de matriz de puntos puede parecer una aplicación obvia de BoxView, pero los elementos BoxView también son capaces de realizar un reloj analógico:

Reloj BoxView

Todos los objetos visuales del programa de ejemplo son elementos secundarios de un AbsoluteLayout. Estos elementos tienen el tamaño de la propiedad adjunta LayoutBounds y se giran mediante la propiedad Rotation.

Se crean instancias para los tres elementos BoxView para las manecillas del reloj en el archivo XAML, pero no se colocan ni se ajusta el tamaño:

<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>

El constructor del archivo de código subyacente crea una instancia de los 60 elementos BoxView para las marcas de verificación alrededor de la circunferencia del reloj:

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);
    }

    ···

}

El ajuste de tamaño y posicionamiento de todos los elementos BoxView se produce en el controlador SizeChanged para el AbsoluteLayout. Una pequeña estructura interna a la clase denominada HandParams describe el tamaño de cada una de las tres manecillas con respecto al tamaño total del reloj:

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);

    ···

 }

El controlador SizeChanged determina el centro y el radio de AbsoluteLayout y, a continuación, ajusta el tamaño y coloca los 60 elementos BoxView usados como marcas de verificación. El bucle for concluye estableciendo la propiedad Rotation de cada uno de estos elementos BoxView. Al final del controlador SizeChanged, se llama al método LayoutHand para ajustar el tamaño y colocar las tres manecillas del reloj:

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;
    }

    ···

}

El método LayoutHand ajusta los tamaños y coloca cada manecilla para que apunte directamente hasta la posición de las 12:00. Al final del método, la propiedad AnchorY se establece en una posición correspondiente al centro del reloj. Esto indica el centro de rotación.

Las manecillas se giran en la función de devolución de llamada del 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;
    }
}

La segunda manecilla se trata un poco diferente: se aplica una función de aceleración de animación para hacer que el movimiento parezca mecánico en lugar de suave. En cada tic, la segunda manecilla retrocede un poco y luego supera su destino. Este trocito de código aumenta mucho el realismo del movimiento.