에서 사용자 지정 레이아웃 만들기 Xamarin.Forms
Xamarin.Forms 는 StackLayout, AbsoluteLayout, RelativeLayout, Grid 및 FlexLayout의 5가지 레이아웃 클래스를 정의하고 각각 다른 방식으로 자식을 정렬합니다. 그러나 경우에 따라 에서 제공하지 Xamarin.Forms않는 레이아웃을 사용하여 페이지 콘텐츠를 구성해야 합니다. 이 문서에서는 사용자 지정 레이아웃 클래스를 작성하는 방법을 설명하고, 페이지에 걸쳐 자식을 가로로 정렬한 다음 후속 자식의 표시를 추가 행으로 래핑하는 방향 구분 WrapLayout 클래스를 보여 줍니다.
에서 Xamarin.Forms모든 레이아웃 클래스는 클래스에서 Layout<T>
파생되고 제네릭 형식과 해당 파생 형식을 View
제한합니다. 차례로 클래스는 Layout<T>
자식 요소의 Layout
위치 지정 및 크기 조정 메커니즘을 제공하는 클래스에서 파생됩니다.
모든 시각적 요소는 요청된 크기라고 하는 자체 기본 설정 크기를 결정합니다. Page
, Layout
및 Layout<View>
파생 형식은 자식 또는 자식의 위치와 크기를 자체에 상대적으로 결정하는 역할을 합니다. 따라서 레이아웃에는 부모-자식 관계가 포함됩니다. 여기서 부모는 자식의 크기를 결정하지만 요청된 자식 크기를 수용하려고 시도합니다.
사용자 지정 레이아웃을 Xamarin.Forms 만들려면 레이아웃 및 무효화 주기를 철저히 이해해야 합니다. 이제 이러한 주기에 대해 설명합니다.
레이아웃
레이아웃은 페이지가 있는 시각적 트리의 맨 위에서 시작하며, 페이지의 모든 시각적 요소를 포함하도록 시각적 트리의 모든 분기를 진행합니다. 다른 요소의 부모인 요소는 자녀를 기준으로 크기를 조정하고 배치하는 작업을 담당합니다.
이 클래스는 VisualElement
레이아웃 작업에 대한 요소를 측정하는 메서드와 Layout
요소가 렌더링될 사각형 영역을 지정하는 메서드를 정의 Measure
합니다. 애플리케이션이 시작되고 첫 번째 페이지가 표시되면 개체에서 첫 번째 Measure
호출 및 Layout
호출로 구성된 레이아웃 주기가 Page
시작됩니다.
- 레이아웃 주기 동안 모든 부모 요소는 자식에서 메서드를 호출해야
Measure
합니다. - 자식을 측정한 후에는 모든 부모 요소가 자식에서 메서드를 호출합니다
Layout
.
이 주기를 사용하면 페이지의 모든 시각적 요소가 해당 및 Layout
메서드에 대한 호출을 Measure
받습니다. 이 프로세스는 다음 다이어그램에 나와 있습니다.
참고 항목
레이아웃에 영향을 주기 위해 변경되는 경우 시각적 트리의 하위 집합에서도 레이아웃 주기가 발생할 수 있습니다. 여기에는 컬렉션에서 추가되거나 제거되는 항목( 예: StackLayout
요소의 속성 변경 IsVisible
또는 요소 크기 변경)이 포함됩니다.
속성이 있는 Content
Children
모든 Xamarin.Forms 클래스에는 재정의 가능한 메서드가 있습니다LayoutChildren
. 파생되는 Layout<View>
사용자 지정 레이아웃 클래스는 이 메서드를 재정의하고 Measure
원하는 사용자 지정 레이아웃을 제공하기 위해 모든 요소의 자식에 대해 메서드와 Layout
메서드를 호출해야 합니다.
또한 파생 Layout
되거나 Layout<View>
재정의되어야 하는 모든 클래스는 레이아웃 클래스가 자식 메서드를 호출하여 필요한 크기를 결정하는 메서드를 Measure
재정 OnMeasure
의해야 합니다.
참고 항목
요소는 제약 조건에 따라 크기를 결정하며, 이는 요소의 부모 내에서 요소에 사용할 수 있는 공간의 양을 나타냅니다. 및 OnMeasure
메서드에 Measure
전달된 제약 조건은 0Double.PositiveInfinity
에서 . 요소가 무한 인수가 없는 메서드에 Measure
대한 호출을 받을 때 요소가 제한되거나 완전히 제한됩니다. 요소는 특정 크기로 제한됩니다. 요소가 하나 이상의 인수 Double.PositiveInfinity
를 사용하여 메서드에 대한 호출 Measure
을 수신할 때 제약이 없거나 부분적으로 제한됩니다. 무한 제약 조건은 자동 크기 조정을 나타내는 것으로 간주될 수 있습니다.
무효화
무효화는 페이지의 요소를 변경하면 새 레이아웃 주기가 트리거되는 프로세스입니다. 요소가 더 이상 올바른 크기나 위치가 없는 경우 잘못된 것으로 간주됩니다. 예를 들어 변경 Button
된 FontSize
속성 Button
이 더 이상 올바른 크기가 아니므로 유효하지 않다고 합니다. 크기를 조정하면 Button
페이지의 나머지 부분을 통해 레이아웃이 변경되는 파급 효과가 있을 수 있습니다.
요소는 일반적으로 요소의 속성이 변경되어 요소의 새 크기가 발생할 수 있는 경우 메서드를 호출 InvalidateMeasure
하여 자신을 무효화합니다. 이 메서드는 MeasureInvalidated
요소의 부모가 새 레이아웃 주기를 트리거하기 위해 처리하는 이벤트를 실행합니다.
클래스는 Layout
속성 또는 Children
컬렉션에 추가된 Content
모든 자식에 대해 이벤트에 대한 MeasureInvalidated
처리기를 설정하고 자식이 제거되면 처리기를 분리합니다. 따라서 자식이 있는 시각적 트리의 모든 요소는 자식 중 하나가 크기를 변경할 때마다 경고됩니다. 다음 다이어그램에서는 시각적 트리의 요소 크기 변경으로 인해 트리가 파급되는 변경이 발생하는 방법을 보여 줍니다.
그러나 클래스는 Layout
페이지 레이아웃에 대한 자식 크기 변경의 영향을 제한하려고 합니다. 레이아웃의 크기가 제한되는 경우 자식 크기 변경은 시각적 트리의 부모 레이아웃보다 큰 항목에 영향을 주지 않습니다. 그러나 일반적으로 레이아웃의 크기 변경은 레이아웃이 자식을 정렬하는 방식에 영향을 줍니다. 따라서 레이아웃 크기를 변경하면 레이아웃에 대한 레이아웃 주기가 시작되고 레이아웃은 해당 및 LayoutChildren
메서드에 대한 호출을 OnMeasure
받습니다.
또한 클래스는 Layout
메서드와 InvalidateLayout
비슷한 용도의 메서드를 정의합니다 InvalidateMeasure
. 레이아웃이 InvalidateLayout
자식의 위치를 지정하고 크기를 조정하는 방식에 영향을 주는 변경이 있을 때마다 메서드를 호출해야 합니다. 예를 들어 클래스는 Layout
자식이 레이아웃에 InvalidateLayout
추가되거나 레이아웃에서 제거될 때마다 메서드를 호출합니다.
InvalidateLayout
레이아웃 자식 메서드의 반복 Measure
호출을 최소화하기 위해 캐시를 구현하도록 재정의할 수 있습니다. 메서드를 InvalidateLayout
재정의하면 자식이 레이아웃에 추가되거나 레이아웃에서 제거되는 시기를 알 수 있습니다. 마찬가지로 레이아웃의 OnChildMeasureInvalidated
자식 중 하나가 크기를 변경할 때 알림을 제공하도록 메서드를 재정의할 수 있습니다. 두 메서드 재정의의 경우 사용자 지정 레이아웃은 캐시를 지워 응답해야 합니다. 자세한 내용은 계산 및 캐시 레이아웃 데이터를 참조 하세요.
사용자 지정 레이아웃 만들기
사용자 지정 레이아웃을 만드는 프로세스는 다음과 같습니다.
Layout<View>
클래스에서 파생되는 클래스를 만듭니다. 자세한 내용은 WrapLayout 만들기를 참조하세요.[선택 사항] 레이아웃 클래스에서 설정해야 하는 매개 변수에 대해 바인딩 가능한 속성으로 지원되는 속성을 추가합니다. 자세한 내용은 바인딩 가능한 속성으로 지원되는 속성 추가를 참조 하세요.
모든 레이아웃의 자식에서 메서드를
Measure
호출하고 레이아웃에 대해 요청된 크기를 반환하도록 메서드를 재정OnMeasure
의합니다. 자세한 내용은 OnMeasure 메서드 재정의를 참조 하세요.모든 레이아웃의
LayoutChildren
자식에서 메서드를Layout
호출하도록 메서드를 재정의합니다. 레이아웃에서 각 자식에서 메서드를 호출Layout
하지 않으면 자식이 올바른 크기나 위치를 받지 못하므로 자식이 페이지에 표시되지 않습니다. 자세한 내용은 LayoutChildren 메서드 재정의를 참조 하세요.참고 항목
및 재정의에서
OnMeasure
LayoutChildren
자식을 열거하는 경우 속성이IsVisible
.로 설정된false
모든 자식을 건너뜁니다. 이렇게 하면 사용자 지정 레이아웃이 보이지 않는 자식에 대한 공간을 남기지 않습니다.[선택 사항] 자식이 레이아웃에 추가되거나 레이아웃에서 제거될 때 알림을 받을 메서드를 재정
InvalidateLayout
의합니다. 자세한 내용은 InvalidateLayout 메서드 재정의를 참조 하세요.[선택 사항] 레이아웃의
OnChildMeasureInvalidated
자식 중 하나가 크기를 변경할 때 알림을 받을 메서드를 재정의합니다. 자세한 내용은 OnChildMeasureInvalidated 메서드 재정의를 참조하세요.
참고 항목
레이아웃의 크기가 OnMeasure
자식이 아닌 부모에 의해 제어되는 경우 재정의가 호출되지 않습니다. 그러나 제약 조건 중 하나 또는 둘 다 무한하거나 레이아웃 클래스에 기본 HorizontalOptions
VerticalOptions
값이 아닌 속성 값이 있는 경우 재정의가 호출됩니다. 이러한 이유로 재정의는 LayoutChildren
메서드 호출 중에 OnMeasure
얻은 자식 크기에 의존할 수 없습니다. 대신 메서드 LayoutChildren
를 Measure
호출하기 전에 레이아웃의 자식에서 메서드를 Layout
호출해야 합니다. 또는 재정의에서 OnMeasure
가져온 자식의 크기를 캐시하여 재정의에서 나중에 Measure
호출 LayoutChildren
하지 않도록 할 수 있지만 레이아웃 클래스는 크기를 다시 가져와야 하는 시기를 알아야 합니다. 자세한 내용은 계산 및 캐시 레이아웃 데이터를 참조 하세요.
그런 다음 레이아웃 클래스를 추가하고 Page
레이아웃에 자식을 추가하여 레이아웃 클래스를 사용할 수 있습니다. 자세한 내용은 WrapLayout 사용 방법을 참조하세요.
WrapLayout 만들기
샘플 애플리케이션은 페이지에 걸쳐 자식을 가로로 정렬한 다음 후속 자식의 표시를 추가 행으로 래핑하는 방향 구분 WrapLayout
클래스를 보여 줍니다.
클래스는 WrapLayout
자식의 최대 크기에 따라 셀 크기라고 하는 각 자식에 대해 동일한 양의 공간을 할당합니다. 셀 크기보다 작은 자식은 해당 값과 VerticalOptions
속성 값에 HorizontalOptions
따라 셀 내에 배치할 수 있습니다.
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
LayoutChildren
에 대한 인수입니다height
. 디바이스를 가로 방향으로 회전할 때 재정의 OnMeasure
및 재정의 LayoutChildren
가 다시 호출되어 다른 두 LayoutData
개체가 사전에 캐시됩니다. 그러나 디바이스를 세로 방향으로 반환하는 경우 필요한 데이터가 이미 있으므로 layoutDataCache
더 이상 계산이 필요하지 않습니다.
다음 코드 예제에서는 특정 크기에 따라 구조적 속성을 LayoutData
계산하는 메서드를 보여 GetLayoutData
줍니다.
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
동시에 속성 값과 ColumnSpacing
속성 값을 고려 RowSpacing
합니다. 메서드에 대한 GetLayoutData
자세한 내용은 계산 및 캐시 레이아웃 데이터를 참조 하세요.
Important
및 메서드는 Measure
속성이 설정된 값을 반환하여 SizeRequest
무한 차원을 요청해서는 안 됩니다Double.PositiveInfinity
.OnMeasure
그러나 제약 조건 인수 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
수행됩니다. 이 메서드는 해당 값과 VerticalOptions
속성 값에 HorizontalOptions
따라 직사각형 내에 자식을 배치하는 데 사용됩니다. 이는 자식 메서드 Layout
를 호출하는 것과 같습니다.
참고 항목
메서드에 전달된 사각형에는 자식이 상주할 LayoutChildIntoBoundingRegion
수 있는 전체 영역이 포함됩니다.
메서드에 대한 GetLayoutData
자세한 내용은 계산 및 캐시 레이아웃 데이터를 참조 하세요.
InvalidateLayout 메서드 재정의
재정의는 InvalidateLayout
다음 코드 예제와 같이 자식이 레이아웃에 추가되거나 제거되거나 속성 중 WrapLayout
하나가 값을 변경할 때 호출됩니다.
protected override void InvalidateLayout()
{
base.InvalidateLayout();
layoutInfoCache.Clear();
}
재정의는 레이아웃을 무효화하고 캐시된 모든 레이아웃 정보를 삭제합니다.
참고 항목
자식이 레이아웃에 추가되거나 레이아웃에서 제거될 때마다 메서드를 호출하는 클래스를 중지 Layout
하려면 해당 메서드와 ShouldInvalidateOnChildRemoved
메서드를 재정의 ShouldInvalidateOnChildAdded
하고 반환합니다false
.InvalidateLayout
레이아웃 클래스는 자식이 추가되거나 제거될 때 사용자 지정 프로세스를 구현할 수 있습니다.
OnChildMeasureInvalidated 메서드 재정의
재정의는 OnChildMeasureInvalidated
레이아웃의 자식 중 하나가 크기를 변경할 때 호출되며 다음 코드 예제에 나와 있습니다.
protected override void OnChildMeasureInvalidated()
{
base.OnChildMeasureInvalidated();
layoutInfoCache.Clear();
}
재정의는 자식 레이아웃을 무효화하고 캐시된 레이아웃 정보를 모두 삭제합니다.
WrapLayout 사용
클래스는 WrapLayout
다음 XAML 코드 예제와 같이 파생 형식에 Page
배치하여 사용할 수 있습니다.
<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
수 있습니다. 다음 코드 예제에서는 다음 요소에 추가되는 요소를 WrapLayout
보여줍니다Image
.
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;
}
이 페이지가 WrapLayout
표시되면 샘플 애플리케이션은 사진 목록이 포함된 원격 JSON 파일에 비동기적으로 액세스하고 각 사진에 대한 요소를 만들고 Image
해당 WrapLayout
파일에 추가합니다. 이로 인해 결국 다음 스크린샷에 표시된 모양이 됩니다.
다음 스크린샷은 가로 방향으로 회전된 후를 보여 WrapLayout
줍니다.
각 행의 열 수는 사진 크기, 화면 너비 및 디바이스 독립적 단위당 픽셀 수에 따라 달라집니다. Image
요소는 사진을 비동기적으로 로드하므로 WrapLayout
각 Image
요소가 로드된 사진에 따라 새 크기를 받을 때 클래스는 해당 LayoutChildren
메서드에 대한 호출을 자주 받습니다.