Xamarin.Forms BoxView
BoxView
rendert ein einfaches Rechteck mit einer angegebenen Breite, Höhe und Farbe. Sie können für Die Dekoration, rudimentäre Grafiken und für die Interaktion mit dem Benutzer durch Toucheingabe verwenden BoxView
.
Da Xamarin.Forms nicht über ein integriertes Vektorgrafiksystem verfügt, hilft, die BoxView
zu kompensieren. Einige der in diesem Artikel beschriebenen Beispielprogramme verwenden BoxView
zum Rendern von Grafiken. Der BoxView
kann einer Linie mit einer bestimmten Breite und Stärke ähneln und dann mithilfe der Rotation
-Eigenschaft um einen beliebigen Winkel gedreht werden.
Obwohl BoxView
einfache Grafiken imitiert werden können, sollten Sie die Verwendung von SkiaSharp in Xamarin.Forms für komplexere Grafikanforderungen untersuchen.
Festlegen von BoxView-Farbe und -Größe
In der Regel legen Sie die folgenden Eigenschaften von fest BoxView
:
Color
, um die Farbe festzulegen.CornerRadius
, um den Eckenradius festzulegen.WidthRequest
, um die Breite derBoxView
in geräteunabhängigen Einheiten festzulegen.HeightRequest
, um die Höhe vonBoxView
festzulegen.
Die Color
-Eigenschaft ist vom Typ Color
. Die -Eigenschaft kann auf einen beliebigen Color
Wert festgelegt werden, einschließlich der 141 statischen schreibgeschützten Felder benannter Farben, die alphabetisch von AliceBlue
bis reichen YellowGreen
.
Die CornerRadius
-Eigenschaft ist vom Typ CornerRadius
. Die -Eigenschaft kann auf einen einzelnen double
einheitlichen Eckradiuswert oder eine CornerRadius
Struktur festgelegt werden, die durch vier double
Werte definiert wird, die oben links, oben rechts, unten links und unten rechts des BoxView
angewendet werden.
Die WidthRequest
Eigenschaften und HeightRequest
spielen nur dann eine Rolle, wenn die BoxView
im Layout nicht eingeschränkt ist. Dies ist der Fall, wenn der Layoutcontainer die Größe des untergeordneten Elements kennen muss, z. B. wenn das BoxView
ein untergeordnetes Element einer Zelle mit automatischer Größe im Grid
Layout ist. Ein BoxView
ist auch nicht eingeschränkt, wenn seine HorizontalOptions
Eigenschaften und VerticalOptions
auf andere Werte als LayoutOptions.Fill
festgelegt sind. Wenn nicht BoxView
eingeschränkt ist, aber die WidthRequest
Eigenschaften und HeightRequest
nicht festgelegt sind, wird die Breite oder Höhe auf Standardwerte von 40 Einheiten oder etwa 1/4 Zoll auf mobilen Geräten festgelegt.
Die WidthRequest
Eigenschaften und HeightRequest
werden ignoriert, wenn die BoxView
im Layout eingeschränkt ist. In diesem Fall erzwingt der Layoutcontainer seine eigene Größe für .BoxView
Eine BoxView
kann in einer Dimension eingeschränkt und in einer anderen uneingeschränkt sein. Wenn BoxView
z. B. ein untergeordnetes Element einer vertikalen StackLayout
ist, ist die vertikale Dimension des uneingeschränkt und die BoxView
horizontale Dimension im Allgemeinen eingeschränkt. Es gibt jedoch Ausnahmen für diese horizontale Dimension: Wenn die BoxView
HorizontalOptions
-Eigenschaft auf etwas anderes als LayoutOptions.Fill
festgelegt ist, ist die horizontale Dimension ebenfalls uneingeschränkt. Es ist auch möglich, dass der StackLayout
sich selbst eine uneingeschränkte horizontale Dimension hat, in diesem Fall wird auch horizontal BoxView
uneingeschränkt sein.
Im BasicBoxView-Beispiel wird ein 1-Zoll-Quadrat ohne Einschränkungen BoxView
in der Mitte der Seite angezeigt:
<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>
Das Ergebnis lautet wie folgt:
Wenn die VerticalOptions
Eigenschaften und HorizontalOptions
aus dem BoxView
Tag entfernt oder auf Fill
festgelegt sind, wird die BoxView
durch die Größe der Seite eingeschränkt und erweitert, um die Seite zu füllen.
Ein BoxView
kann auch ein untergeordnetes Element eines AbsoluteLayout
sein. In diesem Fall werden sowohl die Position als auch die BoxView
Größe des mithilfe der LayoutBounds
angefügten bindbaren Eigenschaft festgelegt. Die AbsoluteLayout
wird im Artikel AbsoluteLayout erläutert.
Beispiele für all diese Fälle finden Sie in den folgenden Beispielprogrammen.
Rendern von Textdekorationen
Sie können verwenden BoxView
, um ihren Seiten einige einfache Dekorationen in Form von horizontalen und vertikalen Linien hinzuzufügen. Das TextDecoration-Beispiel veranschaulicht dies. Alle Visuals des Programms sind in der Datei MainPage.xaml definiert, die mehrere Label
- und BoxView
-Elemente im StackLayout
hier gezeigten enthält:
<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>
Alle folgenden Markups sind untergeordnete Elemente von StackLayout
. Dieses Markup besteht aus mehreren Arten von dekorativen BoxView
Elementen, die mit dem Label
-Element verwendet werden:
Die stilvolle Kopfzeile oben auf der Seite wird mit einem AbsoluteLayout
erreicht, dessen untergeordnete Elemente vier BoxView
Elemente sind, und ein Label
, denen alle bestimmten Speicherorten und Größen zugewiesen sind:
<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>
In der XAML-Datei folgt ein AbsoluteLayout
Label
mit formatiertem Text, der die AbsoluteLayout
beschreibt.
Sie können eine Textzeichenfolge unterstreichen, indem Sie sowohl die Label
als BoxView
auch in eine StackLayout
umschließen, deren HorizontalOptions
Wert auf etwas anderes als Fill
festgelegt ist. Die Breite von StackLayout
wird dann durch die Breite von Label
bestimmt, die dann diese Breite für das BoxView
aufzwingt. Dem BoxView
wird nur eine explizite Höhe zugewiesen:
<StackLayout HorizontalOptions="Center">
<Label Text="Underlined Text"
FontSize="24" />
<BoxView HeightRequest="2" />
</StackLayout>
Sie können diese Technik nicht verwenden, um einzelne Wörter in längeren Textzeichenfolgen oder einem Absatz zu unterstreichen.
Es ist auch möglich, ein BoxView
zu verwenden, um einem HTML-Element hr
(horizontale Regel) zu ähneln. Lassen Sie einfach die Breite des BoxView
durch den übergeordneten Container bestimmen, der in diesem Fall der StackLayout
ist:
<BoxView HeightRequest="3" />
Schließlich können Sie eine vertikale Linie auf einer Seite eines Textabsatzs zeichnen, indem Sie sowohl die BoxView
als auch die Label
in einer horizontalen StackLayout
einschließen. In diesem Fall ist die Höhe des BoxView
identisch mit der Höhe von StackLayout
, die von der Höhe des Label
bestimmt wird:
<StackLayout Orientation="Horizontal">
<BoxView WidthRequest="4"
Margin="0, 0, 10, 0" />
<Label>
···
</Label>
</StackLayout>
Auflisten von Farben mit BoxView
Eignet BoxView
sich für die Anzeige von Farben. Dieses Programm verwendet ein ListView
, um alle öffentlichen statischen schreibgeschützten Felder der Xamarin.FormsColor
-Struktur auflisten:
Das ListViewColors-Programm enthält eine Klasse mit dem Namen NamedColor
. Der statische Konstruktor verwendet Reflektion, um auf alle Felder der Color
Struktur zuzugreifen und für jedes objekt ein NamedColor
-Objekt zu erstellen. Diese werden in der statischen All
Eigenschaft gespeichert:
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; }
}
Die Programmvisuals werden in der XAML-Datei beschrieben. Die ItemsSource
-Eigenschaft von ListView
wird auf die statische NamedColor.All
Eigenschaft festgelegt, d. h., dass alle ListView
einzelnen NamedColor
Objekte angezeigt werden:
<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>
Die NamedColor
-Objekte werden von dem ViewCell
-Objekt formatiert, das als Datenvorlage des ListView
festgelegt ist. Diese Vorlage enthält eine BoxView
, deren Color
Eigenschaft an die Color
-Eigenschaft des NamedColor
-Objekts gebunden ist.
Spielen des Lebensspiels durch Unterklassifizierung von BoxView
The Game of Life ist ein zellulärer Automat, der vom Mathematiker John Conway erfunden und in den Seiten von Scientific American in den 1970er Jahren populär gemacht wurde. Eine gute Einführung bietet der Wikipedia-Artikel Conways Spiel des Lebens.
Das Xamarin.FormsGameOfLife-Programm definiert eine Klasse namens LifeCell
, die von abgeleitet wird BoxView
. Diese Klasse kapselt die Logik einer einzelnen Zelle im Spiel des Lebens:
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
fügt drei weitere Eigenschaften hinzu BoxView
: Die Col
Eigenschaften und Row
speichern die Position der Zelle innerhalb des Rasters, und die -Eigenschaft gibt ihren IsAlive
Zustand an. Die IsAlive
-Eigenschaft legt auch die Color
-Eigenschaft von BoxView
auf schwarz fest, wenn die Zelle aktiv ist, und weiß, wenn die Zelle nicht aktiv ist.
LifeCell
installiert außerdem ein TapGestureRecognizer
, damit der Benutzer den Zustand der Zellen durch Tippen umschalten kann. Die -Klasse übersetzt das Tapped
Ereignis von der Gestenerkennung in ein eigenes Tapped
Ereignis.
Das GameOfLife-Programm enthält auch eine LifeGrid
Klasse, die einen Großteil der Logik des Spiels kapselt, und eine MainPage
Klasse, die die Visuals des Programms behandelt. Dazu gehört ein Overlay, das die Regeln des Spiels beschreibt. Hier ist das Programm in Aktion, das ein paar hundert LifeCell
Objekte auf der Seite zeigt:
Erstellen einer digitalen Uhr
Das DotMatrixClock-Programm erstellt 210 BoxView
Elemente, um die Punkte einer altmodisch 5-mal-7-Punktmatrix-Anzeige zu simulieren. Sie können die Zeit entweder im Hoch- oder Querformat lesen, aber sie ist im Querformat größer:
Die XAML-Datei führt kaum mehr aus, als die für die AbsoluteLayout
Uhr verwendete instanziieren:
<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>
Alles andere geschieht in der CodeBehind-Datei. Die Anzeigelogik der Punktmatrix wird durch die Definition mehrerer Arrays erheblich vereinfacht, die die Punkte beschreiben, die jeder der 10 Ziffern und einem Doppelpunkt entsprechen:
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];
···
}
Diese Felder schließen mit einem dreidimensionalen Array von BoxView
Elementen zum Speichern der Punktmuster für die sechs Ziffern ab.
Der Konstruktor erstellt alle BoxView
Elemente für die Ziffern und den Doppelpunkt und initialisiert auch die Color
-Eigenschaft der BoxView
Elemente für den Doppelpunkt:
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();
}
···
}
Dieses Programm verwendet die relative Positionierungs- und Größenanpassungsfunktion von AbsoluteLayout
. Die Breite und Höhe der einzelnen BoxView
Werte sind auf Bruchwerte festgelegt, insbesondere 85 % von 1 dividiert durch die Anzahl der horizontalen und vertikalen Punkte. Die Positionen werden auch auf Bruchwerte festgelegt.
Da alle Positionen und Größen relativ zur Gesamtgröße von AbsoluteLayout
sind, muss der SizeChanged
Handler für die Seite nur einen HeightRequest
von AbsoluteLayout
festlegen:
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;
}
···
}
Die Breite von AbsoluteLayout
wird automatisch festgelegt, da sie auf die volle Breite der Seite gestreckt wird.
Der endgültige Code in der MainPage
-Klasse verarbeitet den Timerrückruf und ordnet die Punkte jeder Ziffer ein. Die Definition der mehrdimensionalen Arrays am Anfang der CodeBehind-Datei trägt dazu bei, diese Logik zum einfachsten Teil des Programms zu machen:
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;
}
}
}
Erstellen einer analogen Uhr
Eine Punktmatrixuhr scheint eine offensichtliche Anwendung von BoxView
zu sein, aber BoxView
Elemente sind auch in der Lage, eine analoge Uhr zu realisieren:
Alle Visuals im BoxViewClock-Programm sind untergeordnete Elemente von AbsoluteLayout
. Diese Elemente werden mithilfe der LayoutBounds
angefügten Eigenschaft dimensioniert und mithilfe der Rotation
-Eigenschaft gedreht.
Die drei BoxView
Elemente für die Zeiger der Uhr werden in der XAML-Datei instanziiert, aber nicht positioniert oder dimensioniert:
<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>
Der Konstruktor der CodeBehind-Datei instanziiert die 60 BoxView
Elemente für die Teilstriche um den Umfang der Uhr:
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);
}
···
}
Die Größenanpassung und Positionierung aller BoxView
Elemente erfolgt im SizeChanged
Handler für .AbsoluteLayout
Eine kleine interne Struktur der Klasse namens HandParams
beschreibt die Größe jeder der drei Zeiger im Verhältnis zur Gesamtgröße der Uhr:
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);
···
}
Der SizeChanged
Handler bestimmt die Mitte und den Radius von AbsoluteLayout
, und dann werden die 60 BoxView
Elemente, die als Häkchen verwendet werden, größen und positioniert. Die for
Schleife schließt durch Festlegen der Rotation
Eigenschaft jedes dieser BoxView
Elemente. Am Ende des SizeChanged
Handlers wird die LayoutHand
-Methode aufgerufen, um die drei Zeiger der Uhr zu vergrößern und zu positionieren:
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;
}
···
}
Mit LayoutHand
der Methode wird jede Hand so groß und positioniert, dass sie gerade auf die 12:00-Position zeigt. Am Ende der -Methode wird die AnchorY
-Eigenschaft auf eine Position festgelegt, die der Mitte der Uhr entspricht. Dies gibt den Drehpunkt an.
Die Zeiger werden in der Timer-Rückruffunktion gedreht:
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;
}
}
Die zweite Hand wird etwas anders behandelt: Eine Animationslockerungsfunktion wird angewendet, um das Uhrwerk mechanisch statt glatt erscheinen zu lassen. Bei jedem Tick zieht der Zweite ein wenig zurück und überschießt dann sein Ziel. Dieses wenig Code trägt viel zum Realismus der Bewegung bei.