Membuat Tata Letak Kustom di Xamarin.Forms
Xamarin.Forms mendefinisikan lima kelas tata letak – StackLayout, AbsoluteLayout, RelativeLayout, Grid, dan FlexLayout, dan masing-masing mengatur turunannya dengan cara yang berbeda. Namun, terkadang perlu untuk mengatur konten halaman menggunakan tata letak yang tidak disediakan oleh Xamarin.Forms. Artikel ini menjelaskan cara menulis kelas tata letak kustom, dan menunjukkan kelas WrapLayout peka orientasi yang mengatur anak-anaknya secara horizontal di seluruh halaman, lalu membungkus tampilan anak-anak berikutnya ke baris tambahan.
Dalam Xamarin.Forms, semua kelas tata letak berasal dari Layout<T>
kelas dan membatasi jenis generik ke View
dan jenis turunannya. Pada gilirannya Layout<T>
, kelas berasal dari Layout
kelas , yang menyediakan mekanisme untuk memosisikan dan mengukur elemen anak.
Setiap elemen visual bertanggung jawab untuk menentukan ukuran pilihannya sendiri, yang dikenal sebagai ukuran yang diminta . Page
, Layout
, dan Layout<View>
jenis turunan bertanggung jawab untuk menentukan lokasi dan ukuran anak mereka, atau anak-anak mereka, relatif terhadap diri mereka sendiri. Oleh karena itu, tata letak melibatkan hubungan induk-anak, di mana induk menentukan ukuran anak-anaknya, tetapi akan mencoba mengakomodasi ukuran anak yang diminta.
Pemahaman menyeluruh tentang Xamarin.Forms siklus tata letak dan pembatalan diperlukan untuk membuat tata letak kustom. Siklus ini sekarang akan dibahas.
Tata letak
Tata letak dimulai di bagian atas pohon visual dengan halaman, dan berlanjut melalui semua cabang pohon visual untuk mencakup setiap elemen visual pada halaman. Elemen yang merupakan orang tua untuk elemen lain bertanggung jawab untuk mengukur dan memposisikan anak-anak mereka relatif terhadap diri mereka sendiri.
Kelas VisualElement
menentukan Measure
metode yang mengukur elemen untuk operasi tata letak, dan Layout
metode yang menentukan area persegi panjang elemen akan dirender di dalamnya. Ketika aplikasi dimulai dan halaman pertama ditampilkan, siklus tata letak yang terdiri dari Measure
panggilan pertama, lalu Layout
memanggil, dimulai pada Page
objek:
- Selama siklus tata letak, setiap elemen induk bertanggung jawab untuk memanggil metode pada turunannya
Measure
. - Setelah anak-anak diukur, setiap elemen induk bertanggung jawab untuk memanggil
Layout
metode pada anak-anaknya.
Siklus ini memastikan bahwa setiap elemen visual di halaman menerima panggilan ke Measure
metode dan Layout
. Proses ini diperlihatkan dalam diagram berikut:
Catatan
Perhatikan bahwa siklus tata letak juga dapat terjadi pada subset pohon visual jika terjadi perubahan pada tata letak. Ini termasuk item yang ditambahkan atau dihapus dari koleksi seperti dalam StackLayout
, perubahan properti IsVisible
elemen, atau perubahan ukuran elemen.
Setiap Xamarin.Forms kelas yang memiliki Content
properti atau Children
memiliki metode yang dapat LayoutChildren
diganti. Kelas tata letak kustom yang berasal dari Layout<View>
harus mengambil alih metode ini dan memastikan bahwa Measure
metode dan Layout
dipanggil pada semua turunan elemen, untuk menyediakan tata letak kustom yang diinginkan.
Selain itu, setiap kelas yang berasal dari Layout
atau Layout<View>
harus mengambil alih OnMeasure
metode , yang merupakan tempat kelas tata letak menentukan ukuran yang diperlukan dengan melakukan panggilan ke Measure
metode anak-anaknya.
Catatan
Elemen menentukan ukurannya berdasarkan batasan, yang menunjukkan berapa banyak ruang yang tersedia untuk elemen dalam induk elemen. Batasan yang diteruskan ke Measure
metode dan OnMeasure
dapat berkisar dari 0 hingga Double.PositiveInfinity
. Elemen dibatasi, atau sepenuhnya dibatasi, ketika menerima panggilan ke metodenya Measure
dengan argumen tidak terbatas - elemen dibatasi ke ukuran tertentu. Elemen tidak dibatasi, atau dibatasi sebagian, ketika menerima panggilan ke metodenya Measure
dengan setidaknya satu argumen yang sama Double.PositiveInfinity
dengan - batasan tak terbatas dapat dianggap sebagai mengindikasikan autosizing.
Pembatalan
Pembatalan adalah proses di mana perubahan elemen pada halaman memicu siklus tata letak baru. Elemen dianggap tidak valid ketika tidak lagi memiliki ukuran atau posisi yang benar. Misalnya, jika FontSize
properti perubahan Button
, Button
dikatakan tidak valid karena tidak akan lagi memiliki ukuran yang benar. Mengubah ukuran Button
mungkin kemudian memiliki efek riak perubahan tata letak melalui halaman lainnya.
Elemen membatalkan sendiri dengan memanggil InvalidateMeasure
metode , umumnya ketika properti elemen berubah yang mungkin mengakibatkan ukuran baru elemen. Metode ini mengaktifkan MeasureInvalidated
peristiwa, yang ditangani induk elemen untuk memicu siklus tata letak baru.
Kelas Layout
mengatur handler untuk MeasureInvalidated
peristiwa pada setiap anak yang ditambahkan ke properti atau Children
koleksinyaContent
, dan melepaskan handler saat anak dihapus. Oleh karena itu, setiap elemen di pohon visual yang memiliki anak diperingatkan setiap kali salah satu anaknya berubah ukuran. Diagram berikut mengilustrasikan bagaimana perubahan ukuran elemen di pohon visual dapat menyebabkan perubahan yang menyobek pohon:
Namun, Layout
kelas mencoba membatasi dampak perubahan ukuran anak pada tata letak halaman. Jika tata letak dibatasi ukuran, perubahan ukuran anak tidak memengaruhi sesuatu yang lebih tinggi dari tata letak induk di pohon visual. Namun, biasanya perubahan ukuran tata letak memengaruhi bagaimana tata letak mengatur anak-anaknya. Oleh karena itu, setiap perubahan ukuran tata letak akan memulai siklus tata letak untuk tata letak, dan tata letak akan menerima panggilan ke metode dannya OnMeasure
LayoutChildren
.
Kelas ini Layout
juga mendefinisikan InvalidateLayout
metode yang memiliki tujuan InvalidateMeasure
serupa dengan metode . Metode InvalidateLayout
harus dipanggil setiap kali perubahan dilakukan yang memengaruhi bagaimana posisi tata letak dan ukuran anak-anaknya. Misalnya, Layout
kelas memanggil InvalidateLayout
metode setiap kali anak ditambahkan ke atau dihapus dari tata letak.
InvalidateLayout
dapat ditimpa untuk mengimplementasikan cache untuk meminimalkan pemanggilan berulang metode Measure
anak tata letak. Mengesampingkan InvalidateLayout
metode akan memberikan pemberitahuan kapan anak ditambahkan ke atau dihapus dari tata letak. Demikian pula, OnChildMeasureInvalidated
metode ini dapat ditimpa untuk memberikan pemberitahuan ketika salah satu anak tata letak berubah ukuran. Untuk kedua metode mengambil alih, tata letak kustom harus merespons dengan menghapus cache. Untuk informasi selengkapnya, lihat Menghitung dan Cache Data Tata Letak.
Membuat Tata Letak Kustom
Proses untuk membuat tata letak kustom adalah sebagai berikut:
Buat kelas yang berasal dari kelas
Layout<View>
. Untuk informasi selengkapnya, lihat Membuat WrapLayout.[opsional] Tambahkan properti, didukung oleh properti yang dapat diikat, untuk parameter apa pun yang harus diatur pada kelas tata letak. Untuk informasi selengkapnya, lihat Menambahkan Properti yang Didukung oleh Properti yang Dapat Diikat.
Ambil alih
OnMeasure
metode untuk memanggilMeasure
metode pada semua anak tata letak, dan mengembalikan ukuran yang diminta untuk tata letak. Untuk informasi selengkapnya, lihat Mengambil alih Metode OnMeasure.Ambil alih
LayoutChildren
metode untuk memanggilLayout
metode pada semua anak tata letak. Kegagalan untuk memanggilLayout
metode pada setiap anak dalam tata letak akan mengakibatkan anak tidak pernah menerima ukuran atau posisi yang benar, dan karenanya anak tidak akan terlihat di halaman. Untuk informasi selengkapnya, lihat Mengambil alih Metode LayoutChildren.Catatan
Saat menghitung anak-anak dalam
OnMeasure
danLayoutChildren
mengambil alih, lewati anak mana pun yang propertinyaIsVisible
diatur kefalse
. Ini akan memastikan bahwa tata letak kustom tidak akan meninggalkan ruang untuk anak yang tidak terlihat.[opsional] Ambil alih metode yang
InvalidateLayout
akan diberi tahu ketika anak ditambahkan ke atau dihapus dari tata letak. Untuk informasi selengkapnya, lihat Mengambil alih Metode InvalidateLayout.[opsional] Ambil alih metode yang
OnChildMeasureInvalidated
akan diberi tahu ketika salah satu anak tata letak berubah ukuran. Untuk informasi selengkapnya, lihat Mengambil alih Metode OnChildMeasureInvalidated.
Catatan
Perhatikan bahwa penimpaan OnMeasure
tidak akan dipanggil jika ukuran tata letak diatur oleh induknya, bukan turunannya. Namun, penimpaan akan dipanggil jika satu atau kedua batasan tidak terbatas, atau jika kelas tata letak memiliki nilai non-default HorizontalOptions
atau VerticalOptions
properti. Untuk alasan ini, penimpaan LayoutChildren
tidak dapat mengandalkan ukuran anak yang diperoleh selama OnMeasure
panggilan metode. Sebagai gantinyaMeasure
, LayoutChildren
harus memanggil metode pada anak tata letak, sebelum memanggil Layout
metode . Atau, ukuran anak yang diperoleh dalam OnMeasure
penimpaan dapat di-cache untuk menghindari pemanggilan nanti Measure
dalam LayoutChildren
penimpaan, tetapi kelas tata letak perlu mengetahui kapan ukuran perlu diperoleh lagi. Untuk informasi selengkapnya, lihat Menghitung dan Cache Data Tata Letak.
Kelas tata letak kemudian dapat dikonsumsi dengan menambahkannya ke Page
, dan dengan menambahkan anak ke tata letak. Untuk informasi selengkapnya, lihat Menggunakan WrapLayout.
Membuat WrapLayout
Aplikasi sampel menunjukkan kelas sensitif WrapLayout
orientasi yang mengatur anak-anaknya secara horizontal di seluruh halaman, lalu membungkus tampilan anak-anak berikutnya ke baris tambahan.
Kelas WrapLayout
mengalokasikan jumlah ruang yang sama untuk setiap anak, yang dikenal sebagai ukuran sel, berdasarkan ukuran maksimum anak. Anak-anak yang lebih kecil dari ukuran sel dapat diposisikan dalam sel berdasarkan nilai properti dan VerticalOptions
miliknyaHorizontalOptions
.
Definisi WrapLayout
kelas ditampilkan dalam contoh kode berikut:
public class WrapLayout : Layout<View>
{
Dictionary<Size, LayoutData> layoutDataCache = new Dictionary<Size, LayoutData>();
...
}
Hitung dan Cache Data Tata Letak
Struktur LayoutData
menyimpan data tentang kumpulan anak-anak di sejumlah properti:
VisibleChildCount
– jumlah anak yang terlihat dalam tata letak.CellSize
– ukuran maksimum semua anak, disesuaikan dengan ukuran tata letak.Rows
– jumlah baris.Columns
– jumlah kolom.
Bidang layoutDataCache
digunakan untuk menyimpan beberapa LayoutData
nilai. Ketika aplikasi dimulai, dua LayoutData
objek akan di-cache ke dalam layoutDataCache
kamus untuk orientasi saat ini - satu untuk argumen batasan untuk OnMeasure
penimpaan, dan satu untuk width
argumen dan height
ke LayoutChildren
penimpaan. Saat memutar perangkat menjadi orientasi lanskap, OnMeasure
penimpaan dan LayoutChildren
penimpaan akan kembali dipanggil, yang akan mengakibatkan dua LayoutData
objek lain di-cache ke dalam kamus. Namun, saat mengembalikan perangkat ke orientasi potret, tidak ada perhitungan lebih lanjut yang diperlukan karena layoutDataCache
sudah memiliki data yang diperlukan.
Contoh kode berikut menunjukkan GetLayoutData
metode , yang menghitung properti LayoutData
terstruktur berdasarkan ukuran tertentu:
LayoutData GetLayoutData(double width, double height)
{
Size size = new Size(width, height);
// Check if cached information is available.
if (layoutDataCache.ContainsKey(size))
{
return layoutDataCache[size];
}
int visibleChildCount = 0;
Size maxChildSize = new Size();
int rows = 0;
int columns = 0;
LayoutData layoutData = new LayoutData();
// Enumerate through all the children.
foreach (View child in Children)
{
// Skip invisible children.
if (!child.IsVisible)
continue;
// Count the visible children.
visibleChildCount++;
// Get the child's requested size.
SizeRequest childSizeRequest = child.Measure(Double.PositiveInfinity, Double.PositiveInfinity);
// Accumulate the maximum child size.
maxChildSize.Width = Math.Max(maxChildSize.Width, childSizeRequest.Request.Width);
maxChildSize.Height = Math.Max(maxChildSize.Height, childSizeRequest.Request.Height);
}
if (visibleChildCount != 0)
{
// Calculate the number of rows and columns.
if (Double.IsPositiveInfinity(width))
{
columns = visibleChildCount;
rows = 1;
}
else
{
columns = (int)((width + ColumnSpacing) / (maxChildSize.Width + ColumnSpacing));
columns = Math.Max(1, columns);
rows = (visibleChildCount + columns - 1) / columns;
}
// Now maximize the cell size based on the layout size.
Size cellSize = new Size();
if (Double.IsPositiveInfinity(width))
cellSize.Width = maxChildSize.Width;
else
cellSize.Width = (width - ColumnSpacing * (columns - 1)) / columns;
if (Double.IsPositiveInfinity(height))
cellSize.Height = maxChildSize.Height;
else
cellSize.Height = (height - RowSpacing * (rows - 1)) / rows;
layoutData = new LayoutData(visibleChildCount, cellSize, rows, columns);
}
layoutDataCache.Add(size, layoutData);
return layoutData;
}
Metode ini GetLayoutData
melakukan operasi berikut:
- Ini menentukan apakah nilai terhitung
LayoutData
sudah ada di cache dan mengembalikannya jika tersedia. - Jika tidak, ia menghitung melalui semua anak, memanggil
Measure
metode pada setiap anak dengan lebar dan tinggi tak terbatas, dan menentukan ukuran anak maksimum. - Asalkan setidaknya ada satu anak yang terlihat, ini menghitung jumlah baris dan kolom yang diperlukan, lalu menghitung ukuran sel untuk anak berdasarkan dimensi
WrapLayout
. Perhatikan bahwa ukuran sel biasanya sedikit lebih lebar dari ukuran anak maksimum, tetapi juga bisa lebih kecil jikaWrapLayout
tidak cukup lebar untuk anak terluas atau cukup tinggi untuk anak tertinggi. - Ini menyimpan nilai baru
LayoutData
dalam cache.
Tambahkan Properti yang Didukung oleh Properti yang Dapat Diikat
Kelas WrapLayout
menentukan ColumnSpacing
dan RowSpacing
properti, yang nilainya digunakan untuk memisahkan baris dan kolom dalam tata letak, dan yang didukung oleh properti yang dapat diikat. Properti yang dapat diikat ditampilkan dalam contoh kode berikut:
public static readonly BindableProperty ColumnSpacingProperty = BindableProperty.Create(
"ColumnSpacing",
typeof(double),
typeof(WrapLayout),
5.0,
propertyChanged: (bindable, oldvalue, newvalue) =>
{
((WrapLayout)bindable).InvalidateLayout();
});
public static readonly BindableProperty RowSpacingProperty = BindableProperty.Create(
"RowSpacing",
typeof(double),
typeof(WrapLayout),
5.0,
propertyChanged: (bindable, oldvalue, newvalue) =>
{
((WrapLayout)bindable).InvalidateLayout();
});
Handler yang diubah properti dari setiap properti yang dapat diikat memanggil InvalidateLayout
penimpaan metode untuk memicu pass tata letak baru pada WrapLayout
. Untuk informasi selengkapnya, lihat Mengambil alih Metode InvalidateLayout dan Mengambil alih Metode OnChildMeasureInvalidated.
Mengambil alih Metode OnMeasure
Penimpaan OnMeasure
ditampilkan dalam contoh kode berikut:
protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint)
{
LayoutData layoutData = GetLayoutData(widthConstraint, heightConstraint);
if (layoutData.VisibleChildCount == 0)
{
return new SizeRequest();
}
Size totalSize = new Size(layoutData.CellSize.Width * layoutData.Columns + ColumnSpacing * (layoutData.Columns - 1),
layoutData.CellSize.Height * layoutData.Rows + RowSpacing * (layoutData.Rows - 1));
return new SizeRequest(totalSize);
}
Penimpaan GetLayoutData
memanggil metode dan membuat objek dari data yang SizeRequest
dikembalikan, sambil juga memperhitungkan RowSpacing
nilai properti dan ColumnSpacing
. Untuk informasi selengkapnya tentang metode ini GetLayoutData
, lihat Menghitung dan Cache Data Tata Letak.
Penting
Metode Measure
dan OnMeasure
tidak boleh meminta dimensi tak terbatas dengan mengembalikan SizeRequest
nilai dengan properti yang diatur ke Double.PositiveInfinity
. Namun, setidaknya salah satu argumen batasan OnMeasure
dapat berupa Double.PositiveInfinity
.
Mengambil alih Metode LayoutChildren
Penimpaan LayoutChildren
ditampilkan dalam contoh kode berikut:
protected override void LayoutChildren(double x, double y, double width, double height)
{
LayoutData layoutData = GetLayoutData(width, height);
if (layoutData.VisibleChildCount == 0)
{
return;
}
double xChild = x;
double yChild = y;
int row = 0;
int column = 0;
foreach (View child in Children)
{
if (!child.IsVisible)
{
continue;
}
LayoutChildIntoBoundingRegion(child, new Rectangle(new Point(xChild, yChild), layoutData.CellSize));
if (++column == layoutData.Columns)
{
column = 0;
row++;
xChild = x;
yChild += RowSpacing + layoutData.CellSize.Height;
}
else
{
xChild += ColumnSpacing + layoutData.CellSize.Width;
}
}
}
Penimpaan dimulai dengan panggilan ke GetLayoutData
metode , dan kemudian menghitung semua anak untuk mengukur dan memosisikannya dalam sel setiap anak. Ini dicapai dengan memanggil LayoutChildIntoBoundingRegion
metode , yang digunakan untuk memosisikan anak dalam persegi panjang berdasarkan nilai properti dan VerticalOptions
.HorizontalOptions
Ini setara dengan melakukan panggilan ke metode anak Layout
.
Catatan
Perhatikan bahwa persegi panjang yang diteruskan ke LayoutChildIntoBoundingRegion
metode mencakup seluruh area tempat anak dapat tinggal.
Untuk informasi selengkapnya tentang metode ini GetLayoutData
, lihat Menghitung dan Cache Data Tata Letak.
Mengesampingkan Metode InvalidateLayout
Penimpaan InvalidateLayout
dipanggil saat anak ditambahkan atau dihapus dari tata letak, atau ketika salah WrapLayout
satu properti mengubah nilai, seperti yang ditunjukkan dalam contoh kode berikut:
protected override void InvalidateLayout()
{
base.InvalidateLayout();
layoutInfoCache.Clear();
}
Penimpaan membatalkan tata letak dan membuang semua informasi tata letak yang di-cache.
Catatan
Untuk menghentikan Layout
kelas memanggil metode setiap kali anak ditambahkan ke atau dihapus dari tata letak, ambil alih ShouldInvalidateOnChildAdded
metode dan ShouldInvalidateOnChildRemoved
, dan kembalikan false
InvalidateLayout
. Kelas tata letak kemudian dapat menerapkan proses kustom saat anak ditambahkan atau dihapus.
Mengambil alih Metode OnChildMeasureInvalidated
Penimpaan OnChildMeasureInvalidated
dipanggil saat salah satu anak tata letak berubah ukuran, dan diperlihatkan dalam contoh kode berikut:
protected override void OnChildMeasureInvalidated()
{
base.OnChildMeasureInvalidated();
layoutInfoCache.Clear();
}
Penimpaan membatalkan tata letak anak, dan membuang semua informasi tata letak yang di-cache.
Mengonsumsi WrapLayout
Kelas WrapLayout
dapat dikonsumsi dengan menempatkannya pada jenis turunan, seperti yang Page
ditunjukkan dalam contoh kode XAML berikut:
<ContentPage ... xmlns:local="clr-namespace:ImageWrapLayout">
<ScrollView Margin="0,20,0,20">
<local:WrapLayout x:Name="wrapLayout" />
</ScrollView>
</ContentPage>
Kode C# yang setara ditunjukkan di bawah ini:
public class ImageWrapLayoutPageCS : ContentPage
{
WrapLayout wrapLayout;
public ImageWrapLayoutPageCS()
{
wrapLayout = new WrapLayout();
Content = new ScrollView
{
Margin = new Thickness(0, 20, 0, 20),
Content = wrapLayout
};
}
...
}
Anak-anak kemudian dapat ditambahkan ke WrapLayout
sesuai kebutuhan. Contoh kode berikut menunjukkan Image
elemen yang ditambahkan ke WrapLayout
:
protected override async void OnAppearing()
{
base.OnAppearing();
var images = await GetImageListAsync();
if (images != null)
{
foreach (var photo in images.Photos)
{
var image = new Image
{
Source = ImageSource.FromUri(new Uri(photo))
};
wrapLayout.Children.Add(image);
}
}
}
async Task<ImageList> GetImageListAsync()
{
try
{
string requestUri = "https://raw.githubusercontent.com/xamarin/docs-archive/master/Images/stock/small/stock.json";
string result = await _client.GetStringAsync(requestUri);
return JsonConvert.DeserializeObject<ImageList>(result);
}
catch (Exception ex)
{
Debug.WriteLine($"\tERROR: {ex.Message}");
}
return null;
}
Ketika halaman yang berisi WrapLayout
muncul, aplikasi sampel secara asinkron mengakses file JSON jarak jauh yang berisi daftar foto, membuat Image
elemen untuk setiap foto, dan menambahkannya ke WrapLayout
. Ini menghasilkan tampilan yang ditunjukkan pada cuplikan layar berikut:
Cuplikan layar berikut menunjukkan WrapLayout
setelah diputar ke orientasi lanskap:
Jumlah kolom di setiap baris tergantung pada ukuran foto, lebar layar, dan jumlah piksel per unit independen perangkat. Elemen Image
secara asinkron memuat foto, dan oleh karena itu WrapLayout
kelas akan sering menerima panggilan ke metodenya LayoutChildren
karena setiap Image
elemen menerima ukuran baru berdasarkan foto yang dimuat.