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 deBoxView
en unidades independientes del dispositivo.HeightRequest
para establecer el alto deBoxView
.
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:
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
:
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
:
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:
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:
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:
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.