전체 페이지뷰

2017년 2월 10일 금요일

Chapter 4. Scrolling the stack, part 1

프로그래머라면 이전 장에서 정적 Color 속성 목록을 확인하자마자, LabelText 속성으로 사용하여 색상을 식별하고, TextColor 속성을 사용하여 실제 색상을 표시하는 프로그램을 작성해보고 싶었을 것입니다.


FormattedString 객체를 사용하여 단일 Label로 이 작업을 해볼수도 있지만, 그보다는 여러 Label 객체를 사용하는 편이 훨씬 쉽습니다. 왜냐하면 여러 개의 Label 객체를 다룬다면 화면에 그 모든 객체를 보여주어야 하기 때문입니다.

ContentPage 클래스는 객체에 설정할 수있는 View 타입의 Content 속성을 정의하는데, 단 하나의 객체에 국한됩니다. 여러 개의 view를 표시하려면 Content 타입이 View 타입의 자식을 여러 개 가질 수 있는 클래스의 인스턴스로 설정해야 합니다. 이러한 클래스는 IList<T> 유형의 Children 속성을 정의하는 Layout<T>입니다.

Layout<T> 클래스는 추상 클래스이고, 그것을 구현한 Layout<View>로부터 네 개의 클래스가 파생됩니다.

  • AbsoluteLayout
  • Grid
  • RelativeLayout
  • StackLayout

각각 특징적인 방식으로 childeren을 배열합니다. 이 장에서는 StackLayout에 대해 중점적으로 설명합니다.

Stacks of views

StackLayout 클래스는 스택구조로 children을 배열하고, 오로지 두 가지의 속성만 가집니다.

Orientation (type StackOrientation): Vertical (default)과 Horizontal의 열거형
Spacing (type double): 6.0으로 초기화 되어 있음

StackLayout은 색 목록을 만드는 작업에 이상적입니다. Add 메서드를 사용하여 StackLayout 인스턴스의 Children 컬렉션에 자식을 추가 할 수 있습니다. 다음은 두 개의 배열로부터 여러 Label 객체를 만든 다음 각 LabelStackLayoutChildren 컬렉션에 추가하는 코드입니다.

Color[] colors =                
{
    Color.White, Color.Silver, Color.Gray, Color.Black, Color.Red,
    Color.Maroon, Color.Yellow, Color.Olive, Color.Lime, Color.Green,
    Color.Aqua, Color.Teal, Color.Blue, Color.Navy, Color.Pink,
    Color.Fuchsia, Color.Purple
};
string[] colorNames = 
{
    "White""Silver""Gray""Black""Red",
    "Maroon""Yellow""Olive""Lime""Green",
    "Aqua""Teal""Blue""Navy""Pink",
    "Fuchsia""Purple"
};
StackLayout stackLayout = new StackLayout();
for (int i = 0; i<colors.Length; i++)
{
    Label label = new Label
    {
        Text = colorNames[i],
        TextColor = colors[i],
        FontSize = Device.GetNamedSize(NamedSize.Large, typeof(Label))
    };
    stackLayout.Children.Add(label);
}
cs
그런 다음 StackLayout 객체를 페이지의 Content 속성으로 설정할 수 있습니다

그러나 병렬 배열을 사용하는 기술은 다소 위험합니다. 싱크가 맞지 않거나 배열의 개수가 다를 수 있기 때문입니다. ColorName 필드를 가지는 작은 구조체를 사용하거나, Tuple<Color, string>의 배열을 사용하거나, 아니면 아래 보여드릴 ColorLoop 프로그램 처럼 익명 형식으로 색과 이름을 함께 처리하는 것이 좋습니다.

public class ColorLoopPage : ContentPage
{
    public ColorLoopPage()
    {
        var colors = new[] 
        {
            new { value = Color.White, name = "White" },
            new { value = Color.Silver, name = "Silver" },
            new { value = Color.Gray, name = "Gray" },
            new { value = Color.Black, name = "Black" },
            new { value = Color.Red, name = "Red" },
            new { value = Color.Maroon, name = "Maroon" },
            new { value = Color.Yellow, name = "Yellow" },
            new { value = Color.Olive, name = "Olive" },
            new { value = Color.Lime, name = "Lime" },
            new { value = Color.Green, name = "Green" },
            new { value = Color.Aqua, name = "Aqua" },
            new { value = Color.Teal, name = "Teal" },
            new { value = Color.Blue, name = "Blue" },
            new { value = Color.Navy, name = "Navy" },
            new { value = Color.Pink, name = "Pink" },
            new { value = Color.Fuchsia, name = "Fuchsia" },
            new { value = Color.Purple, name = "Purple" }
        };
        StackLayout stackLayout = new StackLayout();
        foreach (var color in colors)
        {
            stackLayout.Children.Add(
                new Label
                {
                    Text = color.name,
                    TextColor = color.value,
                    FontSize = Device.GetNamedSize(NamedSize.Large, typeof(Label))
                });
        }
        Padding = new Thickness(5, Device.OnPlatform(2055), 55);
        Content = stackLayout;
    }
}
cs

또는 명시적인 컬렉션 뷰를 사용하여 StackLayoutChildren 속성을 초기화 할 수도 있습니다. ColorList 프로그램은 페이지의 Content 속성을 StackLayout 개체로 설정합니다.이 개체는 17 개의 Label 뷰로 초기화 된 Children 속성을가집니다.

public class ColorListPage : ContentPage
{
    public ColorListPage()
    {
        Padding = new Thickness(5, Device.OnPlatform(2055), 55);
        double fontSize = Device.GetNamedSize(NamedSize.Large, typeof(Label));
        Content = new StackLayout
        {
            Children =
            {
                new Label
                {
                    Text = "White",
                    TextColor=Color.White,
                    FontSize=fontSize
                },
                new Label
                {
                    Text = "Silver",
                    TextColor=Color.Silver,
                    FontSize=fontSize
                },
                new Label
                {
                    Text = "Fuschia",
                    TextColor=Color.Fuschia,
                    FontSize=fontSize
                },
                new Label
                {
                    Text = "Purple",
                    TextColor=Color.Purple,
                    FontSize=fontSize
                }
            }
        };
    }
}
cs

17개 색에 대한 모든 코드를 작성하지 않아도 여러분은 무슨 말인지 아셨을 겁니다. 어떤 방법을 사용하던 결과는 다음과 같습니다.


물론 이 화면은 최적화된 상태가 아닙니다. 폰트색이 너무 흐리거나 짙어서 잘 안보이기도 하고, 뒤의 두 플랫폼에서는 페이지가 화면을 넘어가 버리지만 스크롤할 방법이 없습니다.

해결방법 하나는 폰트 사이즈를 줄이는 것입니다. NamedSize.Large 보다 작은 값으로 시도해 보십시오.

또 하나의 해결 방법은 StackLayout 내에서 찾을 수 있습니다. StackLayoutSpacing이라는 double 타입의 속성을 갖고 있어서 children 간의 공간을 정의합니다. 디폴트 값은 6.0이지만 여러분이 조절할 수 있습니다.

Content = new Layout
{
    Spacing = 0,
    Children =
    {
         new Label
        {
            Text = "White",        
            TextColor = Color.White,
            FontSize = fontSize
        }
    ...

이렇게 하면 Label은 자신의 높이에 필요한 만큼의 공간만 차지할 것이고, Spacing을 음수로 설정하면 라벨들이 겹쳐서 나타날 것입니다.

하지만 가장 좋은 방법은 스크롤링이 가능하게 하는 것이며, 이것은 ScrollView라는 옵션을 추가하게 되면 StackLayout에서 자동지원 될것입니다(다음 섹션에서 알아봅니다).

이 색상 프로그램에는 또 하나의 문제점이 있는데 그것은 색상과 이름의 배열, 그리고 각 색상에 대한 Label 뷰를 명시적으로 만들어야 한다는 것입니다. 프로그래머에게 그것은 바람직한 방법은 아닐 것입니다. 그렇다면 그 과정을 자동화할 수 없을까요?


Scrolling  content

Xamarin.Forms는 .NET 기본 클래스 라이브러리를 이용할 수 있고, .NET reflection을 사용하여 Xamarin.Forms.Core와 같이 어셈블리에 정의된 모든 클래스와 구조에 대한 정보를 얻을 수 있다는 점을 기억하시기 바랍니다.

대부분의 .NET 리플렉션은 Type 객체로 시작됩니다. C#의 typeof 연산자를 사용하여 클래스 또는 구조체에 대한 Type 객체를 얻을 수 있습니다. 예를 들어, typeof(Color) 표현식은 Color 구조체의 Type 객체를 반환합니다.

PCL에서 사용할 수있는 .NET 버전에서 GetTypeInfo라는 Type 클래스의 확장 메서드는 추가 정보를 얻을 수있는 TypeInfo 개체를 반환합니다. 아래에 있는 프로그램에서는 필수적인 것은 아닙니다만, GetRuntimeFields 및 GetRuntimeProperties라는 Type 클래스 확장 메서드도 필요할 수 있습니다. 이들은 FieldInfoPropertyInfo 객체의 컬렉션 형태로 유형 및 필드의 속성을 반환합니다. 이것으로부터 프로퍼티의 이름 뿐 아니라 값까지도 알수 있습니다.

아래 ReflectedColors프로그램에 사용법이 나와 있습니다. ReflectedColorsPage.cs는 System.Reflectionusing문으로 포함시켜야 합니다.

두 개의 foreach문으로 ReflectedColorsPage 클래스는 Color 구조체의 필드와 속성을 순회합니다. Color 값을 반환하는 모든 public static 멤버에 대해 두 루프는 CreateColorLabel을 호출하여 Color 값과 이름을 가진 Label을 만든 다음 해당 LabelStackLayout에 추가합니다.

모든 공용 정적 필드 및 속성을 포함하기 때문에, 프로그램은 이전 프로그램에 표시된 17 개의 정적 필드에 덧붙여 Color.Transparent, Color.Default 및 Color.Accent도 포함한 각각의 Label 뷰를 만듭니다. 아래에 그 코드가 있습니다.

public class ReflectedColorsPage : ContentPage
{
    public ReflectedColorsPage()
    {
        StackLayout stackLayout = new StackLayout();
        // Color structure의 필드를 loop
        foreach (FieldInfo info in typeof(Color).GetRuntimeFields())
        {
            // 필요없는 색상(예를 들어 철자가 틀린 색상)은 스킵
            if (info.GetCustomAttribute<ObsoleteAttribute>() != null)
                continue;
            if (info.IsPublic &&
                info.IsStatic &&
                info.FieldType == typeof(Color))
            {
                stackLayout.Children.Add(CreateColorLabel((Color)info.GetValue(null), info.Name));
            }
        }
        // Color structure의 property loop
        foreach (PropertyInfo info in typeof(Color).GetRuntimeProperties())
        {
            MethodInfo methodInfo = info.GetMethod;
            if (methodInfo.IsPublic &&
                methodInfo.IsStatic &&
                methodInfo.ReturnType == typeof(Color))
            {
                stackLayout.Children.Add(CreateColorLabel((Color)info.GetValue(null), info.Name));
            }
        }
        Padding = new Thickness(5, Device.OnPlatform(2055), 55);
        // ScrollView에 StackLayout을 넣음
        Content = new ScrollView
        {
            Content=stackLayout
        };            
    }
    Label CreateColorLabel(Color color, string name)
    {
        Color backgroundColor = Color.Default;
        if (color != Color.Default)
        {
            // 표준 광도 계산
            double luminance = 0.30 * color.R + 
                0.59 * color.G +
                0.11 * color.B;
            backgroundColor = luminance > 0.5 ? Color.Black : Color.White;
        }
        // Label생성
        return new Label
        {
            Text = name,
            TextColor = color,
            FontSize = Device.GetNamedSize(NamedSize.Large, typeof(Label)),
            BackgroundColor = backgroundColor
        };
    }
}
cs

생성자에서 StackLayoutScrollViewContent 속성으로 설정되고, 그 ScrollView는 페이지의 Content 속성으로 설정됩니다.

클래스의 CreateColorLabel 메서드는 배경색과 대비되게 각 색상을 표시하려고합니다. 이 메소드는 각 R,G,B 표준 가중 평균에 기초하여 휘도 값을 계산 한 다음, 백색 또는 흑색의 배경을 선택하게 됩니다.

이 방법은 Transparent에는 통하지 않아서 어떤 방법으로도 출력이 되지 않을 겁니다. 그리고, Color.Default의 경우는 특수한 경우로 다루어, 그 색이 무엇이던 간에 Color.Default background color에 맞추게 됩니다.

미적으로 아직 덜 만족스럽지만 결과는 다음과 같습니다.
StackLayoutScrollView의 Child로 설정되어 스크롤이 가능합니다.

StackLayoutScrollView는 클래스 계층 구조와 관련이 있습니다. StackLayoutLayout <View>에서 파생되며, Layout<T> 클래스는 StackLayout이 상속하는 Children 속성을 정의한다는 것을 기억할 것입니다. 제너릭 Layout<T> 클래스는 넌제너릭 Layout 클래스에서 파생된 것이며, ScrollView 역시 이 넌제너릭 Layout에서 파생됩니다. 그러므로 이론적으로 ScrollView는 일종의 layout 객체입니다(비록 child가 하나 뿐이긴 하지만).

스크린샷에서 보시다시피 Label의 배경색은 StackLayout의 폭과 같고, 그것은 Label의 폭이 StackLayout의 폭과 같다는 것을 의미합니다.

Xamarin.Forms 레이아웃을 더 잘 이해하기 위해 약간의 실험을 해 봅시다. 이 실험에서는 StackLayout과 ScrollView의 배경색을 임시로 지정할 수 있습니다.

public class ReflectedColorsPage : ContentPage
{
    public ReflectedColorsPage()
    {
        StackLayout stackLayout = new StackLayout
        {
            BackgroundColor = Color.Blue
        };

...

        Content = new ScrollView
        {
            BackgroundColor = Color.Red,
            Content = stackLayout
        };
    }
}
cs

레이아웃 객체의 배경색은 transparent가 기본값입니다. 화면상에 공간을 차지하고는 있지만 보이지는 않는 것이죠. 임시로 레이아웃 객체의 배경색을 지정하면 스크린에서 어느 부위를 차지하는 지 알 수 있습니다. 복잡한 레이아웃을 디버깅하는 좋은 테크닉이라 할 수 있겠습니다.

파란색 StackLayout이 개별 Label 뷰 사이의 공간에서 보이게 보게됩니다. 이것은 StackLayout의 기본 Spacing 속성의 결과입니다. StackLayout은 transparent background를 가진 Color.DefaultLabel을 통해서도 볼 수 있습니다. 모든 Label 뷰의 HorizontalOptions 속성을 LayoutOptions.Start로 설정해보십시오.

return new Label 

    Text = name,
    TextColor = color,
    FontSize = Device.GetNamedSize(NamedSize.Large, typeof(Label)),
    BackgroundColor = backgroundColor,
    HorizontalOptions = LayoutOptions.Start 
};

이제 StackLayout의 파란색 배경이 훨씬 두드러지는데, 그 이유는 모든 Label 뷰가 텍스트가 필요로 하는 만큼의 가로 공간만 차지하면서 왼쪽에 붙어 있기 때문입니다. 모든 Label 뷰의 폭이 다르므로 전 버전보다 더 보기가 안 좋습니다.

이제 Label로부터 HorizontalOptions를 제거하고, 대신 StackLayout에 넣어 보십시오.

StackLayout stackLayout = new StackLayout 
{
    BackgroundColor = Color.Blue,
    HorizontalOptions = LayoutOptions.Start 
};

이제 StackLayout은 가장 넓은 Label (적어도 iOS, Android에서는)만큼 넓어지고 ScrollView의 빨간색 배경이 명확하게 보입니다.

Visual object의 트리를 생성하기 시작할 때, 이 객체들은 부모-자식 관계를 획득합니다. 이 때, 하위 객체의 위치와 크기가 상위 객체에 포함되기 때문에 상위 객체는 하위 객체의 컨테이너라고 부릅니다.

기본적으로 HorizontalOptions와 VerticalOptionsLayoutOptions.Fill로 설정되어, 자식 뷰가 부모 컨테이너를 채우려고 시도합니다. 심지어 배경색이 없어서 Label이 필요한 만큼만 공간을 차지하는 것처럼 보이는 상태에서도, 사실 Label은 상위 컨테이너를 채우고 있는 것입니다.

뷰의 HorizontalOptions 또는 VerticalOptions 속성을 LayoutOptions.Start, Center 또는 End로 설정하면, 뷰가 필요로하는 크기로, 가로, 세로 또는 둘 다 축소됩니다.

StackLayout은 자식의 세로 크기에 대해서도 동일한 효과를 보입니다. StackLayout의 모든 자식은 필요한 높이만큼만 차지합니다. StackLayout child의 VerticalOptions 속성을 Start, Center 또는 End로 설정해도 아무런 효과가 없습니다. 그러나, child 뷰는 StackLayout의 폭을 꽉 채울때까지 확장됩니다(HorizontalOptions LayoutOptions.Fill 외의 것을 설정한 때를 제외).

StackLayoutContentPageContent 속성으로 설정된 경우, HorizontalOptions 또는 VerticalOptions를 설정할 수 있습니다. 이 속성들은 두 가지 효과를 가지는데, 첫 째로 StackLayout의 폭 또는 높이 (또는 양쪽 모두)를 그 children의 사이즈에 맞게 축소 시킵니다. 두번째로는 Stacklayout이 페이지 내에서 상대적으로 어디에 위치하게 되는지를 결정합니다.

StackLayoutScrollView 내에 있으면 ScrollViewStackLayout이 children의 높이 합계만큼만 되도록 합니다. 그렇게 하여 ScrollViewStackLayout을 수직으로 스크롤하는 방법을 결정합니다. 또, StackLayoutHorizontalOptions 속성을 설정하여 너비와 가로 위치를 제어 할 수 있습니다.

그러나 ScrollView에서 VerticalOptions를 LayoutOptions.Start, Center 또는 End로 설정하지는 마시기 바랍니다. ScrollView는 children(일반적으로 StackLayout)이 필요로 하는 높이를 가정하여 내용이 얼마나 스크롤 되어야 할지 결정합니다. ScrollView에서 VerticalOptions를 LayoutOptions.Start, Center 또는 End로 설정한다 해도 ScrollView는 스크롤이라는 특성 때문에 높이가 정해져 있지 않으므로, 이론적으로 아무것도 축소되지 않을 겁니다. Xamarin.Forms는 이런 우발적인 사고를 막아주긴 하지만, 가장 좋은 것은 그런 코드를 작성하지 않는 것입니다.

StackLayoutScrollView에 두는 것은 정상이지만 StackLayoutScrollView를 두는 것은 옳지 않습니다. 이론적으로, StackLayoutScrollView가 필요한 만큼의 높이를 갖도록 강제할 것이며 그 높이는 기본적으로 0입니다. 역시 Xamarin.Forms가 막아줄 것이기는 하지만 이런 코드를 만드는 것은 옳지 않습니다.

Xamarin.Forms 레이아웃 원칙을 완벽하게 준수하는 StackLayoutScrollView를 배치하는 적절한 방법을 곧 보여 드리겠습니다

지금까지 StackLayoutScrollView의 수직적 요소에 대한 것을 살펴 보았습니다. StackLayout에는 Orientation이라는 속성이 있는데, 그것은 StackOrientation이라는 열거형의 멤버로서, Vertical(기본값), Horizontal로 이루어집니다. 이와 유사하게 ScrollView에도 ScrollOrientation이라는 열거형의 멤버가 있습니다. 아래의 코드를 따라해 보십시오.

public class ReflectedColorsPage : ContentPage
{        
    public ReflectedColorsPage()
    {
        StackLayout stackLayout = new StackLayout()
        {
            Orientation=StackOrientation.Horizontal                
        };
        ...
        Content = new ScrollView
        {
            Orientation=ScrollOrientation.Horizontal,                
            Content = stackLayout
        };     
    }
}
cs

이렇게 하면 Label 뷰는 가로로 쌓이고, ScrollView는 페이지를 세로로 채우지만 StackLayout을 가로로 스크롤 할 수 있게 됩니다.

기본 수직 레이아웃 옵션하에서 좀 이상하게 보이긴 하지만 , 좀 더 보기 좋게 고칠 수 있습니다.

댓글 없음:

댓글 쓰기