Bagikan melalui


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:

  1. Selama siklus tata letak, setiap elemen induk bertanggung jawab untuk memanggil metode pada turunannya Measure .
  2. 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:

Xamarin.Forms Siklus Tata Letak

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:

Pembatalan di Pohon Visual

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

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:

  1. Buat kelas yang berasal dari kelas Layout<View>. Untuk informasi selengkapnya, lihat Membuat WrapLayout.

  2. [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.

  3. Ambil alih OnMeasure metode untuk memanggil Measure metode pada semua anak tata letak, dan mengembalikan ukuran yang diminta untuk tata letak. Untuk informasi selengkapnya, lihat Mengambil alih Metode OnMeasure.

  4. Ambil alih LayoutChildren metode untuk memanggil Layout metode pada semua anak tata letak. Kegagalan untuk memanggil Layout 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 dan LayoutChildren mengambil alih, lewati anak mana pun yang propertinya IsVisible diatur ke false. Ini akan memastikan bahwa tata letak kustom tidak akan meninggalkan ruang untuk anak yang tidak terlihat.

  5. [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.

  6. [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 jika WrapLayout 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 falseInvalidateLayout . 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 Potret Aplikasi Sampel

Cuplikan layar berikut menunjukkan WrapLayout setelah diputar ke orientasi lanskap:

Cuplikan Layar Lanskap Aplikasi iOS SampelCuplikan Layar Lanskap Aplikasi Android SampelCuplikan Layar Lanskap Aplikasi UWP Sampel

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.