Condividi tramite


Xamarin.Forms BoxView

BoxView esegue il rendering di un semplice rettangolo di larghezza, altezza e colore specificati. È possibile usare BoxView per decorazione, grafica rudimentale e per l'interazione con l'utente tramite tocco.

Poiché Xamarin.Forms non dispone di un sistema di grafica vettoriale predefinito, aiuta BoxView a compensare. Alcuni dei programmi di esempio descritti in questo articolo usano BoxView per il rendering della grafica. Può BoxView essere ridimensionato in modo da assomigliare a una linea di una larghezza e spessore specifici e quindi ruotata da qualsiasi angolo utilizzando la Rotation proprietà .

Anche se BoxView può simulare grafica semplice, è consigliabile esaminare l'uso di SkiaSharp in Xamarin.Forms per requisiti grafici più sofisticati.

Impostazione del colore e delle dimensioni di BoxView

In genere si impostano le proprietà seguenti di BoxView:

  • Color per impostarne il colore.
  • CornerRadius per impostare il raggio dell'angolo.
  • WidthRequest per impostare la larghezza dell'oggetto BoxView in unità indipendenti dal dispositivo.
  • HeightRequest per impostare l'altezza dell'oggetto BoxView.

La proprietà è di tipo Color. La Color proprietà può essere impostata su qualsiasi Color valore, inclusi i campi statici di sola lettura 141 di colori denominati che vanno alfabeticamente da AliceBlue a YellowGreen.

La proprietà è di tipo CornerRadius. La CornerRadius proprietà può essere impostata su un singolo double valore del raggio dell'angolo uniforme oppure su una CornerRadius struttura definita da quattro double valori applicati all'angolo superiore sinistro, in alto a destra, in basso a sinistra e in basso a destra dell'oggetto BoxView.

Le WidthRequest proprietà e HeightRequest svolgono un ruolo solo se l'oggetto BoxView non è vincolato nel layout. Questo è il caso in cui il contenitore di layout deve conoscere le dimensioni dell'elemento figlio, ad esempio quando BoxView è figlio di una cella ridimensionata automaticamente nel Grid layout. Un BoxView oggetto è anche non vincolato quando HorizontalOptions le relative proprietà e VerticalOptions sono impostate su valori diversi da LayoutOptions.Fill. Se l'oggetto BoxView non è vincolato, ma le WidthRequest proprietà e HeightRequest non sono impostate, la larghezza o l'altezza vengono impostate sui valori predefiniti di 40 unità o circa 1/4 pollici nei dispositivi mobili.

Le WidthRequest proprietà e HeightRequest vengono ignorate se l'oggetto BoxView è vincolato nel layout, nel qual caso il contenitore di layout impone le proprie dimensioni per .BoxView

Un BoxView oggetto può essere vincolato in una dimensione e non vincolato nell'altro. Ad esempio, se BoxView è un elemento figlio di un oggetto verticale, la dimensione verticale StackLayoutdi BoxView è non vincolata e la dimensione orizzontale è generalmente vincolata. Esistono tuttavia eccezioni per la dimensione orizzontale: se la BoxView proprietà HorizontalOptions è impostata su un valore diverso da LayoutOptions.Fill, la dimensione orizzontale non è vincolata. È anche possibile che l'oggetto StackLayout stesso abbia una dimensione orizzontale non vincolata, nel qual caso anche l'oggetto BoxView sarà orizzontalmente non vincolato.

Nell'esempio viene visualizzato un vincolo quadrato BoxView di un pollice al centro della pagina:

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

Il risultato è il seguente:

BoxView di base

Se le VerticalOptions proprietà e HorizontalOptions vengono rimosse dal BoxView tag o sono impostate su Fill, l'oggetto BoxView diventa vincolato dalle dimensioni della pagina e si espande per riempire la pagina.

Un BoxView può anche essere un elemento figlio di un oggetto AbsoluteLayout. In tal caso, sia la posizione che le dimensioni di BoxView vengono impostate usando la LayoutBounds proprietà associabile associata. L'oggetto AbsoluteLayout è illustrato nell'articolo AbsoluteLayout.

Verranno visualizzati esempi di tutti questi casi nei programmi di esempio che seguono.

Rendering delle decorazioni di testo

È possibile utilizzare per BoxView aggiungere alcune decorazioni semplici sulle pagine sotto forma di linee orizzontali e verticali. L'esempio illustra questa operazione. Tutti gli oggetti visivi del programma sono definiti nel file MainPage.xaml , che contiene diversi Label elementi e BoxView nell'oggetto StackLayout illustrato di seguito:

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

Tutto il markup che segue è figlio di StackLayout. Questo markup è costituito da diversi tipi di elementi decorativi BoxView usati con l'elemento Label :

Decorazione del testo

L'intestazione elegante nella parte superiore della pagina viene ottenuta con un i AbsoluteLayout cui figli sono quattro BoxView elementi e un , Labeltutti assegnati posizioni e dimensioni specifiche:

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

Nel file XAML l'oggetto AbsoluteLayout è seguito da un Label oggetto con testo formattato che descrive l'oggetto AbsoluteLayout.

È possibile sottolineare una stringa di testo racchiudendo sia l'oggetto Label BoxView che in un StackLayout oggetto con il relativo HorizontalOptions valore impostato su un valore diverso da Fill. La larghezza di StackLayout viene quindi governata dalla larghezza di Label, che quindi impone tale larghezza su BoxView. L'oggetto BoxView viene assegnato solo un'altezza esplicita:

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

Non è possibile usare questa tecnica per sottolineare singole parole all'interno di stringhe di testo più lunghe o di un paragrafo.

È anche possibile usare un BoxView oggetto per assomigliare a un elemento HTML hr (regola orizzontale). È sufficiente lasciare che la larghezza dell'oggetto BoxView venga determinata dal contenitore padre, che in questo caso è :StackLayout

<BoxView HeightRequest="3" />

Infine, è possibile disegnare una linea verticale su un lato di un paragrafo di testo racchiudendo sia e BoxView Label in un oggetto orizzontale StackLayout. In questo caso, l'altezza di BoxView è uguale all'altezza di StackLayout, che è governata dall'altezza di Label:

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

        ···

    </Label>
</StackLayout>

Elenco di colori con BoxView

è BoxView utile per visualizzare i colori. Questo programma usa un ListView oggetto per elencare tutti i campi di sola lettura statici pubblici della Xamarin.FormsColor struttura:

Colori listView

Il programma di esempio include una classe denominata NamedColor. Il costruttore statico usa la reflection per accedere a tutti i campi della Color struttura e creare un NamedColor oggetto per ognuno di essi. Queste vengono archiviate nella proprietà statica 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; }
}

Gli oggetti visivi del programma sono descritti nel file XAML. La ItemsSource proprietà di ListView è impostata sulla proprietà statica NamedColor.All , il che significa che ListView visualizza tutti i singoli NamedColor oggetti:

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

Gli NamedColor oggetti vengono formattati dall'oggetto ViewCell impostato come modello di dati dell'oggetto ListView. Questo modello include un oggetto BoxView la Color cui Color proprietà è associata alla proprietà dell'oggetto NamedColor .

Riproduzione del gioco della vita sottoclassando BoxView

The Game of Life è un automatone cellulare inventato dal matematico John Conway e popolare nelle pagine di Scientific American negli anni '70. Una buona introduzione è fornita dall'articolo di Wikipedia Game of Life di Conway.

Il Xamarin.Forms programma di esempio definisce una classe denominata LifeCell che deriva da BoxView. Questa classe incapsula la logica di una singola cella nel Game of Life:

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 aggiunge altre tre proprietà a BoxView: le Col proprietà e Row archiviano la posizione della cella all'interno della griglia e la proprietà ne indica lo IsAlive stato. La IsAlive proprietà imposta inoltre la Color proprietà dell'oggetto BoxView su nero se la cella è attiva e bianca se la cella non è attiva.

LifeCell installa anche un oggetto TapGestureRecognizer per consentire all'utente di attivare o disattivare lo stato delle celle toccandole. La classe converte l'evento Tapped dal riconoscimento movimento nel proprio Tapped evento.

Il programma GameOfLife include anche una LifeGrid classe che incapsula gran parte della logica del gioco e una MainPage classe che gestisce gli oggetti visivi del programma. Questi includono una sovrimpressione che descrive le regole del gioco. Ecco il programma in azione che mostra un paio di centinaia LifeCell di oggetti nella pagina:

Gioco di vita

Creazione di un orologio digitale

Il programma di esempio crea 210 BoxView elementi per simulare i punti di una visualizzazione a matrice di 5-by-7 vecchio stile. È possibile leggere l'ora in modalità verticale o orizzontale, ma è più grande nel panorama:

Orologio a matrice di punti

Il file XAML esegue poco più di un'istanza dell'oggetto AbsoluteLayout usato per l'orologio:

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

Tutto il resto si verifica nel file code-behind. La logica di visualizzazione della matrice di punti è notevolmente semplificata dalla definizione di diverse matrici che descrivono i punti corrispondenti a ognuna delle 10 cifre e due punti:

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

    ···

}

Questi campi si concludono con una matrice tridimensionale di BoxView elementi per archiviare i modelli di punti per le sei cifre.

Il costruttore crea tutti gli BoxView elementi per le cifre e i due punti e inizializza anche la Color proprietà degli elementi per i BoxView due punti:

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

    ···

}

Questo programma usa la funzionalità relativa di posizionamento e ridimensionamento di AbsoluteLayout. La larghezza e l'altezza di ogni BoxView oggetto vengono impostate su valori frazionari, in particolare l'85% di 1 diviso per il numero di punti orizzontali e verticali. Le posizioni sono impostate anche su valori frazionari.

Poiché tutte le posizioni e le dimensioni sono relative alle dimensioni totali di AbsoluteLayout, il SizeChanged gestore per la pagina deve impostare solo un HeightRequest oggetto di 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;
    }

    ···

}

La larghezza di AbsoluteLayout viene impostata automaticamente perché si estende fino alla larghezza intera della pagina.

Il codice finale nella MainPage classe elabora il callback timer e colora i punti di ogni cifra. La definizione delle matrici multidimensionali all'inizio del file code-behind consente di rendere questa logica la parte più semplice del programma:

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

Creazione di un orologio analogico

Un orologio a matrice di punti potrebbe sembrare un'applicazione ovvia di BoxView, ma BoxView gli elementi sono anche in grado di realizzare un orologio analogico:

Orologio BoxView

Tutti gli oggetti visivi nel programma di esempio sono figli di un oggetto AbsoluteLayout. Questi elementi vengono ridimensionati utilizzando la LayoutBounds proprietà associata e ruotati usando la Rotation proprietà .

I tre BoxView elementi per le mani dell'orologio vengono creati un'istanza nel file XAML, ma non posizionati o ridimensionati:

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

Il costruttore del file code-behind crea un'istanza degli elementi 60 BoxView per i segni di graduazione intorno alla circonferenza dell'orologio:

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

    ···

}

Il ridimensionamento e il posizionamento di tutti gli BoxView elementi si verifica nel SizeChanged gestore per .AbsoluteLayout Una piccola struttura interna alla classe denominata HandParams descrive le dimensioni di ognuna delle tre mani rispetto alle dimensioni totali dell'orologio:

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

    ···

 }

Il SizeChanged gestore determina il centro e il raggio dell'oggetto AbsoluteLayoute quindi ridimensiona e posiziona i 60 BoxView elementi usati come segni di graduazione. Il for ciclo termina impostando la Rotation proprietà di ognuno di questi BoxView elementi. Alla fine del SizeChanged gestore, il LayoutHand metodo viene chiamato per ridimensionare e posizionare le tre mani dell'orologio:

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

    ···

}

Il LayoutHand metodo ridimensiona e posiziona ogni mano in modo che punti direttamente alla posizione 12:00. Alla fine del metodo, la AnchorY proprietà viene impostata su una posizione corrispondente al centro dell'orologio. Indica il centro di rotazione.

Le mani vengono ruotate nella funzione di callback timer:

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 seconda mano viene trattata in modo leggermente diverso: una funzione di interpolazione di animazione viene applicata per rendere il movimento sembra meccanico piuttosto che liscia. Su ogni graduazione, la seconda mano tira indietro un po 'e poi supera la destinazione. Questo piccolo frammento di codice aggiunge molto al realismo del movimento.