Xamarin.Forms BoxView

Download Sample Скачайте пример

BoxView отрисовывает простой прямоугольник указанной ширины, высоты и цвета. Вы можете использовать BoxView для декорирования, рупийной графики и взаимодействия с пользователем через сенсорный ввод.

Так как Xamarin.Forms у вас нет встроенной векторной графической системы, BoxView это помогает компенсировать. Некоторые из примеров программ, описанных в этой статье, используются BoxView для отрисовки графики. Размер BoxView может быть похож на линию определенной ширины и толщины, а затем повернуть на любой угол с помощью Rotation свойства.

Хотя BoxView можно имитировать простую графику, вам может потребоваться изучить использование SkiaSharp для Xamarin.Forms более сложных требований к графике.

Настройка цвета и размера BoxView

Как правило, вы задали следующие свойства BoxView:

  • Color , чтобы задать его цвет.
  • CornerRadius для задания радиуса угла.
  • WidthRequest Для задания ширины в устройствах, независимых от BoxView устройства.
  • HeightRequest , чтобы задать высоту BoxViewобъекта .

Свойство Color имеет тип Color; свойство может быть задано для любого Color значения, включая 141 статические поля только для чтения именованных цветов, начиная с алфавита AliceBlueYellowGreen.

CornerRadius Свойство имеет типCornerRadius; свойство может иметь однородное double значение радиуса угла или CornerRadius структуру, определенную четырьмя double значениями, применяемыми к верхнему левому краю, верхнему правому, нижнему левому и нижнему BoxViewправому краю.

HeightRequest Свойства WidthRequest играют роль только в том случае, если он BoxView не ограниченв макете. Это происходит в том случае, если контейнер макета должен знать размер дочернего элемента, например, когда BoxView он является дочерним элементом ячейки автомасштабирования в макете Grid . A BoxView также не ограничивается, если HorizontalOptions его свойства VerticalOptions заданы для значений, отличных от LayoutOptions.Fillзначений. BoxView Если значение не ограничено, но WidthRequestHeightRequest свойства не заданы, ширина или высота заданы по умолчанию в 40 единицах или около 1/4 дюйма на мобильных устройствах.

HeightRequest Свойства WidthRequest игнорируются, если BoxView он ограничен в макете, в этом случае контейнер макета накладывает собственный размер.BoxView

BoxView может быть ограниченным по одному измерению и неограниченным по другому. Например, если BoxView дочерний элемент вертикали, вертикальное StackLayoutизмерение BoxView не ограничено и его горизонтальное измерение обычно ограничено. Но существуют исключения для этого горизонтального измерения: если BoxView свойство HorizontalOptions имеет значение, отличное LayoutOptions.Fillот этого, горизонтальное измерение также не ограничено. Это также возможно для StackLayout самого себя иметь неограниченное горизонтальное измерение, в этом случае BoxView это также будет горизонтально не ограничено.

В примере BasicBoxView отображается квадратный квадрат без ограничений BoxView в центре страницы:

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

Ниже приведен результат:

Basic BoxView

VerticalOptionsHorizontalOptions Если свойства удаляются из BoxView тега или задаются в качестве значенияFill, BoxView он становится ограниченным размером страницы и расширяется для заполнения страницы.

Может BoxView также быть дочерним элементом AbsoluteLayout. В этом случае расположение и размер BoxView задаются с помощью LayoutBounds присоединенного привязываемого свойства. Рассматривается AbsoluteLayout в статье AbsoluteLayout.

Вы увидите примеры всех этих случаев в приведенных ниже примерах программ.

Оформление текста отрисовки

Можно использовать BoxView для добавления простых украшений на страницах в виде горизонтальных и вертикальных линий. Пример TextDecoration демонстрирует это. Все визуальные элементы программы определяются в файле MainPage.xaml , который содержит несколько Label элементов BoxView , приведенных StackLayout здесь:

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

Все следующие разметки являются дочерними элементами StackLayout. Эта разметка состоит из нескольких типов декоративных BoxView элементов, используемых с элементом Label :

Text Decoration

Стильный заголовок в верхней части страницы достигается с AbsoluteLayout дочерними элементами, четырьмя BoxView элементами и Labelвсеми из которых назначены определенные расположения и размеры:

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

В XAML-файле AbsoluteLayout за ним следует Label форматированный текст, описывающий .AbsoluteLayout

Вы можете подчеркнуть текстовую строку, заключив LabelBoxView и в нее StackLayout значение HorizontalOptions , которое имеет значение, отличное от Fillзначения. Ширина затем регулируется шириной StackLayoutLabel, которая затем накладывает на нее BoxViewширину. Назначается BoxView только явная высота:

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

Этот метод нельзя использовать для подчеркивания отдельных слов в длинных текстовых строках или абзаце.

Кроме того, можно использовать BoxView элемент HTML hr (горизонтальное правило). Просто позвольте ширине родительского BoxView контейнера определяться следующим образом:StackLayout

<BoxView HeightRequest="3" />

Наконец, можно нарисовать вертикальную линию на одной стороне абзаца текста, заключив как BoxView в горизонтальную, так и Label горизонтальную StackLayout. В этом случае высота BoxView объекта совпадает с высотой StackLayout, которая регулируется высотой Label:

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

        ···

    </Label>
</StackLayout>

Перечисление цветов с помощью BoxView

Удобно BoxView отображать цвета. Эта программа использует ListView для перечисления всех общедоступных статических полей структуры, доступных Xamarin.FormsColor только для чтения:

ListView Colors

Программа ListViewColors включает класс с именем NamedColor. Статический конструктор использует отражение для доступа ко всем полям Color структуры и созданию NamedColor объекта для каждого из них. Они хранятся в статическом 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; }
}

Визуальные элементы программы описаны в XAML-файле. ListView Свойство ItemsSource присваивается статическому NamedColor.All свойству, что означает, что ListView отображаются все отдельные 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>

Объекты NamedColor форматируются ViewCell объектом, заданным в качестве шаблона данных объекта ListView. Этот шаблон включает BoxView свойство, свойство которого Color привязано к Color свойству NamedColor объекта.

Играя в игру жизни подклассом BoxView

Игра жизни - это сотовый автомат, изобретенный математиком Джоном Конуэем и популяризирован на страницах Научного американского в 1970-х годах. Хорошее введение представлено в статье Википедии о игре жизни Конуэя.

Программа Xamarin.FormsGameOfLife определяет класс с именем LifeCell , производным от BoxView. Этот класс инкапсулирует логику отдельной ячейки в игре жизни:

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добавляет три дополнительных свойства в : Col и Row свойства BoxViewхранят положение ячейки в сетке, а IsAlive свойство указывает его состояние. Свойство IsAlive также задает Color свойство BoxView черного цвета, если ячейка жива, и белая, если ячейка не жива.

LifeCell Также устанавливается, TapGestureRecognizer чтобы разрешить пользователю переключать состояние ячеек, касаясь их. Класс преобразует Tapped событие из распознавателя жестов в собственное Tapped событие.

Программа GameOfLife также включает LifeGrid класс, который инкапсулирует большую часть логики игры, и MainPage класс, который обрабатывает визуальные элементы программы. К ним относится наложение, описывающее правила игры. Ниже приведена программа в действии с несколькими сотнями LifeCell объектов на странице:

Game of Life

Создание цифровых часов

Программа DotMatrixClock создает 210 BoxView элементов, чтобы имитировать точки старомодного отображения 5–7 точечной матрицы. Вы можете прочитать время в книжном или альбомном режиме, но оно больше в альбомном режиме:

Dot-Matrix Clock

XAML-файл создает экземпляры AbsoluteLayout , используемые для часов:

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

Все остальное происходит в файле кода программной части. Логика отображения dot-matrix значительно упрощается определением нескольких массивов, описывающих точки, соответствующие каждой из 10 цифр и двоеточию:

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

    ···

}

Эти поля заканчивают трехмерным массивом BoxView элементов для хранения шаблонов точек для шести цифр.

Конструктор создает все BoxView элементы для цифр и двоеточия, а также инициализирует Color свойство BoxView элементов для двоеточия:

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

    ···

}

В этой программе используется функция AbsoluteLayoutотносительного позиционирования и изменения размера. Ширина и высота каждого BoxView значения задаются дробными значениями, в частности, 85% от 1, разделенных на количество горизонтальных и вертикальных точек. Позиции также задаются для дробных значений.

Так как все позиции и размеры относительно общего размера AbsoluteLayoutстраницы, SizeChanged обработчик страницы должен задать только следующее 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;
    }

    ···

}

Ширина AbsoluteLayout страницы устанавливается автоматически, так как она растянута до полной ширины страницы.

Окончательный код в MainPage классе обрабатывает обратный вызов таймера и цветет точки каждой цифры. Определение многомерных массивов в начале файла программной части помогает сделать эту логику самой простой частью программы:

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

Создание аналоговых часов

Часы с точками матрицы могут показаться очевидным применением BoxView, но BoxView элементы также способны реализовать аналоговые часы:

BoxView Clock

Все визуальные элементы в программе BoxViewClock являются дочерними элементами AbsoluteLayout. Эти элементы имеют размер с помощью присоединенного LayoutBounds свойства и вращаются с помощью Rotation свойства.

BoxView Три элемента для рук часов создаются в XAML-файле, но не расположены или не расположены:

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

Конструктор файла code-behind создает экземпляры 60 BoxView элементов для тиковых меток вокруг окружности часов:

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

    ···

}

Размер и размещение всех BoxView элементов выполняется в обработчике SizeChangedAbsoluteLayout. Небольшая структура, внутренняя для класса, называемая HandParams , описывает размер каждой из трех рук относительно общего размера часов:

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

    ···

 }

Обработчик SizeChanged определяет центр и радиус AbsoluteLayout, а затем размер и позиционирует 60 BoxView элементов, используемых в качестве меток. Цикл for завершается путем задания Rotation свойства каждого из этих BoxView элементов. В конце обработчика SizeChangedLayoutHand метод вызывается для размера и размещения трех рук часов:

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

    ···

}

Размеры LayoutHand методов и позиции каждой руки, чтобы указать прямо до позиции 12:00. В конце метода AnchorY свойство устанавливается в положение, соответствующее центру часов. Это означает центр поворота.

Руки поворачиваются в функцию обратного вызова таймера:

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

Вторая рука обрабатывается немного по-другому: анимация упрощает функцию, чтобы сделать движение кажется механическим, а не гладким. На каждом галочку, вторая рука оттягивает немного, а затем перехозяет свое назначение. Этот немного кода добавляет много к реалистичности движения.