Tata Letak Terlampir
Kontainer (misalnya, Panel) yang mendelegasikan logika tata letaknya ke objek lain bergantung pada objek tata letak terlampir untuk menyediakan perilaku tata letak untuk elemen anaknya. Model tata letak terlampir memberikan fleksibilitas bagi aplikasi untuk mengubah tata letak item pada runtime, atau lebih mudah berbagi aspek tata letak antara bagian UI yang berbeda (misalnya, item dalam baris tabel yang tampaknya selaras dalam kolom).
Dalam topik ini, kami membahas apa yang terlibat dalam membuat tata letak terlampir (virtualisasi dan non-virtualisasi), konsep dan kelas yang perlu Anda pahami, dan trade-off yang perlu Anda pertimbangkan saat memutuskan di antara mereka.
Dapatkan WinUI |
---|
Kontrol ini disertakan sebagai bagian dari WinUI, paket NuGet yang berisi kontrol baru dan fitur UI untuk aplikasi Windows. Untuk informasi selengkapnya, termasuk instruksi penginstalan, lihat gambaran umum WinUI. |
API penting:
Konsep utama
Melakukan tata letak mengharuskan dua pertanyaan dijawab untuk setiap elemen:
Berapa ukuran elemen ini?
Apa posisi elemen ini?
Sistem tata letak XAML, yang menjawab pertanyaan-pertanyaan ini, dibahas secara singkat sebagai bagian dari diskusi panel Kustom.
Kontainer dan Konteks
Secara konseptual, Panel XAML mengisi dua peran penting dalam kerangka kerja:
- Ini dapat berisi elemen anak dan memperkenalkan percabangan di pohon elemen.
- Ini menerapkan strategi tata letak tertentu untuk anak-anak tersebut.
Untuk alasan ini, Panel di XAML sering kali identik dengan tata letak, tetapi secara teknis, melakukan lebih dari sekadar tata letak.
ItemsRepeater juga berperilaku seperti Panel, tetapi, tidak seperti Panel, tidak mengekspos properti Children yang akan memungkinkan penambahan atau penghapusan anak UIElement secara terprogram. Sebaliknya, masa pakai anak-anaknya dikelola secara otomatis oleh kerangka kerja agar sesuai dengan kumpulan item data. Meskipun tidak berasal dari Panel, itu bersifat dan diperlakukan oleh kerangka kerja seperti Panel.
Catatan
LayoutPanel adalah kontainer, berasal dari Panel, yang mendelegasikan logikanya ke objek Tata Letak yang terpasang. LayoutPanel dalam Pratinjau dan saat ini hanya tersedia di drop Prarilis paket WinUI.
Kontainer
Secara konseptual, Panel adalah kontainer elemen yang juga memiliki kemampuan untuk merender piksel untuk Latar Belakang. Panel menyediakan cara untuk merangkum logika tata letak umum dalam paket yang mudah digunakan.
Konsep tata letak terlampir membuat perbedaan antara dua peran kontainer dan tata letak lebih jelas. Jika kontainer mendelegasikan logika tata letaknya ke objek lain, kami akan memanggil objek tersebut sebagai tata letak terlampir seperti yang terlihat dalam cuplikan di bawah ini. Kontainer yang mewarisi dari FrameworkElement, seperti LayoutPanel, secara otomatis mengekspos properti umum yang menyediakan input ke proses tata letak XAML (misalnya, Tinggi dan Lebar).
<LayoutPanel>
<LayoutPanel.Layout>
<UniformGridLayout/>
</LayoutPanel.Layout>
<Button Content="1"/>
<Button Content="2"/>
<Button Content="3"/>
</LayoutPanel>
Selama proses tata letak, kontainer bergantung pada UniformGridLayout yang terpasang untuk mengukur dan mengatur turunannya.
Status Per Kontainer
Dengan tata letak terlampir, satu instans objek tata letak dapat dikaitkan dengan banyak kontainer seperti dalam cuplikan di bawah ini; oleh karena itu, itu tidak boleh bergantung pada atau langsung mereferensikan kontainer host. Contohnya:
<!-- ... --->
<Page.Resources>
<ExampleLayout x:Name="exampleLayout"/>
<Page.Resources>
<LayoutPanel x:Name="example1" Layout="{StaticResource exampleLayout}"/>
<LayoutPanel x:Name="example2" Layout="{StaticResource exampleLayout}"/>
<!-- ... --->
Untuk situasi ini, ExampleLayout harus mempertimbangkan dengan cermat status yang digunakannya dalam perhitungan tata letaknya dan di mana status tersebut disimpan untuk menghindari berdampak pada tata letak untuk elemen di satu panel dengan panel lainnya. Ini akan dianalogikan ke Panel kustom yang logika MeasureOverride dan ArrangeOverride tergantung pada nilai properti statisnya.
LayoutContext
Tujuan dari LayoutContext adalah untuk menangani tantangan tersebut. Ini menyediakan tata letak terlampir kemampuan untuk berinteraksi dengan kontainer host, seperti mengambil elemen anak, tanpa memperkenalkan dependensi langsung antara keduanya. Konteks ini juga memungkinkan tata letak untuk menyimpan status apa pun yang diperlukan yang mungkin terkait dengan elemen turunan kontainer.
Tata letak sederhana dan tidak divirtualisasi sering kali tidak perlu mempertahankan status apa pun, menjadikannya non-masalah. Tata letak yang lebih kompleks, seperti Grid, bagaimanapun, dapat memilih untuk mempertahankan status antara pengukuran dan mengatur panggilan untuk menghindari komputasi ulang nilai.
Virtualisasi tata letak sering kali perlu mempertahankan beberapa status antara ukuran dan susun serta antara tata letak berulang berlalu.
Menginisialisasi dan Tidak Menginisialisasi Status Per Kontainer
Ketika tata letak dilampirkan ke kontainer, metode InitializeForContextCore-nya dipanggil dan memberikan kesempatan untuk menginisialisasi objek untuk menyimpan status.
Demikian pula, ketika tata letak sedang dihapus dari kontainer, metode UninitializeForContextCore akan dipanggil. Ini memberi tata letak kesempatan untuk membersihkan status apa pun yang terkait dengan kontainer tersebut.
Objek status tata letak dapat disimpan dengan dan diambil dari kontainer dengan properti LayoutState pada konteks.
Virtualisasi UI
Virtualisasi UI berarti menunda pembuatan objek UI hingga kapan diperlukan. Ini adalah pengoptimalan performa. Untuk skenario non-gulir yang menentukan kapan diperlukan mungkin didasarkan pada sejumlah hal yang spesifik untuk aplikasi. Dalam kasus tersebut , aplikasi harus mempertimbangkan untuk menggunakan x:Load. Ini tidak memerlukan penanganan khusus dalam tata letak Anda.
Dalam skenario berbasis gulir seperti daftar, menentukan kapan diperlukan sering kali didasarkan pada "apakah akan terlihat oleh pengguna" yang sangat bergantung pada tempatnya ditempatkan selama proses tata letak dan memerlukan pertimbangan khusus. Skenario ini adalah fokus untuk dokumen ini.
Catatan
Meskipun tidak tercakup dalam dokumen ini, kemampuan yang sama yang memungkinkan virtualisasi UI dalam skenario pengguliran dapat diterapkan dalam skenario non-gulir. Misalnya, kontrol ToolBar berbasis data yang mengelola masa pakai perintah yang disajikannya dan merespons perubahan ruang yang tersedia dengan mendaur ulang/memindahkan elemen antara area yang terlihat dan menu luapan.
Memulai
Pertama, putuskan apakah tata letak yang perlu Anda buat harus mendukung virtualisasi UI.
Beberapa hal yang perlu diingat...
- Tata letak non-virtualisasi lebih mudah ditulis. Jika jumlah item akan selalu kecil, maka penulisan tata letak yang tidak divirtualisasi disarankan.
- Platform ini menyediakan serangkaian tata letak terlampir yang berfungsi dengan ItemsRepeater dan LayoutPanel untuk memenuhi kebutuhan umum. Biasakan diri Anda dengan yang sebelum memutuskan Anda perlu menentukan tata letak kustom.
- Virtualisasi tata letak selalu memiliki beberapa CPU tambahan dan biaya memori/kompleksitas/overhead dibandingkan dengan tata letak yang tidak divirtualisasi. Sebagai aturan umum praktis jika anak-anak tata letak yang perlu dikelola kemungkinan akan pas di area yang berukuran 3x ukuran viewport, maka mungkin tidak ada banyak keuntungan dari tata letak virtualisasi. Ukuran 3x dibahas secara lebih rinci nanti dalam dokumen ini, tetapi disebabkan oleh sifat asinkron menggulir pada Windows dan dampaknya pada virtualisasi.
Tip
Sebagai titik referensi, pengaturan default untuk ListView (dan ItemsRepeater) adalah bahwa daur ulang tidak dimulai sampai jumlah item cukup untuk mengisi 3x ukuran viewport saat ini.
Pilih jenis dasar Anda
Jenis Tata Letak dasar memiliki dua jenis turunan yang berfungsi sebagai titik awal untuk menulis tata letak terlampir:
Tata Letak Non-Virtualisasi
Pendekatan untuk membuat tata letak non-virtualisasi akan terasa akrab bagi siapa pun yang telah membuat Panel Kustom. Konsep yang sama berlaku. Perbedaan utamanya adalah bahwa NonVirtualizingLayoutContext digunakan untuk mengakses koleksi Anak , dan tata letak dapat memilih untuk menyimpan status.
- Berasal dari jenis dasar NonVirtualizingLayout (bukan Panel).
- (Opsional) Tentukan properti dependensi yang ketika diubah akan membatalkan tata letak.
- (Baru/Opsional) Inisialisasi objek status apa pun yang diperlukan oleh tata letak sebagai bagian dari InitializeForContextCore. Simpan dengan kontainer host dengan menggunakan LayoutState yang disediakan dengan konteks .
- Ambil alih MeasureOverride dan panggil metode Pengukuran pada semua turunan.
- Ambil alih ArrangeOverride dan panggil metode Atur pada semua turunan.
- (Baru/Opsional) Bersihkan status tersimpan sebagai bagian dari UninitializeForContextCore.
Contoh: Tata Letak Tumpukan Sederhana (Item Berukuran Bervariasi)
Berikut adalah tata letak tumpukan non-virtualisasi yang sangat mendasar dari berbagai item berukuran. Ini tidak memiliki properti apa pun untuk menyesuaikan perilaku tata letak. Implementasi di bawah ini menggambarkan bagaimana tata letak bergantung pada objek konteks yang disediakan oleh kontainer untuk:
- Dapatkan hitungan anak-anak, dan
- Akses setiap elemen turunan menurut indeks.
public class MyStackLayout : NonVirtualizingLayout
{
protected override Size MeasureOverride(NonVirtualizingLayoutContext context, Size availableSize)
{
double extentHeight = 0.0;
foreach (var element in context.Children)
{
element.Measure(availableSize);
extentHeight += element.DesiredSize.Height;
}
return new Size(availableSize.Width, extentHeight);
}
protected override Size ArrangeOverride(NonVirtualizingLayoutContext context, Size finalSize)
{
double offset = 0.0;
foreach (var element in context.Children)
{
element.Arrange(
new Rect(0, offset, finalSize.Width, element.DesiredSize.Height));
offset += element.DesiredSize.Height;
}
return finalSize;
}
}
<LayoutPanel MaxWidth="196">
<LayoutPanel.Layout>
<local:MyStackLayout/>
</LayoutPanel.Layout>
<Button HorizontalAlignment="Stretch">1</Button>
<Button HorizontalAlignment="Right">2</Button>
<Button HorizontalAlignment="Center">3</Button>
<Button>4</Button>
</LayoutPanel>
Virtualisasi Tata Letak
Mirip dengan tata letak non-virtualisasi, langkah-langkah tingkat tinggi untuk tata letak virtualisasi sama. Kompleksitasnya sebagian besar dalam menentukan elemen apa yang akan berada dalam viewport dan harus diwujudkan.
- Berasal dari jenis dasar VirtualizingLayout.
- (Opsional) Tentukan properti dependensi Anda yang ketika diubah akan membatalkan tata letak.
- Inisialisasi objek status apa pun yang akan diperlukan oleh tata letak sebagai bagian dari InitializeForContextCore. Simpan dengan kontainer host dengan menggunakan LayoutState yang disediakan dengan konteks .
- Ambil alih MeasureOverride dan panggil metode Pengukuran untuk setiap anak yang harus direalisasikan.
- Metode GetOrCreateElementAt digunakan untuk mengambil UIElement yang telah disiapkan oleh kerangka kerja (misalnya, pengikatan data diterapkan).
- Ambil alih ArrangeOverride dan panggil metode Atur untuk setiap anak yang direalisasikan.
- (Opsional) Bersihkan status tersimpan sebagai bagian dari UninitializeForContextCore.
Tip
Nilai yang dikembalikan oleh MeasureOverride digunakan sebagai ukuran konten virtual.
Ada dua pendekatan umum yang perlu dipertimbangkan saat menulis tata letak virtualisasi. Apakah memilih satu atau yang lain sebagian besar tergantung pada "bagaimana Anda akan menentukan ukuran elemen". Jika cukup untuk mengetahui indeks item dalam himpunan data atau data itu sendiri menentukan ukuran akhirnya, maka kita akan menganggapnya tergantung data. Ini lebih mudah untuk dibuat. Namun, jika, satu-satunya cara untuk menentukan ukuran item adalah dengan membuat dan mengukur UI, maka kita akan mengatakan itu tergantung pada konten. Ini lebih kompleks.
Proses Tata Letak
Baik Anda membuat data atau tata letak yang bergantung pada konten, penting untuk memahami proses tata letak dan dampak pengguliran asinkron Windows.
Tampilan (lebih) yang disederhanakan dari langkah-langkah yang dilakukan oleh kerangka kerja dari awal hingga menampilkan UI di layar adalah:
Ini mengurai markup.
Menghasilkan pohon elemen.
Melakukan kode tata letak.
Melakukan pass render.
Dengan virtualisasi UI, membuat elemen yang biasanya akan dilakukan di langkah 2 tertunda atau berakhir lebih awal setelah ditentukan bahwa konten yang cukup telah dibuat untuk mengisi viewport. Kontainer virtualisasi (misalnya, ItemsRepeater) menunda tata letak terlampirnya untuk mendorong proses ini. Ini menyediakan tata letak terlampir dengan VirtualizingLayoutContext yang menampilkan informasi tambahan yang dibutuhkan tata letak virtualisasi.
RealizationRect (yaitu Viewport)
Menggulir pada Windows terjadi asinkron ke utas UI. Ini tidak dikontrol oleh tata letak kerangka kerja. Sebaliknya, interaksi dan pergerakan terjadi di komposittor sistem. Keuntungan dari pendekatan ini adalah bahwa konten panning selalu dapat dilakukan pada 60fps. Namun, tantangannya adalah bahwa "viewport", seperti yang terlihat oleh tata letak, mungkin sedikit kedaluarsa relatif terhadap apa yang sebenarnya terlihat di layar. Jika pengguna menggulir dengan cepat, mereka dapat melampaui kecepatan utas UI untuk menghasilkan konten baru dan "geser ke hitam". Untuk alasan ini, sering kali diperlukan tata letak virtualisasi untuk menghasilkan buffer tambahan elemen yang disiapkan yang cukup untuk mengisi area yang lebih besar dari viewport. Ketika di bawah beban yang lebih berat selama menggulir pengguna masih disajikan dengan konten.
Karena pembuatan elemen mahal, memvirtualisasi kontainer (misalnya, ItemsRepeater) awalnya akan menyediakan tata letak terlampir dengan RealizationRect yang cocok dengan viewport. Pada waktu diam, kontainer dapat menumbuhkan buffer konten yang disiapkan dengan melakukan panggilan berulang ke tata letak menggunakan rekam realisasi yang semakin besar. Perilaku ini adalah pengoptimalan performa yang mencoba mencapai keseimbangan antara waktu startup yang cepat dan pengalaman panning yang baik. Ukuran buffer maksimum yang akan dihasilkan ItemsRepeater dikontrol oleh properti VerticalCacheLength dan HorizontalCacheLength .
Menggunakan kembali Elemen (Daur Ulang)
Tata letak diharapkan untuk mengukur dan memosisikan elemen untuk mengisi RealizationRect setiap kali dijalankan. Secara default VirtualizingLayout akan mendaur ulang elemen yang tidak digunakan di akhir setiap kode tata letak.
VirtualizingLayoutContext yang diteruskan ke tata letak sebagai bagian dari MeasureOverride dan ArrangeOverride menyediakan informasi tambahan yang dibutuhkan tata letak virtualisasi. Beberapa hal yang paling umum digunakan yang disediakannya adalah kemampuan untuk:
- Mengkueri jumlah item dalam data (ItemCount).
- Ambil item tertentu menggunakan metode GetItemAt .
- Ambil RealizationRect yang mewakili viewport dan buffer yang harus diisi tata letak dengan elemen yang direalisasikan.
- Minta UIElement untuk item tertentu dengan metode GetOrCreateElement .
Meminta elemen untuk indeks tertentu akan menyebabkan elemen tersebut ditandai sebagai "sedang digunakan" untuk lulus tata letak tersebut. Jika elemen belum ada, maka akan direalisasikan dan secara otomatis disiapkan untuk digunakan (misalnya, melambungkan pohon UI yang ditentukan dalam DataTemplate, memproses pengikatan data apa pun, dll.). Jika tidak, ini akan diambil dari kumpulan instans yang ada.
Pada akhir setiap lulus pengukuran, elemen apa pun yang ada dan direalisasikan yang tidak ditandai "digunakan" secara otomatis dianggap tersedia untuk digunakan kembali kecuali opsi untuk SuppressAutoRecycle digunakan ketika elemen diambil melalui metode GetOrCreateElementAt . Kerangka kerja secara otomatis memindahkannya ke kumpulan daur ulang dan membuatnya tersedia. Ini kemudian dapat ditarik untuk digunakan oleh kontainer yang berbeda. Kerangka kerja mencoba menghindari hal ini jika memungkinkan karena ada beberapa biaya yang terkait dengan pengasuhan ulang elemen.
Jika tata letak virtualisasi tahu di awal setiap ukuran elemen mana yang tidak akan lagi berada dalam rect realisasi, maka dapat mengoptimalkan penggunaannya kembali. Daripada mengandalkan perilaku default kerangka kerja. Tata letak dapat memindahkan elemen secara preemptivetive ke kumpulan daur ulang dengan menggunakan metode RecycleElement . Memanggil metode ini sebelum meminta elemen baru menyebabkan elemen yang ada tersedia ketika tata letak nanti mengeluarkan permintaan GetOrCreateElementAt untuk indeks yang belum terkait dengan elemen.
VirtualizingLayoutContext menyediakan dua properti tambahan yang dirancang untuk penulis tata letak yang membuat tata letak yang bergantung pada konten. Mereka dibahas secara lebih rinci nanti.
- RecommendedAnchorIndex yang menyediakan input opsional ke tata letak.
- LayoutOrigin yang merupakan output opsional dari tata letak.
Tata Letak Virtualisasi yang bergantung pada data
Tata letak virtualisasi lebih mudah jika Anda tahu berapa ukuran setiap item tanpa perlu mengukur konten yang akan ditampilkan. Dalam dokumen ini kita hanya akan merujuk ke kategori virtualisasi tata letak ini sebagai tata letak data karena biasanya melibatkan pemeriksaan data. Berdasarkan data, aplikasi dapat memilih representasi visual dengan ukuran yang diketahui - mungkin karena bagian datanya atau sebelumnya ditentukan berdasarkan desain.
Pendekatan umum adalah untuk tata letak untuk:
- Hitung ukuran dan posisi setiap item.
- Sebagai bagian dari MeasureOverride:
- Gunakan RealizationRect untuk menentukan item mana yang akan muncul dalam viewport.
- Ambil UIElement yang harus mewakili item dengan metode GetOrCreateElementAt .
- Ukur UIElement dengan ukuran yang telah dihitung sebelumnya.
- Sebagai bagian dari ArrangeOverride, Atur setiap UIElement yang direalisasikan dengan posisi yang telah dihitung sebelumnya.
Catatan
Pendekatan tata letak data sering kali tidak kompatibel dengan virtualisasi data. Secara khusus, di mana satu-satunya data yang dimuat ke dalam memori adalah data yang diperlukan untuk mengisi apa yang terlihat oleh pengguna. Virtualisasi data tidak mengacu pada pemuatan data yang malas atau inkremental saat pengguna menggulir ke bawah tempat data tersebut tetap berada. Sebaliknya, ini mengacu pada kapan item dirilis dari memori saat digulir di luar tampilan. Memiliki tata letak data yang memeriksa setiap item data sebagai bagian dari tata letak data akan mencegah virtualisasi data berfungsi seperti yang diharapkan. Pengecualian adalah tata letak seperti UniformGridLayout yang mengasumsikan bahwa semuanya memiliki ukuran yang sama.
Tip
Jika Anda membuat kontrol kustom untuk pustaka kontrol yang akan digunakan oleh orang lain dalam berbagai situasi, maka tata letak data mungkin bukan pilihan untuk Anda.
Contoh: Tata letak Umpan Aktivitas Xbox
UI untuk Umpan Aktivitas Xbox menggunakan pola berulang di mana setiap baris memiliki petak peta lebar, diikuti oleh dua petak peta sempit yang dibalik pada baris berikutnya. Dalam tata letak ini, ukuran untuk setiap item adalah fungsi dari posisi item dalam himpunan data dan ukuran yang diketahui untuk petak peta (lebar vs sempit).
Kode di bawah ini menjelaskan apa yang mungkin dilakukan oleh antarmuka pengguna virtualisasi kustom untuk umpan aktivitas untuk mengilustrasikan pendekatan umum yang mungkin Anda ambil untuk tata letak data.
Tip
Jika Anda menginstal aplikasi Galeri WinUI 3, klik di sini untuk membuka aplikasi dan melihat ItemsRepeater sedang beraksi. Dapatkan aplikasi dari Microsoft Store atau dapatkan kode sumber di GitHub.
implementasi
/// <summary>
/// This is a custom layout that displays elements in two different sizes
/// wide (w) and narrow (n). There are two types of rows
/// odd rows - narrow narrow wide
/// even rows - wide narrow narrow
/// This pattern repeats.
/// </summary>
public class ActivityFeedLayout : VirtualizingLayout // STEP #1 Inherit from base attached layout
{
// STEP #2 - Parameterize the layout
#region Layout parameters
// We'll cache copies of the dependency properties to avoid calling GetValue during layout since that
// can be quite expensive due to the number of times we'd end up calling these.
private double _rowSpacing;
private double _colSpacing;
private Size _minItemSize = Size.Empty;
/// <summary>
/// Gets or sets the size of the whitespace gutter to include between rows
/// </summary>
public double RowSpacing
{
get { return _rowSpacing; }
set { SetValue(RowSpacingProperty, value); }
}
/// <summary>
/// Gets or sets the size of the whitespace gutter to include between items on the same row
/// </summary>
public double ColumnSpacing
{
get { return _colSpacing; }
set { SetValue(ColumnSpacingProperty, value); }
}
public Size MinItemSize
{
get { return _minItemSize; }
set { SetValue(MinItemSizeProperty, value); }
}
public static readonly DependencyProperty RowSpacingProperty =
DependencyProperty.Register(
nameof(RowSpacing),
typeof(double),
typeof(ActivityFeedLayout),
new PropertyMetadata(0, OnPropertyChanged));
public static readonly DependencyProperty ColumnSpacingProperty =
DependencyProperty.Register(
nameof(ColumnSpacing),
typeof(double),
typeof(ActivityFeedLayout),
new PropertyMetadata(0, OnPropertyChanged));
public static readonly DependencyProperty MinItemSizeProperty =
DependencyProperty.Register(
nameof(MinItemSize),
typeof(Size),
typeof(ActivityFeedLayout),
new PropertyMetadata(Size.Empty, OnPropertyChanged));
private static void OnPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
var layout = obj as ActivityFeedLayout;
if (args.Property == RowSpacingProperty)
{
layout._rowSpacing = (double)args.NewValue;
}
else if (args.Property == ColumnSpacingProperty)
{
layout._colSpacing = (double)args.NewValue;
}
else if (args.Property == MinItemSizeProperty)
{
layout._minItemSize = (Size)args.NewValue;
}
else
{
throw new InvalidOperationException("Don't know what you are talking about!");
}
layout.InvalidateMeasure();
}
#endregion
#region Setup / teardown // STEP #3: Initialize state
protected override void InitializeForContextCore(VirtualizingLayoutContext context)
{
base.InitializeForContextCore(context);
var state = context.LayoutState as ActivityFeedLayoutState;
if (state == null)
{
// Store any state we might need since (in theory) the layout could be in use by multiple
// elements simultaneously
// In reality for the Xbox Activity Feed there's probably only a single instance.
context.LayoutState = new ActivityFeedLayoutState();
}
}
protected override void UninitializeForContextCore(VirtualizingLayoutContext context)
{
base.UninitializeForContextCore(context);
// clear any state
context.LayoutState = null;
}
#endregion
#region Layout // STEP #4,5 - Measure and Arrange
protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
{
if (this.MinItemSize == Size.Empty)
{
var firstElement = context.GetOrCreateElementAt(0);
firstElement.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
// setting the member value directly to skip invalidating layout
this._minItemSize = firstElement.DesiredSize;
}
// Determine which rows need to be realized. We know every row will have the same height and
// only contain 3 items. Use that to determine the index for the first and last item that
// will be within that realization rect.
var firstRowIndex = Math.Max(
(int)(context.RealizationRect.Y / (this.MinItemSize.Height + this.RowSpacing)) - 1,
0);
var lastRowIndex = Math.Min(
(int)(context.RealizationRect.Bottom / (this.MinItemSize.Height + this.RowSpacing)) + 1,
(int)(context.ItemCount / 3));
// Determine which items will appear on those rows and what the rect will be for each item
var state = context.LayoutState as ActivityFeedLayoutState;
state.LayoutRects.Clear();
// Save the index of the first realized item. We'll use it as a starting point during arrange.
state.FirstRealizedIndex = firstRowIndex * 3;
// ideal item width that will expand/shrink to fill available space
double desiredItemWidth = Math.Max(this.MinItemSize.Width, (availableSize.Width - this.ColumnSpacing * 3) / 4);
// Foreach item between the first and last index,
// Call GetElementOrCreateElementAt which causes an element to either be realized or retrieved
// from a recycle pool
// Measure the element using an appropriate size
//
// Any element that was previously realized which we don't retrieve in this pass (via a call to
// GetElementOrCreateAt) will be automatically cleared and set aside for later re-use.
// Note: While this work fine, it does mean that more elements than are required may be
// created because it isn't until after our MeasureOverride completes that the unused elements
// will be recycled and available to use. We could avoid this by choosing to track the first/last
// index from the previous layout pass. The diff between the previous range and current range
// would represent the elements that we can pre-emptively make available for re-use by calling
// context.RecycleElement(element).
for (int rowIndex = firstRowIndex; rowIndex < lastRowIndex; rowIndex++)
{
int firstItemIndex = rowIndex * 3;
var boundsForCurrentRow = CalculateLayoutBoundsForRow(rowIndex, desiredItemWidth);
for (int columnIndex = 0; columnIndex < 3; columnIndex++)
{
var index = firstItemIndex + columnIndex;
var rect = boundsForCurrentRow[index % 3];
var container = context.GetOrCreateElementAt(index);
container.Measure(
new Size(boundsForCurrentRow[columnIndex].Width, boundsForCurrentRow[columnIndex].Height));
state.LayoutRects.Add(boundsForCurrentRow[columnIndex]);
}
}
// Calculate and return the size of all the content (realized or not) by figuring out
// what the bottom/right position of the last item would be.
var extentHeight = ((int)(context.ItemCount / 3) - 1) * (this.MinItemSize.Height + this.RowSpacing) + this.MinItemSize.Height;
// Report this as the desired size for the layout
return new Size(desiredItemWidth * 4 + this.ColumnSpacing * 2, extentHeight);
}
protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
{
// walk through the cache of containers and arrange
var state = context.LayoutState as ActivityFeedLayoutState;
var virtualContext = context as VirtualizingLayoutContext;
int currentIndex = state.FirstRealizedIndex;
foreach (var arrangeRect in state.LayoutRects)
{
var container = virtualContext.GetOrCreateElementAt(currentIndex);
container.Arrange(arrangeRect);
currentIndex++;
}
return finalSize;
}
#endregion
#region Helper methods
private Rect[] CalculateLayoutBoundsForRow(int rowIndex, double desiredItemWidth)
{
var boundsForRow = new Rect[3];
var yoffset = rowIndex * (this.MinItemSize.Height + this.RowSpacing);
boundsForRow[0].Y = boundsForRow[1].Y = boundsForRow[2].Y = yoffset;
boundsForRow[0].Height = boundsForRow[1].Height = boundsForRow[2].Height = this.MinItemSize.Height;
if (rowIndex % 2 == 0)
{
// Left tile (narrow)
boundsForRow[0].X = 0;
boundsForRow[0].Width = desiredItemWidth;
// Middle tile (narrow)
boundsForRow[1].X = boundsForRow[0].Right + this.ColumnSpacing;
boundsForRow[1].Width = desiredItemWidth;
// Right tile (wide)
boundsForRow[2].X = boundsForRow[1].Right + this.ColumnSpacing;
boundsForRow[2].Width = desiredItemWidth * 2 + this.ColumnSpacing;
}
else
{
// Left tile (wide)
boundsForRow[0].X = 0;
boundsForRow[0].Width = (desiredItemWidth * 2 + this.ColumnSpacing);
// Middle tile (narrow)
boundsForRow[1].X = boundsForRow[0].Right + this.ColumnSpacing;
boundsForRow[1].Width = desiredItemWidth;
// Right tile (narrow)
boundsForRow[2].X = boundsForRow[1].Right + this.ColumnSpacing;
boundsForRow[2].Width = desiredItemWidth;
}
return boundsForRow;
}
#endregion
}
internal class ActivityFeedLayoutState
{
public int FirstRealizedIndex { get; set; }
/// <summary>
/// List of layout bounds for items starting with the
/// FirstRealizedIndex.
/// </summary>
public List<Rect> LayoutRects
{
get
{
if (_layoutRects == null)
{
_layoutRects = new List<Rect>();
}
return _layoutRects;
}
}
private List<Rect> _layoutRects;
}
(Opsional) Mengelola Item ke Pemetaan UIElement
Secara default, VirtualizingLayoutContext mempertahankan pemetaan antara elemen yang direalisasikan dan indeks di sumber data yang mereka wakili. Tata letak dapat memilih untuk mengelola pemetaan ini sendiri dengan selalu meminta opsi untuk SuppressAutoRecycle saat mengambil elemen melalui metode GetOrCreateElementAt yang mencegah perilaku daur ulang otomatis default. Tata letak dapat memilih untuk melakukan ini, misalnya, jika hanya akan digunakan saat pengguliran dibatasi untuk satu arah dan item yang dipertimbangkan akan selalu berdekatan (yaitu mengetahui indeks elemen pertama dan terakhir sudah cukup untuk mengetahui semua elemen yang harus direalisasikan).
Contoh: Ukuran Umpan Aktivitas Xbox
Cuplikan di bawah ini menunjukkan logika tambahan yang dapat ditambahkan ke MeasureOverride dalam sampel sebelumnya untuk mengelola pemetaan.
protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
{
//...
// Determine which items will appear on those rows and what the rect will be for each item
var state = context.LayoutState as ActivityFeedLayoutState;
state.LayoutRects.Clear();
// Recycle previously realized elements that we know we won't need so that they can be used to
// fill in gaps without requiring us to realize additional elements.
var newFirstRealizedIndex = firstRowIndex * 3;
var newLastRealizedIndex = lastRowIndex * 3 + 3;
for (int i = state.FirstRealizedIndex; i < newFirstRealizedIndex; i++)
{
context.RecycleElement(state.IndexToElementMap.Get(i));
state.IndexToElementMap.Clear(i);
}
for (int i = state.LastRealizedIndex; i < newLastRealizedIndex; i++)
{
context.RecycleElement(context.IndexElementMap.Get(i));
state.IndexToElementMap.Clear(i);
}
// ...
// Foreach item between the first and last index,
// Call GetElementOrCreateElementAt which causes an element to either be realized or retrieved
// from a recycle pool
// Measure the element using an appropriate size
//
for (int rowIndex = firstRowIndex; rowIndex < lastRowIndex; rowIndex++)
{
int firstItemIndex = rowIndex * 3;
var boundsForCurrentRow = CalculateLayoutBoundsForRow(rowIndex, desiredItemWidth);
for (int columnIndex = 0; columnIndex < 3; columnIndex++)
{
var index = firstItemIndex + columnIndex;
var rect = boundsForCurrentRow[index % 3];
UIElement container = null;
if (state.IndexToElementMap.Contains(index))
{
container = state.IndexToElementMap.Get(index);
}
else
{
container = context = context.GetOrCreateElementAt(index, ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
state.IndexToElementMap.Add(index, container);
}
container.Measure(
new Size(boundsForCurrentRow[columnIndex].Width, boundsForCurrentRow[columnIndex].Height));
state.LayoutRects.Add(boundsForCurrentRow[columnIndex]);
}
}
// ...
}
internal class ActivityFeedLayoutState
{
// ...
Dictionary<int, UIElement> IndexToElementMap { get; set; }
// ...
}
Tata Letak Virtualisasi yang Bergantung Pada Konten
Jika Anda harus terlebih dahulu mengukur konten UI untuk item guna mengetahui ukurannya yang tepat, maka itu adalah tata letak yang bergantung pada konten. Anda juga dapat menganggapnya sebagai tata letak di mana setiap item harus mengukur dirinya sendiri daripada tata letak yang memberi tahu item ukurannya. Virtualisasi tata letak yang termasuk dalam kategori ini lebih terlibat.
Catatan
Tata letak yang bergantung pada konten tidak (tidak boleh) memutus virtualisasi data.
Estimasi
Tata letak yang bergantung pada konten mengandalkan estimasi untuk menebak ukuran konten yang tidak direalisasi dan posisi konten yang direalisasikan. Karena perkiraan tersebut berubah, itu akan menyebabkan konten yang direalisasikan secara teratur menggeser posisi dalam area yang dapat digulir. Hal ini dapat menyebabkan pengalaman pengguna yang sangat frustrasi dan menggairahkan jika tidak dimitigasi. Potensi masalah dan mitigasi dibahas di sini.
Catatan
Tata letak data yang mempertimbangkan setiap item dan mengetahui ukuran yang tepat dari semua item, direalisasikan atau tidak, dan posisinya dapat menghindari masalah ini sepenuhnya.
Gulir Penahanan
XAML menyediakan mekanisme untuk mengurangi pergeseran viewport mendadak dengan memiliki kontrol gulir yang mendukung penjangkaran gulir dengan menerapkan antarmuka IScrollAnchorPovider . Saat pengguna memanipulasi konten, kontrol gulir terus memilih elemen dari kumpulan kandidat yang dipilih untuk dilacak. Jika posisi elemen jangkar bergeser selama tata letak, kontrol gulir secara otomatis menggeser viewport-nya untuk mempertahankan viewport.
Nilai RecommendedAnchorIndex yang disediakan untuk tata letak mungkin mencerminkan bahwa elemen jangkar yang dipilih saat ini dipilih oleh kontrol pengguliran. Atau, jika pengembang secara eksplisit meminta agar elemen direalisasikan untuk indeks dengan metode GetOrCreateElement pada ItemsRepeater, maka indeks tersebut diberikan sebagai RecommendedAnchorIndex pada kode tata letak berikutnya. Ini memungkinkan tata letak disiapkan untuk skenario yang kemungkinan besar pengembang menyadari elemen dan kemudian meminta agar ditampilkan melalui metode StartBringIntoView .
RecommendedAnchorIndex adalah indeks untuk item di sumber data yang harus diposisikan tata letak yang bergantung pada konten terlebih dahulu saat memperkirakan posisi itemnya. Ini harus berfungsi sebagai titik awal untuk memposisikan item lain yang direalisasikan.
Dampak pada ScrollBars
Bahkan dengan penahanan gulir, jika perkiraan tata letak sangat bervariasi, mungkin karena variasi signifikan dalam ukuran konten, maka posisi ibu jari untuk ScrollBar mungkin tampak melompat-lompat. Ini dapat menggoreng untuk pengguna jika ibu jari tampaknya tidak melacak posisi penunjuk mouse mereka saat mereka menyeretnya.
Semakin akurat tata letak dapat berada dalam estimasinya maka semakin kecil kemungkinan pengguna akan melihat jempol ScrollBar melompat.
Koreksi Tata Letak
Tata letak yang bergantung pada konten harus disiapkan untuk merasialisasi perkiraannya dengan realitas. Misalnya, saat pengguna menggulir ke bagian atas konten dan tata letak menyadari elemen pertama, mungkin menemukan bahwa posisi yang diantisipasi elemen relatif terhadap elemen tempat dimulainya akan menyebabkannya muncul di suatu tempat selain asal (x:0, y:0). Ketika ini terjadi, tata letak dapat menggunakan properti LayoutOrigin untuk mengatur posisi yang dihitung sebagai asal tata letak baru. Hasil bersih mirip dengan gulir anchoring di mana viewport kontrol gulir secara otomatis disesuaikan untuk memperhitungkan posisi konten seperti yang dilaporkan oleh tata letak.
Viewport terputus
Ukuran yang dikembalikan dari metode MeasureOverride tata letak mewakili tebakan terbaik pada ukuran konten yang dapat berubah dengan setiap tata letak berturut-turut. Saat pengguna menggulir tata letak akan terus dievaluasi ulang dengan RealizationRect yang diperbarui.
Jika pengguna menyeret ibu jari dengan sangat cepat maka kemungkinannya untuk viewport, dari perspektif tata letak, untuk tampak membuat lompatan besar di mana posisi sebelumnya tidak tumpang tindih dengan posisi sekarang saat ini. Hal ini disebabkan oleh sifat gulir yang tidak sinkron. Dimungkinkan juga bagi aplikasi yang menggunakan tata letak untuk meminta agar elemen ditampilkan untuk item yang saat ini tidak direalisasikan dan diperkirakan berada di luar rentang saat ini yang dilacak oleh tata letak.
Ketika tata letak menemukan dugaannya salah dan/atau melihat pergeseran viewport yang tidak terduga, tata letak perlu mengatur kembali posisi awalnya. Tata letak virtualisasi yang dikirim sebagai bagian dari kontrol XAML dikembangkan sebagai tata letak yang bergantung pada konten karena menempatkan lebih sedikit batasan pada sifat konten yang akan ditampilkan.
Contoh: Tata Letak Tumpukan Virtualisasi Sederhana untuk Item Berukuran Variabel
Sampel di bawah ini menunjukkan tata letak tumpukan sederhana untuk item berukuran variabel yang:
- mendukung virtualisasi UI,
- menggunakan estimasi untuk menebak ukuran item yang tidak direalisasi,
- mengetahui potensi pergeseran viewport yang terhenti, dan
- menerapkan koreksi tata letak untuk memperhitungkan pergeseran tersebut.
Penggunaan: Markup
<ScrollViewer>
<ItemsRepeater x:Name="repeater" >
<ItemsRepeater.Layout>
<local:VirtualizingStackLayout />
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<DataTemplate x:Key="item">
<UserControl IsTabStop="True" UseSystemFocusVisuals="True" Margin="5">
<StackPanel BorderThickness="1" Background="LightGray" Margin="5">
<Image x:Name="recipeImage" Source="{Binding ImageUri}" Width="100" Height="100"/>
<TextBlock x:Name="recipeDescription"
Text="{Binding Description}"
TextWrapping="Wrap"
Margin="10" />
</StackPanel>
</UserControl>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</ScrollViewer>
Codebehind: Main.cs
string _lorem = @"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam laoreet erat vel massa rutrum, eget mollis massa vulputate. Vivamus semper augue leo, eget faucibus nulla mattis nec. Donec scelerisque lacus at dui ultricies, eget auctor ipsum placerat. Integer aliquet libero sed nisi eleifend, nec rutrum arcu lacinia. Sed a sem et ante gravida congue sit amet ut augue. Donec quis pellentesque urna, non finibus metus. Proin sed ornare tellus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam laoreet erat vel massa rutrum, eget mollis massa vulputate. Vivamus semper augue leo, eget faucibus nulla mattis nec. Donec scelerisque lacus at dui ultricies, eget auctor ipsum placerat. Integer aliquet libero sed nisi eleifend, nec rutrum arcu lacinia. Sed a sem et ante gravida congue sit amet ut augue. Donec quis pellentesque urna, non finibus metus. Proin sed ornare tellus.";
var rnd = new Random();
var data = new ObservableCollection<Recipe>(Enumerable.Range(0, 300).Select(k =>
new Recipe
{
ImageUri = new Uri(string.Format("ms-appx:///Images/recipe{0}.png", k % 8 + 1)),
Description = k + " - " + _lorem.Substring(0, rnd.Next(50, 350))
}));
repeater.ItemsSource = data;
Kode: VirtualizingStackLayout.cs
// This is a sample layout that stacks elements one after
// the other where each item can be of variable height. This is
// also a virtualizing layout - we measure and arrange only elements
// that are in the viewport. Not measuring/arranging all elements means
// that we do not have the complete picture and need to estimate sometimes.
// For example the size of the layout (extent) is an estimation based on the
// average heights we have seen so far. Also, if you drag the mouse thumb
// and yank it quickly, then we estimate what goes in the new viewport.
// The layout caches the bounds of everything that are in the current viewport.
// During measure, we might get a suggested anchor (or start index), we use that
// index to start and layout the rest of the items in the viewport relative to that
// index. Note that since we are estimating, we can end up with negative origin when
// the viewport is somewhere in the middle of the extent. This is achieved by setting the
// LayoutOrigin property on the context. Once this is set, future viewport will account
// for the origin.
public class VirtualizingStackLayout : VirtualizingLayout
{
// Estimation state
List<double> m_estimationBuffer = Enumerable.Repeat(0d, 100).ToList();
int m_numItemsUsedForEstimation = 0;
double m_totalHeightForEstimation = 0;
// State to keep track of realized bounds
int m_firstRealizedDataIndex = 0;
List<Rect> m_realizedElementBounds = new List<Rect>();
Rect m_lastExtent = new Rect();
protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
{
var viewport = context.RealizationRect;
DebugTrace("MeasureOverride: Viewport " + viewport);
// Remove bounds for elements that are now outside the viewport.
// Proactive recycling elements means we can reuse it during this measure pass again.
RemoveCachedBoundsOutsideViewport(viewport);
// Find the index of the element to start laying out from - the anchor
int startIndex = GetStartIndex(context, availableSize);
// Measure and layout elements starting from the start index, forward and backward.
Generate(context, availableSize, startIndex, forward:true);
Generate(context, availableSize, startIndex, forward:false);
// Estimate the extent size. Note that this can have a non 0 origin.
m_lastExtent = EstimateExtent(context, availableSize);
context.LayoutOrigin = new Point(m_lastExtent.X, m_lastExtent.Y);
return new Size(m_lastExtent.Width, m_lastExtent.Height);
}
protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
{
DebugTrace("ArrangeOverride: Viewport" + context.RealizationRect);
for (int realizationIndex = 0; realizationIndex < m_realizedElementBounds.Count; realizationIndex++)
{
int currentDataIndex = m_firstRealizedDataIndex + realizationIndex;
DebugTrace("Arranging " + currentDataIndex);
// Arrange the child. If any alignment needs to be done, it
// can be done here.
var child = context.GetOrCreateElementAt(currentDataIndex);
var arrangeBounds = m_realizedElementBounds[realizationIndex];
arrangeBounds.X -= m_lastExtent.X;
arrangeBounds.Y -= m_lastExtent.Y;
child.Arrange(arrangeBounds);
}
return finalSize;
}
// The data collection has changed, since we are maintaining the bounds of elements
// in the viewport, we will update the list to account for the collection change.
protected override void OnItemsChangedCore(VirtualizingLayoutContext context, object source, NotifyCollectionChangedEventArgs args)
{
InvalidateMeasure();
if (m_realizedElementBounds.Count > 0)
{
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
OnItemsAdded(args.NewStartingIndex, args.NewItems.Count);
break;
case NotifyCollectionChangedAction.Replace:
OnItemsRemoved(args.OldStartingIndex, args.OldItems.Count);
OnItemsAdded(args.NewStartingIndex, args.NewItems.Count);
break;
case NotifyCollectionChangedAction.Remove:
OnItemsRemoved(args.OldStartingIndex, args.OldItems.Count);
break;
case NotifyCollectionChangedAction.Reset:
m_realizedElementBounds.Clear();
m_firstRealizedDataIndex = 0;
break;
default:
throw new NotImplementedException();
}
}
}
// Figure out which index to use as the anchor and start laying out around it.
private int GetStartIndex(VirtualizingLayoutContext context, Size availableSize)
{
int startDataIndex = -1;
var recommendedAnchorIndex = context.RecommendedAnchorIndex;
bool isSuggestedAnchorValid = recommendedAnchorIndex != -1;
if (isSuggestedAnchorValid)
{
if (IsRealized(recommendedAnchorIndex))
{
startDataIndex = recommendedAnchorIndex;
}
else
{
ClearRealizedRange();
startDataIndex = recommendedAnchorIndex;
}
}
else
{
// Find the first realized element that is visible in the viewport.
startDataIndex = GetFirstRealizedDataIndexInViewport(context.RealizationRect);
if (startDataIndex < 0)
{
startDataIndex = EstimateIndexForViewport(context.RealizationRect, context.ItemCount);
ClearRealizedRange();
}
}
// We have an anchorIndex, realize and measure it and
// figure out its bounds.
if (startDataIndex != -1 & context.ItemCount > 0)
{
if (m_realizedElementBounds.Count == 0)
{
m_firstRealizedDataIndex = startDataIndex;
}
var newAnchor = EnsureRealized(startDataIndex);
DebugTrace("Measuring start index " + startDataIndex);
var desiredSize = MeasureElement(context, startDataIndex, availableSize);
var bounds = new Rect(
0,
newAnchor ?
(m_totalHeightForEstimation / m_numItemsUsedForEstimation) * startDataIndex : GetCachedBoundsForDataIndex(startDataIndex).Y,
availableSize.Width,
desiredSize.Height);
SetCachedBoundsForDataIndex(startDataIndex, bounds);
}
return startDataIndex;
}
private void Generate(VirtualizingLayoutContext context, Size availableSize, int anchorDataIndex, bool forward)
{
// Generate forward or backward from anchorIndex until we hit the end of the viewport
int step = forward ? 1 : -1;
int previousDataIndex = anchorDataIndex;
int currentDataIndex = previousDataIndex + step;
var viewport = context.RealizationRect;
while (IsDataIndexValid(currentDataIndex, context.ItemCount) &&
ShouldContinueFillingUpSpace(previousDataIndex, forward, viewport))
{
EnsureRealized(currentDataIndex);
DebugTrace("Measuring " + currentDataIndex);
var desiredSize = MeasureElement(context, currentDataIndex, availableSize);
var previousBounds = GetCachedBoundsForDataIndex(previousDataIndex);
Rect currentBounds = new Rect(0,
forward ? previousBounds.Y + previousBounds.Height : previousBounds.Y - desiredSize.Height,
availableSize.Width,
desiredSize.Height);
SetCachedBoundsForDataIndex(currentDataIndex, currentBounds);
previousDataIndex = currentDataIndex;
currentDataIndex += step;
}
}
// Remove bounds that are outside the viewport, leaving one extra since our
// generate stops after generating one extra to know that we are outside the
// viewport.
private void RemoveCachedBoundsOutsideViewport(Rect viewport)
{
int firstRealizedIndexInViewport = 0;
while (firstRealizedIndexInViewport < m_realizedElementBounds.Count &&
!Intersects(m_realizedElementBounds[firstRealizedIndexInViewport], viewport))
{
firstRealizedIndexInViewport++;
}
int lastRealizedIndexInViewport = m_realizedElementBounds.Count - 1;
while (lastRealizedIndexInViewport >= 0 &&
!Intersects(m_realizedElementBounds[lastRealizedIndexInViewport], viewport))
{
lastRealizedIndexInViewport--;
}
if (firstRealizedIndexInViewport > 0)
{
m_firstRealizedDataIndex += firstRealizedIndexInViewport;
m_realizedElementBounds.RemoveRange(0, firstRealizedIndexInViewport);
}
if (lastRealizedIndexInViewport >= 0 && lastRealizedIndexInViewport < m_realizedElementBounds.Count - 2)
{
m_realizedElementBounds.RemoveRange(lastRealizedIndexInViewport + 2, m_realizedElementBounds.Count - lastRealizedIndexInViewport - 3);
}
}
private bool Intersects(Rect bounds, Rect viewport)
{
return !(bounds.Bottom < viewport.Top ||
bounds.Top > viewport.Bottom);
}
private bool ShouldContinueFillingUpSpace(int dataIndex, bool forward, Rect viewport)
{
var bounds = GetCachedBoundsForDataIndex(dataIndex);
return forward ?
bounds.Y < viewport.Bottom :
bounds.Y > viewport.Top;
}
private bool IsDataIndexValid(int currentDataIndex, int itemCount)
{
return currentDataIndex >= 0 && currentDataIndex < itemCount;
}
private int EstimateIndexForViewport(Rect viewport, int dataCount)
{
double averageHeight = m_totalHeightForEstimation / m_numItemsUsedForEstimation;
int estimatedIndex = (int)(viewport.Top / averageHeight);
// clamp to an index within the collection
estimatedIndex = Math.Max(0, Math.Min(estimatedIndex, dataCount));
return estimatedIndex;
}
private int GetFirstRealizedDataIndexInViewport(Rect viewport)
{
int index = -1;
if (m_realizedElementBounds.Count > 0)
{
for (int i = 0; i < m_realizedElementBounds.Count; i++)
{
if (m_realizedElementBounds[i].Y < viewport.Bottom &&
m_realizedElementBounds[i].Bottom > viewport.Top)
{
index = m_firstRealizedDataIndex + i;
break;
}
}
}
return index;
}
private Size MeasureElement(VirtualizingLayoutContext context, int index, Size availableSize)
{
var child = context.GetOrCreateElementAt(index);
child.Measure(availableSize);
int estimationBufferIndex = index % m_estimationBuffer.Count;
bool alreadyMeasured = m_estimationBuffer[estimationBufferIndex] != 0;
if (!alreadyMeasured)
{
m_numItemsUsedForEstimation++;
}
m_totalHeightForEstimation -= m_estimationBuffer[estimationBufferIndex];
m_totalHeightForEstimation += child.DesiredSize.Height;
m_estimationBuffer[estimationBufferIndex] = child.DesiredSize.Height;
return child.DesiredSize;
}
private bool EnsureRealized(int dataIndex)
{
if (!IsRealized(dataIndex))
{
int realizationIndex = RealizationIndex(dataIndex);
Debug.Assert(dataIndex == m_firstRealizedDataIndex - 1 ||
dataIndex == m_firstRealizedDataIndex + m_realizedElementBounds.Count ||
m_realizedElementBounds.Count == 0);
if (realizationIndex == -1)
{
m_realizedElementBounds.Insert(0, new Rect());
}
else
{
m_realizedElementBounds.Add(new Rect());
}
if (m_firstRealizedDataIndex > dataIndex)
{
m_firstRealizedDataIndex = dataIndex;
}
return true;
}
return false;
}
// Figure out the extent of the layout by getting the number of items remaining
// above and below the realized elements and getting an estimation based on
// average item heights seen so far.
private Rect EstimateExtent(VirtualizingLayoutContext context, Size availableSize)
{
double averageHeight = m_totalHeightForEstimation / m_numItemsUsedForEstimation;
Rect extent = new Rect(0, 0, availableSize.Width, context.ItemCount * averageHeight);
if (context.ItemCount > 0 && m_realizedElementBounds.Count > 0)
{
extent.Y = m_firstRealizedDataIndex == 0 ?
m_realizedElementBounds[0].Y :
m_realizedElementBounds[0].Y - (m_firstRealizedDataIndex - 1) * averageHeight;
int lastRealizedIndex = m_firstRealizedDataIndex + m_realizedElementBounds.Count;
if (lastRealizedIndex == context.ItemCount - 1)
{
var lastBounds = m_realizedElementBounds[m_realizedElementBounds.Count - 1];
extent.Y = lastBounds.Bottom;
}
else
{
var lastBounds = m_realizedElementBounds[m_realizedElementBounds.Count - 1];
int lastRealizedDataIndex = m_firstRealizedDataIndex + m_realizedElementBounds.Count;
int numItemsAfterLastRealizedIndex = context.ItemCount - lastRealizedDataIndex;
extent.Height = lastBounds.Bottom + numItemsAfterLastRealizedIndex * averageHeight - extent.Y;
}
}
DebugTrace("Extent " + extent + " with average height " + averageHeight);
return extent;
}
private bool IsRealized(int dataIndex)
{
int realizationIndex = dataIndex - m_firstRealizedDataIndex;
return realizationIndex >= 0 && realizationIndex < m_realizedElementBounds.Count;
}
// Index in the m_realizedElementBounds collection
private int RealizationIndex(int dataIndex)
{
return dataIndex - m_firstRealizedDataIndex;
}
private void OnItemsAdded(int index, int count)
{
// Using the old indexes here (before it was updated by the collection change)
// if the insert data index is between the first and last realized data index, we need
// to insert items.
int lastRealizedDataIndex = m_firstRealizedDataIndex + m_realizedElementBounds.Count - 1;
int newStartingIndex = index;
if (newStartingIndex > m_firstRealizedDataIndex &&
newStartingIndex <= lastRealizedDataIndex)
{
// Inserted within the realized range
int insertRangeStartIndex = newStartingIndex - m_firstRealizedDataIndex;
for (int i = 0; i < count; i++)
{
// Insert null (sentinel) here instead of an element, that way we do not
// end up creating a lot of elements only to be thrown out in the next layout.
int insertRangeIndex = insertRangeStartIndex + i;
int dataIndex = newStartingIndex + i;
// This is to keep the contiguousness of the mapping
m_realizedElementBounds.Insert(insertRangeIndex, new Rect());
}
}
else if (index <= m_firstRealizedDataIndex)
{
// Items were inserted before the realized range.
// We need to update m_firstRealizedDataIndex;
m_firstRealizedDataIndex += count;
}
}
private void OnItemsRemoved(int index, int count)
{
int lastRealizedDataIndex = m_firstRealizedDataIndex + m_realizedElementBounds.Count - 1;
int startIndex = Math.Max(m_firstRealizedDataIndex, index);
int endIndex = Math.Min(lastRealizedDataIndex, index + count - 1);
bool removeAffectsFirstRealizedDataIndex = (index <= m_firstRealizedDataIndex);
if (endIndex >= startIndex)
{
ClearRealizedRange(RealizationIndex(startIndex), endIndex - startIndex + 1);
}
if (removeAffectsFirstRealizedDataIndex &&
m_firstRealizedDataIndex != -1)
{
m_firstRealizedDataIndex -= count;
}
}
private void ClearRealizedRange(int startRealizedIndex, int count)
{
m_realizedElementBounds.RemoveRange(startRealizedIndex, count);
if (startRealizedIndex == 0)
{
m_firstRealizedDataIndex = m_realizedElementBounds.Count == 0 ? 0 : m_firstRealizedDataIndex + count;
}
}
private void ClearRealizedRange()
{
m_realizedElementBounds.Clear();
m_firstRealizedDataIndex = 0;
}
private Rect GetCachedBoundsForDataIndex(int dataIndex)
{
return m_realizedElementBounds[RealizationIndex(dataIndex)];
}
private void SetCachedBoundsForDataIndex(int dataIndex, Rect bounds)
{
m_realizedElementBounds[RealizationIndex(dataIndex)] = bounds;
}
private Rect GetCachedBoundsForRealizationIndex(int relativeIndex)
{
return m_realizedElementBounds[relativeIndex];
}
void DebugTrace(string message, params object[] args)
{
Debug.WriteLine(message, args);
}
}
Artikel terkait
Windows developer