創建自定義佈局
- PDF用於離線使用
- 示例代碼:
- 相關文章:
- 相關API:
- 有關的影片:
讓我們知道你對此的感受
最後更新:4個月前
Xamarin.Forms定義了四個佈局類 - StackLayout,AbsoluteLayout,RelativeLayout和Grid,並且每個佈局類別都以不同的方式排列它們的子類。但是,有時需要使用Xamarin.Forms不提供的佈局來組織頁面內容。本文介紹瞭如何編寫自定義佈局類,並演示了一種方向敏感的WrapLayout類,它將子級別橫跨頁面排列,然後將後續子項的顯示包裝到其他行。
概觀
在Xamarin.Forms中,所有佈局類都從類派生,Layout<T>
並將泛型類型限制View
爲其派生類型。反過來,這個Layout<T>
階級從課堂中得到Layout
,它提供了定位和調整兒童元素大小的機制。
每個視覺元素負責確定自己的首選大小,這被稱爲請求的大小。Page
,Layout
和Layout<View>
衍生類型負責確定他們的孩子或孩子相對於自己的位置和大小。因此,佈局涉及父子關係,其中父級確定其子級的大小應該是多少,但會嘗試容納所請求的小孩大小。
創建自定義佈局需要徹底瞭解Xamarin.Forms佈局和無效循環。現在將討論這些週期。
佈局
佈局從頁面的視覺樹頂部開始,並通過視覺樹的所有分支進行,以包含頁面上的每個視覺元素。父母對其他因素的要素負責相對於自己的尺寸和定位他們的孩子。
的VisualElement
類定義了一個Measure
測量用於佈局操作的元素,和一種方法Layout
,用於指定矩形區域的元件將在被渲染方法。當應用程序啓動並顯示第一個頁面時,首先調用一個佈局循環Measure
,然後Layout
調用,從該Page
對象開始:
- 在佈局循環中,每個父元素都負責調用
Measure
其子對象的方法。 - 在孩子被測量之後,每個父元素都負責調用
Layout
孩子的方法。
這個循環保證了每個頁面上的視覺元素接收到來電Measure
和Layout
方法。該過程如下圖所示:
請注意,如果某些更改影響佈局,佈局週期也可能發生在視覺樹的子集上。這包括被添加或刪除的項目從集合諸如在
StackLayout
,在一個變化IsVisible
的元件,或在元件的尺寸變化的屬性。
每個具有一個Content
或一個Children
屬性的Xamarin.Forms類具有可覆蓋的LayoutChildren
方法。派生的自定義佈局類Layout<View>
必須覆蓋此方法,並確保在所有元素的子項上調用Measure
和Layout
方法,以便提供所需的自定義佈局。
另外,派生Layout
或者Layout<View>
必須覆蓋OnMeasure
方法的每個類,這是佈局類決定通過調用Measure
其子對象的方式所需的大小。
元素基於約束來確定它們的大小,這些約束指示元素的父元素內的元素有多少空間。傳遞給約束
Measure
和OnMeasure
方法的範圍可從0到Double.PositiveInfinity
。當元素被接收到具有非無限參數的方法的調用時,元素被約束或完全約束Measure
- 元素被約束到特定的大小。當一個元素接收到對其方法的調用時,它至少有一個參數等於- 無限約束可以被認爲是表示自動調整,這個元素是無約束的或部分受限的。Measure
Double.PositiveInfinity
失效
無效是頁面上的元素更改觸發新的佈局循環的過程。當元素不再具有正確的大小或位置時,元素被認爲是無效的。例如,如果FontSize
某個屬性Button
發生更改,則說明該屬性Button
將不再具有正確的大小。調整大小Button
可能會通過頁面的其餘部分產生布局變化的波紋效應。
元素通過調用該InvalidateMeasure
方法無效,通常當元素的屬性更改時可能會導致元素的新大小。該方法觸發MeasureInvalidated
事件,元素的父處理器觸發新的佈局循環。
將Layout
類設置爲一個處理器MeasureInvalidated
上添加到它的每一個孩子的事件Content
屬性或Children
集合,當孩子被刪除分離的處理程序。因此,每當其中一個子節點改變大小時,每個可視樹中的具有子節點的元素就會被提醒。下圖說明了視覺樹中元素的大小如何改變可能會導致紋理變化的變化:
但是,Layout
該類試圖限制一個孩子大小的變化對頁面佈局的影響。如果佈局大小受限,則子視圖樹中的父佈局不會影響任何高度的子尺寸更改。但是,通常情況下,佈局大小的變化會影響佈局如何佈置其子項。因此,佈局大小的任何更改都將爲佈局開始佈局循環,佈局將接收對其OnMeasure
和LayoutChildren
方法的調用。
的Layout
類也定義一個InvalidateLayout
具有類似目的的方法InvalidateMeasure
的方法。InvalidateLayout
每當進行更改時,都應調用該方法,影響其子項的佈局位置和大小。例如,每當將子添加到佈局或從佈局中移除時,Layout
該類將調用該InvalidateLayout
方法。
將InvalidateLayout
可以覆蓋來實現高速緩存,以儘量減少重複調用Measure
佈局的孩子的方法。覆蓋該InvalidateLayout
方法將提供一個關於什麼時候將子添加到佈局或從佈局中移除的通知。類似地,OnChildMeasureInvalidated
當其中一個佈局的子代改變大小時,該方法可被覆蓋以提供通知。對於這兩種方法覆蓋,自定義佈局應通過清除緩存來進行響應。有關更多信息,請參閱計算和緩存數據。
創建自定義佈局
創建自定義佈局的過程如下:
- 創建一個派生
Layout<View>
類的類。有關詳細信息,請參閱創建WrapLayout。 - [ 可選 ]添加可綁定屬性支持的屬性,用於佈局類中應設置的任何參數。有關詳細信息,請參閱添加由綁定屬性支持的屬性。
- 覆蓋在所有佈局的子項上
OnMeasure
調用Measure
方法的方法,並返回所需的佈局大小。有關更多信息,請參閱覆蓋OnMeasure方法。 -
覆蓋在所有佈局的子項上
LayoutChildren
調用Layout
方法的方法。未能Layout
在佈局中調用每個孩子的方法將導致孩子從未收到正確的大小或位置,因此該小孩將不會在頁面上顯示。有關更多信息,請參閱覆蓋LayoutChildren方法。當枚舉子進程
OnMeasure
並LayoutChildren
覆蓋時,請跳過其IsVisible
屬性設置爲的任何子級別false
。這將確保自定義佈局不會爲不可見的孩子留出空間。 -
[ 可選 ]覆蓋將
InvalidateLayout
子添加到佈局或從佈局中移除時通知的方法。有關更多信息,請參閱覆蓋InvalidateLayout方法。 - [ 可選 ]覆蓋
OnChildMeasureInvalidated
當其中一個佈局的子節點更改大小時要通知的方法。有關更多信息,請參閱覆蓋OnChildMeasureInvalidated方法。
請注意,
OnMeasure
如果佈局的大小由其父級而不是其子級控制,則不會調用override。但是,如果一個或兩個約束是無限的,或者如果佈局類具有非默認值HorizontalOptions
或VerticalOptions
屬性值,則將調用override 。因此,LayoutChildren
覆蓋不能依賴於OnMeasure
方法調用期間獲得的子大小。而是在調用該方法之前LayoutChildren
必須調用Measure
佈局的子節點上的Layout
方法。或者,OnMeasure
可以緩存在覆蓋中獲取的子項的大小,以避免Measure
在LayoutChildren
覆蓋中稍後調用,但佈局類將需要知道何時需要再次獲取大小。有關詳細信息,請參閱計算和緩存佈局數據。
然後可以通過將佈局類添加到一個Page
,並通過將子項添加到佈局來消費。有關更多信息,請參閱使用WrapLayout。
創建一個WrapLayout
示例應用程序演示了一個方向敏感WrapLayout
類,將其子級別橫跨頁面排列,然後將後續子項的顯示包裝到其他行。
根據WrapLayout
孩子的最大大小,該類爲每個孩子分配相同數量的空間,稱爲單元格大小。小於細胞大小的小孩可以根據其細胞的數量HorizontalOptions
和VerticalOptions
屬性值定位在細胞內。
所述WrapLayout
類的定義示於下面的代碼示例:
public class WrapLayout : Layout<View>
{
Dictionary<Size, LayoutData> layoutDataCache = new Dictionary<Size, LayoutData>();
...
}
計算和緩存佈局數據
該LayoutData
結構存儲有關的一些屬性的子項的集合數據:
VisibleChildCount
- 在佈局中可見的子項數。CellSize
- 所有孩子的最大大小,調整到佈局的大小。Rows
- 行數。Columns
- 列數。
該layoutDataCache
字段用於存儲多個LayoutData
值。當應用程序啓動時,兩個LayoutData
對象將被緩存到layoutDataCache
字典中用於當前方向
- 一個用於OnMeasure
覆蓋的約束參數,另一個用於覆蓋的參數width
和height
參數LayoutChildren
。當將設備旋轉爲橫向時,將再次調用OnMeasure
覆蓋和LayoutChildren
覆蓋,這將導致另外兩個LayoutData
對象被緩存到字典中。然而,當將設備返回到縱向方向時,不需要進一步的計算,因爲layoutDataCache
已經具有所需的數據。
以下代碼示例顯示了基於特定大小GetLayoutData
計算LayoutData
結構化屬性的方法:
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;
}
該GetLayoutData
方法執行以下操作:
- 它確定計算的
LayoutData
值是否已經在緩存中,並在可用時返回它。 - 否則,它枚舉所有的孩子,調用
Measure
每個孩子的方法無限寬和高,並確定最大的子大小。 - 如果至少有一個可見的子項,則它計算所需的行數和列數,然後根據該維數計算子項的單元格大小
WrapLayout
。請注意,單元格大小通常比最大小孩大小略寬,但如果WrapLayout
最寬的小孩不夠寬,或者最高的孩子足夠高,那麼它也可以更小。 - 它將新
LayoutData
值存儲在緩存中。
添加由綁定屬性支持的屬性
在WrapLayout
類定義ColumnSpacing
和RowSpacing
屬性,其值用於在佈局中的行和列中分離,並且通過綁定屬性被備份。可綁定屬性顯示在以下代碼示例中:
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();
});
每個可綁定屬性的屬性更改的處理程序調用InvalidateLayout
方法覆蓋以觸發新的佈局傳遞WrapLayout
。有關更多信息,請參閱覆蓋InvalidateLayout方法並覆蓋OnChildMeasureInvalidated方法。
覆蓋OnMeasure方法
的OnMeasure
倍率顯示在下面的代碼示例:
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);
}
覆蓋調用該GetLayoutData
方法並SizeRequest
從返回的數據構造一個對象,同時考慮RowSpacing
和ColumnSpacing
屬性值。有關該GetLayoutData
方法的更多信息,請參閱計算和緩存數據。
⚠️該
Measure
和OnMeasure
方法不應該通過返回的請求的無窮維SizeRequest
與屬性設置爲值Double.PositiveInfinity
。但是,至少有一個約束參數OnMeasure
可以是Double.PositiveInfinity
。
覆蓋LayoutChildren方法
的LayoutChildren
倍率顯示在下面的代碼示例:
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;
}
}
}
覆蓋開始於對該GetLayoutData
方法的調用,然後枚舉所有的孩子的大小並將它們放置在每個孩子的單元格內。這是通過調用LayoutChildIntoBoundingRegion
方法來實現的,該方法用於根據其HorizontalOptions
和VerticalOptions
屬性值將一個子項定位在一個矩形內。這相當於調用孩子的Layout
方法。
請注意,傳遞給該
LayoutChildIntoBoundingRegion
方法的矩形包括小孩所在的整個區域。
有關該GetLayoutData
方法的更多信息,請參閱計算和緩存數據。
覆蓋InvalidateLayout方法
InvalidateLayout
當孩子被添加到佈局中或從佈局中移除時,或當其中一個WrapLayout
屬性更改值時,將調用該覆蓋,如以下代碼示例所示:
protected override void InvalidateLayout()
{
base.InvalidateLayout();
layoutInfoCache.Clear();
}
覆蓋使佈局無效,並丟棄所有緩存的佈局信息。
要停止
Layout
類調用InvalidateLayout
每當孩子添加或刪除從佈局方法,覆蓋ShouldInvalidateOnChildAdded
和ShouldInvalidateOnChildRemoved
方法,並返回false
。然後,當添加或刪除子項時,佈局類可以實現自定義進程。
覆蓋OnChildMeasureInvalidated方法
OnChildMeasureInvalidated
當其中一個佈局的子節點更改大小時,將調用該覆蓋,並顯示在以下代碼示例中:
protected override void OnChildMeasureInvalidated()
{
base.OnChildMeasureInvalidated();
layoutInfoCache.Clear();
}
該覆蓋使子版面無效,並丟棄所有緩存的佈局信息。
消耗WrapLayout
的WrapLayout
類可以通過將其放置在被消耗Page
派生類型,如下面的XAML代碼示例表明:
<ContentPage ... xmlns:local="clr-namespace:ImageWrapLayout">
<ScrollView Margin="0,20,0,20">
<local:WrapLayout x:Name="wrapLayout" />
</ScrollView>
</ContentPage>
等效的C#代碼如下所示:
public class ImageWrapLayoutPageCS : ContentPage
{
WrapLayout wrapLayout;
public ImageWrapLayoutPageCS()
{
wrapLayout = new WrapLayout();
Content = new ScrollView
{
Margin = new Thickness(0, 20, 0, 20),
Content = wrapLayout
};
}
...
}
然後可以WrapLayout
根據需要將兒童加入。以下代碼示例顯示Image
要添加到的元素WrapLayout
:
protected override async void OnAppearing()
{
base.OnAppearing();
var images = await GetImageListAsync();
foreach (var photo in images.Photos)
{
var image = new Image
{
Source = ImageSource.FromUri(new Uri(photo + string.Format("?width={0}&height={0}&mode=max", Device.OnPlatform(240, 240, 120))))
};
wrapLayout.Children.Add(image);
}
}
async Task<ImageList> GetImageListAsync()
{
var requestUri = "https://docs.xamarin.com/demo/stock.json";
using (var client = new HttpClient())
{
var result = await client.GetStringAsync(requestUri);
return JsonConvert.DeserializeObject<ImageList>(result);
}
}
當包含WrapLayout
出現的頁面時,示例應用程序異步訪問包含照片列表的遠程JSON文件,Image
爲每張照片創建一個元素,並將其添加到WrapLayout
。這將導致以下屏幕截圖中顯示的外觀:
以下屏幕截圖顯示了WrapLayout
它被旋轉到橫向:
每行中的列數取決於照片大小,屏幕寬度和每個與設備無關的單元的像素數。該Image
元件異步加載的照片,並且因此,WrapLayout
類將接收到其頻繁調用LayoutChildren
每個方法Image
元件接收基於所加載的照片中的新的大小。
概要
本文介紹瞭如何編寫自定義佈局類,並演示了一種方向敏感WrapLayout
類,將其子級別橫跨頁面排列,然後將後續子項的顯示包裝到其他行。