에서 사용자 지정 레이아웃 만들기 Xamarin.Forms

Download Sample 샘플 다운로드

Xamarin.Forms 는 StackLayout, AbsoluteLayout, RelativeLayout, Grid 및 FlexLayout의 5가지 레이아웃 클래스를 정의하고 각각 다른 방식으로 자식을 정렬합니다. 그러나 경우에 따라 에서 제공하지 Xamarin.Forms않는 레이아웃을 사용하여 페이지 콘텐츠를 구성해야 합니다. 이 문서에서는 사용자 지정 레이아웃 클래스를 작성하는 방법을 설명하고, 페이지에 걸쳐 자식을 가로로 정렬한 다음 후속 자식의 표시를 추가 행으로 래핑하는 방향 구분 WrapLayout 클래스를 보여 줍니다.

에서 Xamarin.Forms모든 레이아웃 클래스는 클래스에서 Layout<T> 파생되고 제네릭 형식과 해당 파생 형식을 View 제한합니다. 차례로 클래스는 Layout<T> 자식 요소의 Layout 위치 지정 및 크기 조정 메커니즘을 제공하는 클래스에서 파생됩니다.

모든 시각적 요소는 요청된 크기라고 하는 자체 기본 설정 크기를 결정합니다. Page, LayoutLayout<View> 파생 형식은 자식 또는 자식의 위치와 크기를 자체에 상대적으로 결정하는 역할을 합니다. 따라서 레이아웃에는 부모-자식 관계가 포함됩니다. 여기서 부모는 자식의 크기를 결정하지만 요청된 자식 크기를 수용하려고 시도합니다.

사용자 지정 레이아웃을 Xamarin.Forms 만들려면 레이아웃 및 무효화 주기를 철저히 이해해야 합니다. 이제 이러한 주기에 대해 설명합니다.

Layout

레이아웃은 페이지가 있는 시각적 트리의 맨 위에서 시작하며, 페이지의 모든 시각적 요소를 포함하도록 시각적 트리의 모든 분기를 진행합니다. 다른 요소의 부모인 요소는 자녀를 기준으로 크기를 조정하고 배치하는 작업을 담당합니다.

이 클래스는 VisualElement 레이아웃 작업에 대한 요소를 측정하는 메서드와 Layout 요소가 렌더링될 사각형 영역을 지정하는 메서드를 정의 Measure 합니다. 애플리케이션이 시작되고 첫 번째 페이지가 표시되면 개체에서 첫 번째 Measure 호출 및 Layout 호출로 구성된 레이아웃 주기Page 시작됩니다.

  1. 레이아웃 주기 동안 모든 부모 요소는 자식에서 메서드를 호출해야 Measure 합니다.
  2. 자식을 측정한 후에는 모든 부모 요소가 자식에서 메서드를 호출합니다 Layout .

이 주기를 사용하면 페이지의 모든 시각적 요소가 해당 및 Layout 메서드에 대한 호출을 Measure 받습니다. 이 프로세스는 다음 다이어그램에 나와 있습니다.

Xamarin.Forms Layout Cycle

참고 항목

레이아웃에 영향을 주기 위해 변경되는 경우 시각적 트리의 하위 집합에서도 레이아웃 주기가 발생할 수 있습니다. 여기에는 컬렉션에서 추가되거나 제거되는 항목( 예: StackLayout요소의 속성 변경 IsVisible 또는 요소 크기 변경)이 포함됩니다.

속성이 있는 ContentChildren 모든 Xamarin.Forms 클래스에는 재정의 가능한 메서드가 있습니다LayoutChildren. 파생되는 Layout<View> 사용자 지정 레이아웃 클래스는 이 메서드를 재정의하고 Measure 원하는 사용자 지정 레이아웃을 제공하기 위해 모든 요소의 자식에 대해 메서드와 Layout 메서드를 호출해야 합니다.

또한 파생 Layout 되거나 Layout<View> 재정의되어야 하는 모든 클래스는 레이아웃 클래스가 자식 메서드를 호출하여 필요한 크기를 결정하는 메서드를 Measure 재정 OnMeasure 의해야 합니다.

참고 항목

요소는 제약 조건에 따라 크기를 결정하며, 이는 요소의 부모 내에서 요소에 사용할 수 있는 공간의 양을 나타냅니다. 및 OnMeasure 메서드에 Measure 전달된 제약 조건은 0Double.PositiveInfinity에서 . 요소가 무한 인수가 없는 메서드에 Measure 대한 호출을 받을 때 요소가 제한되거나 완전히 제한됩니다. 요소는 특정 크기로 제한됩니다. 요소가 하나 이상의 인수 Double.PositiveInfinity 를 사용하여 메서드에 대한 호출 Measure 을 수신할 때 제약이 없거나 부분적으로 제한됩니다. 무한 제약 조건은 자동 크기 조정을 나타내는 것으로 간주될 수 있습니다.

무효화

무효화는 페이지의 요소를 변경하면 새 레이아웃 주기가 트리거되는 프로세스입니다. 요소가 더 이상 올바른 크기나 위치가 없는 경우 잘못된 것으로 간주됩니다. 예를 들어 변경 ButtonFontSize 속성 Button 이 더 이상 올바른 크기가 아니므로 유효하지 않다고 합니다. 크기를 조정하면 Button 페이지의 나머지 부분을 통해 레이아웃이 변경되는 파급 효과가 있을 수 있습니다.

요소는 일반적으로 요소의 속성이 변경되어 요소의 새 크기가 발생할 수 있는 경우 메서드를 호출 InvalidateMeasure 하여 자신을 무효화합니다. 이 메서드는 MeasureInvalidated 요소의 부모가 새 레이아웃 주기를 트리거하기 위해 처리하는 이벤트를 실행합니다.

클래스는 Layout 속성 또는 Children 컬렉션에 추가된 Content 모든 자식에 대해 이벤트에 대한 MeasureInvalidated 처리기를 설정하고 자식이 제거되면 처리기를 분리합니다. 따라서 자식이 있는 시각적 트리의 모든 요소는 자식 중 하나가 크기를 변경할 때마다 경고됩니다. 다음 다이어그램에서는 시각적 트리의 요소 크기 변경으로 인해 트리가 파급되는 변경이 발생하는 방법을 보여 줍니다.

Invalidation in the Visual Tree

그러나 클래스는 Layout 페이지 레이아웃에 대한 자식 크기 변경의 영향을 제한하려고 합니다. 레이아웃의 크기가 제한되는 경우 자식 크기 변경은 시각적 트리의 부모 레이아웃보다 큰 항목에 영향을 주지 않습니다. 그러나 일반적으로 레이아웃의 크기 변경은 레이아웃이 자식을 정렬하는 방식에 영향을 줍니다. 따라서 레이아웃 크기를 변경하면 레이아웃에 대한 레이아웃 주기가 시작되고 레이아웃은 해당 및 LayoutChildren 메서드에 대한 호출을 OnMeasure 받습니다.

또한 클래스는 Layout 메서드와 InvalidateLayout 비슷한 용도의 메서드를 정의합니다 InvalidateMeasure . 레이아웃이 InvalidateLayout 자식의 위치를 지정하고 크기를 조정하는 방식에 영향을 주는 변경이 있을 때마다 메서드를 호출해야 합니다. 예를 들어 클래스는 Layout 자식이 레이아웃에 InvalidateLayout 추가되거나 레이아웃에서 제거될 때마다 메서드를 호출합니다.

InvalidateLayout 레이아웃 자식 메서드의 반복 Measure 호출을 최소화하기 위해 캐시를 구현하도록 재정의할 수 있습니다. 메서드를 InvalidateLayout 재정의하면 자식이 레이아웃에 추가되거나 레이아웃에서 제거되는 시기를 알 수 있습니다. 마찬가지로 레이아웃의 OnChildMeasureInvalidated 자식 중 하나가 크기를 변경할 때 알림을 제공하도록 메서드를 재정의할 수 있습니다. 두 메서드 재정의의 경우 사용자 지정 레이아웃은 캐시를 지워 응답해야 합니다. 자세한 내용은 계산 및 캐시 레이아웃 데이터를 참조 하세요.

사용자 지정 레이아웃 만들기

사용자 지정 레이아웃을 만드는 프로세스는 다음과 같습니다.

  1. Layout<View> 클래스에서 파생되는 클래스를 만듭니다. 자세한 내용은 WrapLayout 만들기를 참조하세요.

  2. [선택 사항] 레이아웃 클래스에서 설정해야 하는 매개 변수에 대해 바인딩 가능한 속성으로 지원되는 속성을 추가합니다. 자세한 내용은 바인딩 가능한 속성으로 지원되는 속성 추가를 참조 하세요.

  3. 모든 레이아웃의 자식에서 메서드를 Measure 호출하고 레이아웃에 대해 요청된 크기를 반환하도록 메서드를 재정 OnMeasure 의합니다. 자세한 내용은 OnMeasure 메서드 재정의를 참조 하세요.

  4. 모든 레이아웃의 LayoutChildren 자식에서 메서드를 Layout 호출하도록 메서드를 재정의합니다. 레이아웃에서 각 자식에서 메서드를 호출 Layout 하지 않으면 자식이 올바른 크기나 위치를 받지 못하므로 자식이 페이지에 표시되지 않습니다. 자세한 내용은 LayoutChildren 메서드 재정의를 참조 하세요.

    참고 항목

    및 재정의에서 OnMeasureLayoutChildren 자식을 열거하는 경우 속성이 IsVisible .로 설정된 false모든 자식을 건너뜁니다. 이렇게 하면 사용자 지정 레이아웃이 보이지 않는 자식에 대한 공간을 남기지 않습니다.

  5. [선택 사항] 자식이 레이아웃에 추가되거나 레이아웃에서 제거될 때 알림을 받을 메서드를 재정 InvalidateLayout 의합니다. 자세한 내용은 InvalidateLayout 메서드 재정의를 참조 하세요.

  6. [선택 사항] 레이아웃의 OnChildMeasureInvalidated 자식 중 하나가 크기를 변경할 때 알림을 받을 메서드를 재정의합니다. 자세한 내용은 OnChildMeasureInvalidated 메서드 재정의를 참조하세요.

참고 항목

레이아웃의 크기가 OnMeasure 자식이 아닌 부모에 의해 제어되는 경우 재정의가 호출되지 않습니다. 그러나 제약 조건 중 하나 또는 둘 다 무한하거나 레이아웃 클래스에 기본 HorizontalOptionsVerticalOptions 값이 아닌 속성 값이 있는 경우 재정의가 호출됩니다. 이러한 이유로 재정의는 LayoutChildren 메서드 호출 중에 OnMeasure 얻은 자식 크기에 의존할 수 없습니다. 대신 메서드 LayoutChildrenMeasure 호출하기 전에 레이아웃의 자식에서 메서드를 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 에 대한 것이고, 하나는 재정의 widthLayoutChildren 에 대한 인수입니다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 값을 저장합니다.

바인딩 가능한 속성으로 백업된 속성 추가

클래스는 WrapLayoutColumnSpacing 레이아웃의 행과 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파일에 추가합니다. 이로 인해 결국 다음 스크린샷에 표시된 모양이 됩니다.

Sample Application Portrait Screenshots

다음 스크린샷은 가로 방향으로 회전된 후를 보여 WrapLayout 줍니다.

Sample iOS Application Landscape ScreenshotSample Android Application Landscape ScreenshotSample UWP Application Landscape Screenshot

각 행의 열 수는 사진 크기, 화면 너비 및 디바이스 독립적 단위당 픽셀 수에 따라 달라집니다. Image 요소는 사진을 비동기적으로 로드하므로 WrapLayoutImage 요소가 로드된 사진에 따라 새 크기를 받을 때 클래스는 해당 LayoutChildren 메서드에 대한 호출을 자주 받습니다.