전체 페이지뷰

2017년 4월 17일 월요일

Chapter 8. Code and XAML in harmony, part 2


Custom XAML-based views



이전 장의 ScaryColorList 프로그램은 Frame, BoxView, Label을 사용하여 StackLayout에 몇 가지 색상을 나열했습니다. 3 가지 색상만으로도 마크 업이 기분 나쁘도록 반복적으로 작성 되었습니다. 불행히도 C# for 혹은 while 루프를 복제하는 XAML 태그가 없으므로 코드에서 유사한 항목을 반복 생성하거나 태그에서 더 좋은 방법을 찾는 수밖에 없습니다.

이 책에서 XAML로 색상 리스트를 만드는 몇가지 방법을 보시게 될 것이고, 아주 깔끔하고 우아한 방법을 배우시게 될 겁니다. 그러나 거기에 도달하기 전에 Xamarin.Forms에 대해 좀 더 공부하셔야 합니다. 그 때까지는 비슷한 환경에서 유사한 방법들을 찾아 보겠습니다.

한 가지 전략은 색상 상자와 이름을 표시하는 목적만을 가진 custom 뷰를 만드는 것입니다. 그리고 색상의 16진수 RGB값도 동시에 표시합니다. 그런 다음 개별 색상에 대해 XAML 페이지 파일에서 그 custom view를 사용할 수 있습니다.

어떻게 XAML 파일에서 이것을 참조할 수 있을까요?
보다 정확하게 표현하자면, 마크업이 어떻게 보이길 원하십니까?

마크업이 이와 같이 보일 경우 반복한다고 해도 별로 이상해 보이지 않으며, 코드의 Color 값 배열을 명시적으로 정의하는 것보다는 나아 보입니다.

<StackLayout>
    <MyColorView Color="Red" /
    <MyColorView Color="Green" />
    <MyColorView Color="Blue" />
      …
</StackLayout>
cs

딱 이렇게 보이지는 않을지 몰라도, 어쨌거나 MyColorView는 분명히 사용자 정의 클래스이며 Xamarin.Forms API의 일부가 아닙니다. 따라서 XML 네임스페이스 선언에 정의된 네임스페이스 접두사 없이는 XAML 파일에 나타날 수 없습니다.

XML 접두사를 적용하면 이 커스텀 뷰가 Xamarin.Forms API의 일부인지 혼동될 리 없으므로,이제 MyColorView가 아나라 당당하게 ColorView라는 이름을 지정하겠습니다.

이 가상의 ColorView 클래스는 LabelView, FrameView, BoxView와 같은 기존 뷰만으로 구성되기 때문에 매우 쉬운 사용자 정의보기의 예이며, StackLayout을 사용하여 정렬되었습니다. Xamarin.Forms는 이러한 뷰 배열의 부모로서 특별히 설계된 뷰를 정의하며, 그것이 ContentView입니다. ContentPage와 마찬가지로, ContentView에는 Content 속성이 있어서 다른 뷰를 시각적 트리로서 설정할 수 있습니다. ContentView의 content를 코드로 정의할 수도 있지만, XAML로 하는 편이 더 흥미로울 것입니다.

ColorViewList라는 솔루션을 만들어 보도록 합시다. 이 솔루션에는  XAML과 code-behind파일 두 세트가 들어갈 것인데, 첫 번째는 ContentPage에서 파생 된 ColorViewListPage 클래스이고 두 번째는 ContentView에서 파생 된 ColorView라는 클래스입니다.

Visual Studio에서 ColorView 클래스를 만들려면 ColorViewList 프로젝트에 새 XAML 페이지를 추가 할 때와 동일한 절차로 진행합니다: 솔루션 탐색기 상에서 프로젝트 명에 우클릭하고, 추가 > 새항목을 선택합니다. 새 항목 추가 대화창 왼쪽에서 Visual C# > Cross-Platform을 선택하고 Forms Xaml Page를 고릅니다. 이름을 ColorView.cs로 입력하고, 잊기 전에 즉시 ColorView.xaml 파일로 들어가서 ContentPage 시작, 끝 태그를 ContentView로 바꿔줍니다. ColorView.xaml.cs 파일에서 베이스 클래스 역시 ContentView로 바꿉니다.
-- 이 과정은 현재 자마린 스튜디오의 것과 유사하게 바뀌었습니다. 전에 덧붙인 것과 마찬가지로 ColorViewList를 생성하고 추가>새 항목, Visual C# > Cross-Platform을 선택하면 위의 설명과는 다르게 Forms ContentView Xaml 항목이 있어서 그것을 선택하면 되겠습니다--

Xamarin studio에서는 이 과정이 좀 더 간단한데, ColorViewList 툴메뉴로부터 Add > New File을 선택하고, New File 대화창이 뜨면 좌측에서 Forms를 선택하고, Forms ContentView Xaml(Forms ContentPage Xaml이 아님)를 선택하고 ColorView라고 이름을 지어줍니다.

평소와 같이 ColorViewListPage 클래스에 대한 XAML 파일과 code-behind 파일도 만듭니다.

ColorView.xaml 파일은 실제 색상 값은 넣지 않고 개별 색상 항목의 레이아웃만을 서술합니다. 대신 BoxView와 두 개의 Label 뷰에는 이름을 지정합니다.

<ContentView xmlns="http://xamarin.com/schemas/2014/forms" 
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
             x:Class="ColorViewList.ColorView">
    <Frame OutlineColor="Accent">
        <StackLayout Orientation="Horizontal">
            <BoxView x:Name="boxView"
                     WidthRequest="70"
                     HeightRequest="70" />
            <StackLayout>
                <Label x:Name="colorNameLabel"
                       FontSize="Large"
                       VerticalOptions="CenterAndExpand" />
                <Label x:Name="colorValueLabel"
                       VerticalOptions="CenterAndExpand" />
            </StackLayout>
        </StackLayout>                     
    </Frame>
</ContentView>
cs

처음에는 named view를 모두 저기 가져 오고 싶을 겁니다.하지만 실제 프로그래밍에서는 나중에 시각적인 면을 미세 조정할 시간이 충분합니다.

비주얼 말고도, 이 ColorView 클래스는 색상을 설정하는 새로운 프로퍼티가 필요합니다. 이 프로퍼티는 code-behind 파일에 정의되어야 합니다. 처음에는, ColorViewColor 타입의 Color라는 속성을 지정하는 것이 합리적으로 보입니다 (전에 MyColorView에서 본 XAML 스니펫처럼 말이죠). 그러나 ColorView 클래스는 색상 이름을 출력해야 하고, Color 값에서 색상 이름을 가져올 수는 없습니다.

대신 string 타입의 ColorName이라는 프로퍼티를 정의하는 것이 더 합리적으로 보입니다. code-behind 파일은 리플렉션을 사용하여 해당 이름에 해당하는 Color 클래스의 정적 필드를 가져올 수 있습니다.

잠깐만요.  Xamarin.Forms에는 XAML 파서가 "빨강"또는 "파랑"과 같은 텍스트 색상 이름을 Color 값으로 변환하는데 사용하는 public ColorTypeConverter 클래스가 포함되어 있습니다. 그것을 사용하면 어떨까요?

ColorView의 code-behind 파일을 보여 드리겠습니다. ColorNameLabelText 프로퍼티를 색상 이름으로 설정하도록 set 접근자를 둔 ColorName 프로퍼티를 정의하고 있고, ColorTypeConverter를 사용해서 이름을 Color 값으로 변환합니다. 이 Color 값은 boxViewColor 프로퍼티와 colorValueLabelText 프로퍼티를 RGB 값으로 설정하는데 사용됩니다.
    public partial class ColorView : ContentView
    {
        string colorName;
        ColorTypeConverter colorTypeConv = new ColorTypeConverter();
        public ColorView()
        {
            InitializeComponent();
        }
        public string ColorName
        {
            set
            {
                // Set the name
                colorName = value;
                colorNameLabel.Text = value;
                // 실제 Color 값을 가져오고 다른 뷰들을 설정
                Color color = (Color)colorTypeConv.ConvertFrom(colorName);
                boxView.Color = color;
                colorValueLabel.Text = String.Format("{0:X2}-{1:X2}-{2:X2}",
                    (int)255 * color.R,
                    (int)255 * color.G,
                    (int)255 * color.B);
            }
            get
            {
                return colorName;
            }
        }
    }
cs

*ConvertFrom도 obsolete가 떴네요. ConvertFromInvariantString을 사용하라고 합니다.

ColorView 클래스가 끝났습니다. 이제 ColorViewListPage(바뀐 템플릿으로 생성된 솔루션에서는 MainPage)를 봅시다. ColorViewListPage.xaml 파일(MainPage.xaml)은 여러 ColorView 인스턴스를 나열해야하므로 ColorView element를 참조하는 새로운 네임스페이스 접두사가 있는 XML 네임스페이스 선언이 필요합니다.

ColorView 클래스는 ColorViewListPage와 동일한 프로젝트의 일부입니다. 일반적으로 프로그래머는 이러한 경우에 local이라는 XML 네임스페이스 접두어를 사용합니다. 새 네임스페이스 선언은 다음 형식으로 XAML 파일의 루트 element에 나타납니다.

xmlns:local="clr-namespace:ColorViewList;assembly=ColorViewList"
cs

일반적인 경우 사용자 지정 XML 네임스페이스 선언은 공용 언어 런타임(CLR, .NET 네임 스페이스라고도 부름) 네임스페이스와 어셈블리를 지정해야합니다. 이들을 지정하는 키워드가 clr-namespaceassembly입니다. CLR 네임스페이스는 때로 여기서처럼 어셈블리와 동일하게 취급되지만 그럴 필요는 없습니다. 둘은 세미콜론으로 연결됩니다.

clr-namespace 다음에는 콜론이, assembly 다음에는 등호가 온다는 것에 주목하시기 바랍니다. 거기에는 이유가 있습니다: 네임 스페이스 선언의 형식은 일반 네임스페이스 선언에 있는 URI를 모방하기 위한 것이며, 거기서 콜론이 URI scheme name 다음에 옵니다.

외부 PCL 객체 참조시에도 같은 문법이 사용됩니다. 이 경우의 유일한 차이점은 프로젝트가 외부 PCL에 대한 참조가 필요하다는 것입니다(10장에서 보시게 될 겁니다).

local 접두사는 동일한 어셈블리에서는 공통적이며, 따라서 이 경우 어셈블리 부분은 필요하지 않습니다.

xmlns:local="clr-namespace:ColorViewList"
cs

PCL의 XAML 파일의 경우, 원한다면 assembly 파트를 포함할 수 있지만 필수는 아닙니다. 그러나 SAP의 XAML 파일의 경우, 연결된 어셈블리가 없기 때문에 assembly 파트가 들어가서는 안 됩니다. SAP의 코드는 개별 플랫폼 어셈블리의 일부이며 모두 다른 이름을 사용합니다.

다음은 ColorViewListPage 클래스의 XAML입니다. code-behind 파일에는 InitializeComponent 호출 외에는 아무 것도 들어 있지 않습니다.

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:ColorViewList"
             x:Class="ColorViewList.MainPage">
    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness"
                    iOS="0,20,0,0" />
    </ContentPage.Padding>
    <ScrollView>
        <StackLayout Padding="6,0">
            <local:ColorView ColorName="Aqua" />
            <local:ColorView ColorName="Black" />
            <local:ColorView ColorName="Blue" />
            <local:ColorView ColorName="Fuchsia" />
            <local:ColorView ColorName="Gray" />
            <local:ColorView ColorName="Green" />
            <local:ColorView ColorName="Lime" />
            <local:ColorView ColorName="Maroon" />
            <local:ColorView ColorName="Navy" />
            <local:ColorView ColorName="Olive" />
            <local:ColorView ColorName="Purple" />
            <local:ColorView ColorName="Pink" />
            <local:ColorView ColorName="Red" />
            <local:ColorView ColorName="Silver" />
            <local:ColorView ColorName="Teal" />
            <local:ColorView ColorName="White" />
            <local:ColorView ColorName="Yellow" />
        </StackLayout>
    </ScrollView>
</ContentPage>
cs

이전의 예제에서 제안한 것처럼 마크업이 이상해 보이지 않으면서도, XAML 기반 클래스에서 비주얼을 캡슐화하는 방법을 보여줍니다. StackLayoutScrollView의 자식이므로 스크롤 가능합니다.


그러나 이 ColorViewList 프로젝트를 최고의 방법이라고 부르기엔 조금 모자란 측면이 있습니다. 그것은 ColorViewColorName 프로퍼티 정의입니다. 이것은 BindableProperty 객체로 구현되어야 합니다. 바인딩 가능한 객체와 바인딩 가능한 속성을 추구하는 것이 최우선 과제이며, 11 장 "The bindable infrastructure"에서 다룰 것입니다.

Events and handlers


Button을 클릭하면 Clicked 이벤트가 발생합니다. 버튼은 XAML에서 인스턴스화할 수 있지만  Clicked 이벤트 핸들러 자체는 code-behind 파일 내에 있어야 합니다. Button은 주로 이벤트를 생성하기 위해 존재하는 많은 view들 중 하나이므로, 이벤트를 처리하는 프로세스는 XAML 및 코드 파일을 조정에 중요한 역할을 합니다.

이벤트 핸들러를 XAML의 이벤트에 연결하는 것은 프로퍼티를 설정하는 것 만큼이나 간단합니다. 사실상 프로퍼티를 세팅하는 것과 아예 시각적으로 구분이 불가능 합니다. XamlKeypad 프로젝트는 6 장의 PersistentKeypad 프로젝트의 XAML 버전입니다. 이 프로젝트는 XAML에서 이벤트 핸들러를 세팅하고 code-behind 파일에서 이벤트를 다루는 방법을 보여줍니다. 또한 프로그램이 종료되었을 때 키패드에 입력된 값을 저장하는 로직도 포함하고 있습니다.

SimplestKeypadPage 또는 PersistentKeypadPage 클래스의 생성자 코드를 살펴보면, 키패드의 숫자 부분을 구성하는 버튼을 만들기 위해 루프들이 여러개 사용된 것을 알 수 있습니다. 물론 루프를 돌리는 것은 XAML에서 할 수 없는 종류의 일이지만, 코드와 비교했을 때 XamlKeypadPage의 마크업이 얼마나 깔끔하게 이 일을 처리했는지 보시기 바랍니다.
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XamlKeypad"
             x:Class="XamlKeypad.MainPage">
    <StackLayout VerticalOptions="Center"
                 HorizontalOptions="Center">
        
        <Label x:Name="displayLabel"
               Font="Large"
               VerticalOptions="Center"
               HorizontalTextAlignment="End" />
        <Button x:Name="backspaceButton"
                Text="&#x21E6;"
                Font="Large"
                IsEnabled="False"
                Clicked="OnBackspaceButtonClicked" />
        <StackLayout Orientation="Horizontal">
            <Button Text="7" StyleId="7" Font="Large"
                    Clicked="OnDigitalButtonClicked" />
            <Button Text="8" StyleId="8" Font="Large"
                    Clicked="OnDigitalButtonClicked" />
            <Button Text="9" StyleId="9" Font="Large"
                    Clicked="OnDigitalButtonClicked" />
        </StackLayout>
        <StackLayout Orientation="Horizontal">
            <Button Text="4" StyleId="4" Font="Large"
                    Clicked="OnDigitalButtonClicked" />
            <Button Text="5" StyleId="5" Font="Large"
                    Clicked="OnDigitalButtonClicked" />
            <Button Text="6" StyleId="6" Font="Large"
                    Clicked="OnDigitalButtonClicked" />
        </StackLayout>
        
        <StackLayout Orientation="Horizontal">
            <Button Text="1" StyleId="1" Font="Large"
                    Clicked="OnDigitalButtonClicked" />
            <Button Text="2" StyleId="2" Font="Large"
                    Clicked="OnDigitalButtonClicked" />
            <Button Text="3" StyleId="3" Font="Large"
                    Clicked="OnDigitalButtonClicked" />
        </StackLayout>
        <Button Text="0" StyleId="0" Font="Large" 
                Clicked="OnDigitButtonClicked" />
    </StackLayout>
</ContentPage>
cs

이 파일은 각각의 숫자 버튼에 세 개의 프로퍼티가 세 줄로 구성되었던 때보다 훨씬 짧고, 함께 묶어졌을 때 마크업의 균일성과 가독성이 훨씬 좋아집니다.

여기서 질문을 드립니다. SimplestKeypad의 코드와 XamlKeypad의 XAML 중, 어떤 것을 유지 보수 하는 편이 좋겠습니까?

스크린샷을 보여 드립니다. 키는 전화기 순서가 아닌 계산기 순서입니다.


백스페이스 버튼의 Clicked 이벤트는 OnBackspaceButtonClicked 핸들러로 설정되고 숫자 버튼들은 OnDigitButtonClicked 핸들러를 공유합니다. StyleId 프로퍼티는 동일한 이벤트 핸들러를 공유하는 뷰를 구별하기 위해 종종 사용됩니다. 다시 말해, 두 이벤트 핸들러는 code-behind 파일에서도 코드 전용 프로그램에서와 똑같이 구현 될 수 있습니다.
namespace XamlKeypad
{
    public partial class MainPage : ContentPage
    {
        App app = Application.Current as App;
        public MainPage()
        {
            InitializeComponent();
            displayLabel.Text = app.DisplayLabelText;
            backspaceButton.IsEnabled = displayLabel.Text != null &&
                displayLabel.Text.Length > 0;
        }
        void OnDigitalButtonClicked(object sender, EventArgs args)
        {
            Button button = (Button)sender;
            displayLabel.Text += (string)button.StyleId;
            backspaceButton.IsEnabled = true;
            app.DisplayLabelText = displayLabel.Text;
        }
        void OnBackspaceButtonClicked(object sender, EventArgs args)
        {
            string text = displayLabel.Text;
            displayLabel.Text = text.Substring(0, text.Length - 1);
            backspaceButton.IsEnabled = displayLabel.Text.Length > 0;
            app.DisplayLabelText = displayLabel.Text;
        }
    }
}
cs

InitializeComponent에 의해 호출 된 LoadFromXaml 메서드가 XAML 파일에서 인스턴스화 된 개체에 이벤트 처리기를 연결합니다.

XamlKeypad 프로젝트의 App 클래스에는 PersistentKeypad의 것과 같은 코드가 추가로 포함되어 있어 프로그램 종료시 키패드 텍스트를 저장할 수 있습니다.

namespace XamlKeypad
{
    public partial class App : Application
    {
        const string displayLabelText = "displayLabelText";
        public App()
        {
            
            if (Properties.ContainsKey(displayLabelText))
            {
                DisplayLabelText = (string)Properties[displayLabelText];
            }
            MainPage = new XamlKeypad.MainPage();
        }
        public string DisplayLabelText { get; set; }
        protected override void OnStart()
        {
            // Handle when your app starts
        }
        protected override void OnSleep()
        {
            // Handle when your app sleeps
            Properties[displayLabelText] = DisplayLabelText;
        }
        protected override void OnResume()
        {
            // Handle when your app resumes
        }
    }
}
cs



Tap gestures


Xamarin.Forms Button은 손가락 탭에 반응합니다. 뿐만 아니라 View로부터 파생된 모든 클래스(Label, BoxView, Frame 등)는 사실 탭에 반응을 합니다. 이러한 탭 이벤트는 View 클래스에 내장된 것은 아니지만 GestureRecognizers라는 프로퍼티가 정의되어 있습니다. 이 GestureRecognizers 컬렉션에 개체를 추가하여 탭을 사용할 수 있습니다.  GestureRecognizer에서 파생된 어떤 클래스의 인스턴스도 이 컬렉션에 추가될 수 있지만, 의심의 여지없이 가장 유용한 템플릿은 TapGestureRecognizer입니다.

BoxView에 코드로 TapGestureRecognizer를 추가하는 방법은 다음과 같습니다.

BoxView boxView = new BoxView 
{
    Color = Color.Blue 
}; 
TapGestureRecognizer tapGesture = new TapGestureRecognizer(); 
tapGesture.Tapped += OnBoxViewTapped; 
boxView.GestureRecognizers.Add(tapGesture);
cs

TapGestureRecognizerNumberOfTapsRequired 속성이 기본값 1로 정의되어 있습니다. 이중 탭을 구현하려면 2로 설정합니다.

Tapped 이벤트를 생성하려면 View 개체의 IsEnabled 프로퍼티를 true, IsVisible 프로퍼티도 true, InputTransparent 프로퍼티는 false로 설정해야합니다. 이것이 모두 디폴트 조건입니다.

Tapped 핸들러는 Button에 붙였던 Clicked 핸들러와 유사합니다.
void OnBoxViewTapped(object sender, EventArgs args) 
    … 
}
cs

아시다시피 이벤트 핸들러의 sender 인자는 보통 이벤트를 일으키는 객체이며, 이 경우는 TapGestureRecognizer 객체인데 아마도 별로 쓸모가 없을 것입니다. 그러나, Tapped 핸들러에 대한 sender 인수는 눌려진 view이며, 이 경우 BoxView입니다. 그것은 훨씬 더 유용합니다!

Button과 마찬가지로 TapGestureRecognizerCommand, CommandParameter 프로퍼티를 가지고 있어서 MVVM 디자인 패턴 구현시에 사용되는데 그것은 나중에 공부하게 될 것입니다.

TapGestureRecognizer에는 또한 TappedCallback, TappedCallbackParameter라는 프로퍼티와 TappedCallback 인수가 포함된 생성자가 정의되어 있는데, 모두 deprecated 되어 있으므로 더 이상 사용해서는 안됩니다.

XAML로는 GestureRecognizers 컬렉션을 property element로 표현하여 TapGestureRecognizer를 뷰에 붙일 수 있습니다.

<BoxView Color="Blue">
    <BoxView.GestureRecognizers
        <TapGestureRecognizer Tapped="OnBoxViewTapped" />
    </BoxView.GestureRecognizers
</BoxView>
cs

늘 그렇듯이 XAML이 해당하는 코드보다 좀 짧습니다.

최초의 독립형 컴퓨터 게임에서 영감을 얻은 프로그램을 한번 만들어 봅시다.

모방 게임이므로 MonkeyTap이라 부르도록 합니다. 빨,파,노,초 네 개의 BoxView element가 나열되어 있으며, 게임이 시작되면 BoxView 중 하나가 빛나고, 그 BoxView를 탭하는 게임입니다. 다음으로는 두번째가 빛나고, 여러분은 순서대로 눌러야 합니다. 그 다음으로는 세번째, 네번째가 이어지고 계속해서 순서대로 눌러주는 게임입니다(오리지널에는 사운드도 들어있지만 물론 여기에는 없습니다). 잔인한 게임입니다. 왜냐하면 이기는 방법이 없기 때문입니다. 이 게임은 계속해서 어려워질 뿐입니다.

MonkeyTap.MainPage.xaml은 네개의 BoxView element와 가운데 "Begin" Button을 인스턴스화 합니다.

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:MonkeyTap"
             x:Class="MonkeyTap.MainPage">
 
    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness"
                    iOS="0,20,0,0" />
    </ContentPage.Padding>
 
    <StackLayout>
        <BoxView x:Name="boxview0"
                 VerticalOptions="FillAndExpand">
            <BoxView.GestureRecognizers>
                <TapGestureRecognizer Tapped="OnBoxViewTapped" />
            </BoxView.GestureRecognizers>
        </BoxView>
 
        <BoxView x:Name="boxview1"
                 VerticalOptions="FillAndExpand">
            <BoxView.GestureRecognizers>
                <TapGestureRecognizer Tapped="OnBoxViewTapped" />
            </BoxView.GestureRecognizers>
        </BoxView>
 
        <Button x:Name="startGameButton"
                Text="Begin"
                FontSize="Large"
                HorizontalOptions="Center"
                Clicked="OnStartGameButtonClicked" />
 
        <BoxView x:Name="boxview2"
                 VerticalOptions="FillAndExpand">
            <BoxView.GestureRecognizers>
                <TapGestureRecognizer Tapped="OnBoxViewTapped" />
            </BoxView.GestureRecognizers>
        </BoxView>
 
        <BoxView x:Name="boxview3"
                 VerticalOptions="FillAndExpand">
            <BoxView.GestureRecognizers>
                <TapGestureRecognizer Tapped="OnBoxViewTapped" />
            </BoxView.GestureRecognizers>
        </BoxView>
    </StackLayout>
</ContentPage>
cs

네 개의 BoxViewTapGestureRecognizer가 붙었지만 아직  색깔은 연결되지 않았습니다. 그것은 code-behind 파일에서 다루게 될 것인데, 그 이유는 색이 변할 것이기 때문입니다. 색은 번쩍이는 효과와 함께 바뀌게 될 것입니다.

code-behind 파일은 몇개의 상수, 변수로 시작합니다(그 중 하나가 protected 되어 있는 것이 보이실 겁니다. 다음 챕터에서 여기로부터 파생된 클래스가 필요하고 필드에 접근하게 됩니다. 메소드 중 몇개도 역시 protected 선언되었습니다).

namespace MonkeyTap
{
    public partial class MainPage : ContentPage
    {
        const int sequenceTime = 750// in msec
        protected const int flashDuration = 250;
 
        const double offLuminosity = 0.4// somewhat dimmer
        const double onLuminosity = 0.75// much brighter
 
        BoxView[] boxViews;
        Color[] colors = { Color.Red, Color.Blue, Color.Yellow, Color.Green };
        List<int> sequence = new List<int>();
        int sequenceIndex;
        bool awaitingTaps;
        bool gameEnded;
 
        Random random = new Random();
 
        public MainPage()
        {
            InitializeComponent();
            boxViews = new BoxView[] { boxview0, boxview1, boxview2, boxview3 };
            InitializeBoxViewColors();
        }
 
        void InitializeBoxViewColors()
        {
            for (int index = 0; index < 4; index++)
                boxViews[index].Color = colors[index].WithLuminosity(offLuminosity);
        }
 
        ...
 
    }
}
cs

생성자에서 모든 BoxView element를 배열에 넣어서 0,1,2,3의 값을 가지는 단순한 인덱스가 참조하도록 합니다. InitializeBoxViewColors 메소드는 모든 BoxView 요소를 약간 희미한 무광 상태로 설정합니다.

이제 프로그램은 유저가 Begin 버튼을 누르길 기다립니다. 같은 Button이 리플레이의 역할도 하므로 색상을 반복적으로 초기화하게 됩니다. Button 핸들러는 sequence 리스트를 지우고 StartSequence를 호출하여, 깜박이는 BoxView element의 순서를 정할 준비를 합니다.

namespace MonkeyTap
{
    public partial class MainPage : ContentPage
    {
        ...
 
        protected void OnStartGameButtonClicked(object sender, EventArgs args)
        {
            gameEnded = false;
            startGameButton.IsVisible = false;
            InitializeBoxViewColors();
            sequence.Clear();
            StartSequence();
        }
 
        void StartSequence()
        {
            sequence.Add(random.Next(4));
            sequenceIndex = 0;
            Device.StartTimer(TimeSpan.FromMilliseconds(sequenceTime), OnTimerTick);
        }
 
        ...
 
    }
}
cs


StartSequence는 시퀀스 목록에 새로운 임의의 정수를 추가하고 sequenceIndex를 0으로 초기화 한 다음 타이머를 시작합니다.

일반적인 경우 타이머 틱 핸들러는 시퀀스 목록의 각 인덱스에 대해 호출되며 FlashBoxView를 호출하여 BoxView를 깜박이게 합니다. 타이머 핸들러는 시퀀스가 ​​끝날 때 false를 반환하고 유저가 순서를 따라할 시간임을 awaitingTaps로 설정하여 나타냅니다.

namespace MonkeyTap
{
    public partial class MainPage : ContentPage
    {
        ...
 
        bool OnTimerTick()
        {
            if (gameEnded)
                return false;
 
            FlashBoxView(sequence[sequenceIndex]);
            sequenceIndex++;
            awaitingTaps = sequenceIndex == sequence.Count;
            sequenceIndex = awaitingTaps ? 0 : sequenceIndex;
            return !awaitingTaps;
        }
 
        protected virtual void FlashBoxView(int index)
        {
            boxViews[index].Color = colors[index].WithLuminosity(onLuminosity);
            Device.StartTimer(TimeSpan.FromMilliseconds(flashDuration), () =>
            {
                if (gameEnded)
                    return false;
 
                boxViews[index].Color = colors[index].WithLuminosity(offLuminosity);
                return false;
            });
        }
 
        ...
 
    }
}
cs


번쩍임은 1/4초 간격입니다. FlashBoxView 메서드는 밝은 색상의 광도를 먼저 설정하고 타이머 콜백 메서드(람다 함수로 표시됨)가 false를 반환한 후 색상의 광도를 복원하고 타이머를 종료하는 "원샷"타이머를 만듭니다.

BoxView element에 대한 Tapped 핸들러는 게임이 이미 끝난 경우 탭을 무시하고, 사용자가 프로그램 시퀀스를 기다리지 않고 조기에 탭하면 게임을 종료합니다. 그렇지 않으면 그냥 탭 된 BoxView를 시퀀스 상의 다음 BoxView와 비교하고, 올바르다면 BoxView를 깜박이고 그렇지 않으면 게임을 종료합니다.

namespace MonkeyTap
{
    public partial class MainPage : ContentPage
    {
        ...
 
        protected void OnBoxViewTapped(object sender, EventArgs args)
        {
            if (gameEnded)
                return;
 
            if (!awaitingTaps)
            {
                EndGame();
                return;
            }
 
            BoxView tappedBoxView = (BoxView)sender;
            int index = Array.IndexOf(boxViews, tappedBoxView);
 
            if (index != sequence[sequenceIndex])
            {
                EndGame();
                return;
            }
 
            FlashBoxView(index);
 
            sequenceIndex++;
            awaitingTaps = sequenceIndex < sequence.Count;
 
            if (!awaitingTaps)
                StartSequence();
        }
 
        protected virtual void EndGame()
        {
            gameEnded = true;
 
            for (int index = 0; index < 4; index++)
                boxViews[index].Color = Color.Gray;
 
            startGameButton.Text = "Try again?";
            startGameButton.IsVisible = true;
        }
    }
}
cs

유저가 시퀀스 전체를 "성공적으로 따라하고 나면, StartSequence가 다시 호출되고 시퀀스 목록에 새 인덱스가 추가되어 다시 시퀀스가 ​​재생되기 시작합니다.

아래는 Button이 눌려서 숨겨진 후 의 모습입니다.

압니다 알아요. 사운드 없이는 진짜 심심하다는 거.
다음 장에서 그걸 한번 개선해 봅시다.

댓글 없음:

댓글 쓰기