Udostępnij za pośrednictwem


Xamarin.Forms BoxView

BoxView Renderuje prosty prostokąt o określonej szerokości, wysokości i kolorze. Można użyć BoxView do dekoracji, rudimentarnej grafiki i interakcji z użytkownikiem za pośrednictwem dotyku.

Ponieważ Xamarin.Forms nie ma wbudowanego systemu grafiki wektorowej, BoxView pomaga zrekompensować. Niektóre przykładowe programy opisane w tym artykule są używane BoxView do renderowania grafiki. Rozmiar BoxView może przypominać linię o określonej szerokości i grubości, a następnie obracany według dowolnego kąta Rotation przy użyciu właściwości .

Chociaż BoxView można naśladować prostą grafikę, warto zbadać użycie biblioteki SkiaSharp w celu Xamarin.Forms uzyskania bardziej zaawansowanych wymagań graficznych.

Ustawianie koloru i rozmiaru elementu BoxView

Zazwyczaj należy ustawić następujące właściwości :BoxView

  • Color aby ustawić jego kolor.
  • CornerRadius aby ustawić promień rogu.
  • WidthRequest aby ustawić szerokość BoxView elementu w jednostkach niezależnych od urządzenia.
  • HeightRequest aby ustawić wysokość obiektu BoxView.

Właściwość Color jest typu Color; właściwość można ustawić na dowolną Color wartość, w tym 141 statycznych pól tylko do odczytu nazwanych kolorów, począwszy od alfabetycznie od AliceBlue do YellowGreen.

Właściwość CornerRadius jest typu CornerRadius; właściwość można ustawić na pojedynczą double jednolitą wartość promienia rogu lub strukturę zdefiniowaną CornerRadius przez cztery double wartości, które są stosowane do lewej górnej, prawej górnej, dolnej lewej i dolnej prawej strony BoxView.

Właściwości WidthRequest i HeightRequest odgrywają rolę tylko wtedy, gdy BoxView element nie jest ograniczony w układzie. Dzieje się tak, gdy kontener układu musi znać rozmiar elementu podrzędnego, na przykład gdy BoxView element podrzędny jest elementem podrzędnym komórki o automatycznym rozmiarze Grid w układzie. Element A BoxView jest również nieprzyciągnięty, gdy jego HorizontalOptions właściwości i VerticalOptions są ustawione na wartości inne niż LayoutOptions.Fill. Jeśli właściwość BoxView jest niezwiązana, ale WidthRequest właściwości i HeightRequest nie są ustawione, szerokość lub wysokość są ustawione na wartości domyślne 40 jednostek lub około 1/4 cala na urządzeniach przenośnych.

Właściwości WidthRequest i HeightRequest są ignorowane, jeśli BoxView element jest ograniczony w układzie, w tym przypadku kontener układu nakłada własny rozmiar na BoxView.

Element BoxView może być ograniczony w jednym wymiarze i nieskrępowany w drugim. Jeśli na przykład BoxView element jest elementem podrzędnym w pionie StackLayout, pionowy wymiar obiektu BoxView jest nieskrępowany, a jego wymiar poziomy jest zwykle ograniczony. Istnieją jednak wyjątki dla tego wymiaru poziomego: jeśli BoxView właściwość ma ustawioną HorizontalOptions właściwość na inną niż LayoutOptions.Fill, wymiar poziomy również nie jest ograniczony. Możliwe jest również, aby sam miał StackLayout niekonseksowany wymiar poziomy, w tym przypadku BoxView również będzie on poziomo nieprzyciągnięty.

W przykładzie pokazano jeden-calowy kwadrat bez ograniczeń BoxView w środku strony:

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

Oto wynik:

Podstawowy widok BoxView

VerticalOptions Jeśli właściwości i HorizontalOptions zostaną usunięte z tagu BoxView lub są ustawione na Fill, BoxView właściwość staje się ograniczona przez rozmiar strony i rozwija się w celu wypełnienia strony.

Element BoxView może być również elementem podrzędnym elementu AbsoluteLayout. W takim przypadku zarówno lokalizacja, jak i rozmiar BoxView obiektu są ustawiane przy użyciu dołączonej właściwości możliwej LayoutBounds do powiązania. Element AbsoluteLayout jest omówiony w artykule AbsoluteLayout.

Zobaczysz przykłady wszystkich tych przypadków w poniższych przykładowych programach.

Renderowanie dekoracji tekstu

Możesz użyć polecenia BoxView , aby dodać kilka prostych dekoracji na stronach w postaci linii poziomych i pionowych. W przykładzie pokazano to. Wszystkie wizualizacje programu są zdefiniowane w pliku MainPage.xaml , który zawiera kilka Label elementów i BoxView w pokazanym StackLayout tutaj:

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

Wszystkie poniższe znaczniki to elementy podrzędne .StackLayout Ten znacznik składa się z kilku typów elementów dekoracyjnych BoxView używanych z elementem Label :

Dekoracja tekstu

Stylowy nagłówek w górnej części strony jest osiągany z elementami AbsoluteLayout , których elementy podrzędne są czterema BoxView elementami i Label, z których wszystkie mają przypisane określone lokalizacje i rozmiary:

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

W pliku XAML następuje AbsoluteLayoutLabel ciąg z sformatowanym tekstem opisujący element AbsoluteLayout.

Możesz podkreślić ciąg tekstowy, ujętą zarówno element , jak i BoxView w obiekcieStackLayout, który ma jego HorizontalOptions wartość ustawioną na inną wartość niż Fill.Label Szerokość StackLayout obiektu jest następnie określana przez szerokość Labelobiektu , która następnie nakłada na wartość BoxView. Obiekt BoxView ma przypisaną tylko jawną wysokość:

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

Nie można użyć tej techniki, aby podkreślić poszczególne wyrazy w dłuższych ciągach tekstowych lub akapicie.

Można również użyć elementu BoxView , aby przypominać element HTML hr (reguła pozioma). Po prostu pozwól, aby szerokość BoxView obiektu została określona przez jego kontener nadrzędny, w tym przypadku jest to StackLayout:

<BoxView HeightRequest="3" />

Na koniec możesz narysować pionową linię po jednej stronie akapitu tekstu, ujętą zarówno element , jak BoxView i Label w poziomy StackLayout. W takim przypadku wysokość BoxView obiektu jest taka sama jak wysokość StackLayoutobiektu , która jest określana przez wysokość Labelobiektu :

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

        ···

    </Label>
</StackLayout>

Wyświetlanie listy kolorów za pomocą kontrolki BoxView

Jest BoxView wygodny w wyświetlaniu kolorów. Ten program używa elementu , ListView aby wyświetlić listę wszystkich publicznych pól Xamarin.FormsColor statycznych tylko do odczytu struktury:

Kolory elementu ListView

Przykładowy program zawiera klasę o nazwie NamedColor. Konstruktor statyczny używa odbicia, aby uzyskać dostęp do wszystkich pól Color struktury i utworzyć NamedColor obiekt dla każdego z nich. Są one przechowywane we właściwości statycznej 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; }
}

Wizualizacje programu są opisane w pliku XAML. Właściwość ItemsSourceListView obiektu jest ustawiona na właściwość statyczną NamedColor.All , co oznacza, że ListView wyświetla wszystkie poszczególne NamedColor obiekty:

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

Obiekty NamedColor są formatowane przez ViewCell obiekt, który jest ustawiany jako szablon danych obiektu ListView. Ten szablon zawiera BoxView właściwość, której Color właściwość jest powiązana z właściwością ColorNamedColor obiektu.

Gra w grę życia według podklasy BoxView

Game of Life to automat komórkowy wymyślony przez matematyka Johna Conwaya i spopularyzowany na stronach Scientific American w 1970 roku. Dobre wprowadzenie jest dostarczane przez artykuł Wikipedii Conway's Game of Life.

Przykładowy Xamarin.Forms program definiuje klasę o nazwie LifeCell , która pochodzi z BoxViewklasy . Ta klasa hermetyzuje logikę pojedynczej komórki w grze życia:

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 Dodaje trzy kolejne właściwości do BoxView: Col właściwości i Row przechowują położenie komórki w siatce, a IsAlive właściwość wskazuje jego stan. Właściwość IsAlive ustawia Color również właściwość BoxView na czarny, jeśli komórka jest aktywna, i biały, jeśli komórka nie jest aktywna.

LifeCell Program instaluje również element , TapGestureRecognizer aby umożliwić użytkownikowi przełączenie stanu komórek przez ich naciśnięcie. Klasa tłumaczy Tapped zdarzenie z rozpoznawania gestów na własne Tapped zdarzenie.

Program GameOfLife zawiera również klasę LifeGrid , która hermetyzuje znaczną część logiki gry i klasę MainPage , która obsługuje wizualizacje programu. Obejmują one nakładkę, która opisuje reguły gry. Oto program w akcji pokazujący kilkaset LifeCell obiektów na stronie:

Gra życia

Tworzenie zegara cyfrowego

Przykładowy program tworzy 210 BoxView elementów, aby symulować kropki staroświeckiego wyświetlacza macierzy 5-do-7-krotnej. Czas można odczytać w trybie pionowym lub poziomym, ale jest większy w poziomie:

Zegar kropkowy

Plik XAML wykonuje niewiele więcej niż utworzenie wystąpienia AbsoluteLayout używanego dla zegara:

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

Wszystkie inne elementy występują w pliku za pomocą kodu. Logika wyświetlania kropki macierzy jest znacznie uproszczona przez definicję kilku tablic, które opisują kropki odpowiadające każdemu z 10 cyfr i dwukropka:

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

    ···

}

Te pola kończą się trójwymiarową tablicą BoxView elementów do przechowywania wzorców kropki dla sześciu cyfr.

Konstruktor tworzy wszystkie BoxView elementy cyfr i dwukropka, a także inicjuje Color właściwość BoxView elementów dwukropka:

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

    ···

}

Ten program korzysta z funkcji pozycjonowania względnego i określania rozmiaru elementu AbsoluteLayout. Szerokość i wysokość każdego z nich BoxView są ustawione na wartości ułamkowe, a w szczególności 85% z 1 podzielone przez liczbę kropek poziomych i pionowych. Pozycje są również ustawione na wartości ułamkowe.

Ponieważ wszystkie pozycje i rozmiary są względne względem całkowitego rozmiaru AbsoluteLayoutstrony, SizeChanged program obsługi dla strony musi ustawić tylko element HeightRequestAbsoluteLayout:

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

    ···

}

Szerokość AbsoluteLayout obiektu jest ustawiana automatycznie, ponieważ rozciąga się na pełną szerokość strony.

Końcowy kod w MainPage klasie przetwarza wywołanie zwrotne czasomierza i koloruje kropki każdej cyfry. Definicja tablic wielowymiarowych na początku pliku kodu pomaga uczynić tę logikę najprostszą częścią programu:

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

Tworzenie zegara analogowego

Zegar dot-matrix może wydawać się oczywistym zastosowaniem BoxView, ale BoxView elementy są również w stanie zrealizować zegar analogowy:

Zegar BoxView

Wszystkie wizualizacje w przykładowym programie są elementami podrzędnymi elementu AbsoluteLayout. Te elementy mają rozmiar przy użyciu dołączonej LayoutBounds właściwości i są obracane przy użyciu Rotation właściwości .

Trzy BoxView elementy dla rąk zegara są tworzone wystąpienia w pliku XAML, ale nie są ustawione ani wielkości:

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

Konstruktor pliku stojącego za kodem tworzy wystąpienie 60 BoxView elementów znaczników wokół obwodu zegara:

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

    ···

}

Ustalanie rozmiaru i pozycjonowanie wszystkich BoxView elementów odbywa się w procedurze SizeChanged obsługi dla elementu AbsoluteLayout. Niewielka struktura wewnętrzna klasy o nazwie HandParams opisuje rozmiar każdej z trzech rąk względem całkowitego rozmiaru zegara:

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

    ···

 }

Procedura SizeChanged obsługi określa środek i promień AbsoluteLayoutelementu , a następnie rozmiary i pozycje 60 BoxView elementów używanych jako znaczniki znaczników. Pętla for kończy się przez ustawienie Rotation właściwości każdego z tych BoxView elementów. Na końcu SizeChanged procedury obsługi LayoutHand metoda jest wywoływana w celu rozmiaru i położenia trzech rąk zegara:

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

    ···

}

Metoda LayoutHand ustawia rozmiary i pozycje każdej ręki, aby wskazać prosto do pozycji 12:00. Na końcu metody AnchorY właściwość jest ustawiona na pozycję odpowiadającą środkowi zegara. Wskazuje to środek obrotu.

Ręce są obracane w funkcji wywołania zwrotnego czasomierza:

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

Druga ręka jest traktowana nieco inaczej: Funkcja złagodzenia animacji jest stosowana, aby ruch wydawał się mechaniczny, a nie gładki. Na każdym kleszczu druga ręka cofnie się trochę, a następnie przesłania swoją lokalizację docelową. Ten trochę kodu dodaje wiele do realizmu ruchu.