2016년 11월 26일 토요일

Tip - 조건부 컴파일

C++를 쓸 때 조건에 따라 컴파일이 달라지게 할 수 있었습니다.


#define DEBUG

..................
#ifdef DEBUG
      printf("value : %d\n", value);
#endif


이런 식으로 말이죠. 나중에 맨 앞의 #define DEBUG만 없애주면 쓸데없는 출력이 안나오죠.

C#에서도 그런 기능이 있습니다. 사용법은 비슷합니다.

#define DEBUG
using System;

......
#if DEBUG
     Console.WriteLint("value : {0}", value);
#endif

다만 C#에서는 #define 문은 항상 소스 맨 위 -  using문보다도 앞에 나와야 합니다.


그런데 C#에서는 include기능이 없기에, 저 디파인 문은 항상 파일 하나에만 적용되더군요. 다른 파일에서도 똑같은 define을 하지 않으면 오류가 생길 수 있습니다.

만약 비주얼 스튜디오라면 다음과 같이 할 수 있습니다.


각 프로젝트 밑의 Property를 선택하면 다음과 같은 창이 나옵니다.



여기서 'Build'탭의 "Conditional compilation symbol'란에 필요한 인수를 입력하면 매 파일마다 저 인수가 #define으로 선언된 것으로 간주합니다.




2016년 11월 21일 월요일

Tip - 채워진 폴리곤 그리기

속이 채워진 폴리곤을 그리는 방법입니다.


    public class ViewPort : View
    {
        private readonly PointF[] _points = new[]
                                                {
                                            new PointF(100, 100),
                                            new PointF(200, 200),
                                            new PointF(200, 500),
                                            new PointF(600, 600),
                                            new PointF(400, 200),
                                            new PointF(100, 100)
                                        };
        public ViewPort(Context context) : base(context)
        {
        }

        protected override void OnDraw(Canvas canvas)
        {
            var path = new Path();

            // path 객체에 폴리곤 그리기
            path.MoveTo(_points[0].X, _points[0].Y);
            for (var i = 1; i < _points.Length; i++)
            {
                // Draw a line from the previous point in the path to the new point.
                path.LineTo(_points[i].X, _points[i].Y);
            }

            // path 객체 캔버스에 그리기
            var brush = new Paint
            {
                Color = Color.Blue
            };
            brush.SetStyle(Paint.Style.Fill);   // 속 채움
            canvas.DrawPath(path, brush);

            var border = new Paint
            {
                Color = Color.Green
            };
            border.SetStyle(Paint.Style.Stroke);    // 테두리
            canvas.DrawPath(path, border);
        }
    }

Paint.Style.FillAndStroke란 스타일도 있긴 한데, 테두리가 있고 속이 채워진 다각형이 그려지지 않더군요. 테두리와 속을 같은 색으로 그리는지는 몰라도...
아무튼 테두리와 안쪽색을 구분하기 위해서는 위와같이 canvas.DrawPath()를 두번 그려야 할 것 같습니다.

2016년 11월 18일 금요일

Tip - 화면 크기 구하기

자마린에서 스마트폰 화면 크기를 구하기 위해서는 Resources.DisplayMetrics를 사용하는 방법이 있습니다.

        protected override void OnCreate(Bundle bundle)
        {
            base.OnCreate(bundle);

            // Set our view from the "main" layout resource
            SetContentView(new ScreenView(this));

            var metrics = Resources.DisplayMetrics;
            width = metrics.WidthPixels;
            height = metrics.HeightPixels;
        }

그런데 이런 방식으로 크기를 구해 대각선을 그어본 모습입니다.


보시다시피 원래 스크린 영역을 벗어나는 모습이군요. 아마도 위의 타이틀과 밑의 버튼들까지 포함하는 부분의 크기를 가져오는 듯 합니다.

그렇다면 순수하게 그림을 그릴 수 있는 부분만의 넓이는 어떻게 구할수 있을까요?

OnDraw로 들어오는 canvas에도 크기정보가 있습니다.

        protected override void OnDraw(Canvas canvas)
        {
            int w = canvas.Width;
            int h = canvas.Height;

            MyClass.Inst.Initialize(new DeviceConnect(), 0, 0, w, h);


            Paint pen = new Android.Graphics.Paint();
            pen.Color = Android.Graphics.Color.Yellow;

            canvas.DrawLine(0, 0, w, h, pen);
            canvas.DrawLine(0, h, w, 0, pen);
        }

이렇게 하면 다음과 같이 그려집니다.


다만 OnDraw에서 초기화를 하는 것이 합리적인지에 대한 의문점은 있지만, 아무튼 이런 방식이 있습니다.

혹시 자마린 폼이 아니라 그림이나 게임을 만든다면 이런 것도 필요하지 않을까 생각되는군요.

2016년 11월 16일 수요일

Project A - Millennium Falcon

이번에는 비트맵과 메시지를 이용한 예제를 만들어 보겠습니다. 화면에서 밀레니엄 팔콘 움직이기.


1. 메시지 처리
이 팔콘이 부드럽게 움직이기 위해서는 터치했을 때 순간이동하는 것이 아니라 천천히 목적지까지 가야 합니다. 그러므로 타이머 메시지를 만들어서 터치하지 않을때도 코드가 실행되도록 해야 합니다.

이런 메시지를 위한 시스템이 Handler입니다.


namespace MillenniumFalcon.Droid
{
    [Activity (Label = "MillenniumFalcon.Droid", MainLauncher = true, Icon = "@drawable/icon")]
    public class MainActivity : Activity
    {
        private Handler timerHandler;

        protected override void OnCreate (Bundle bundle)
        {
            base.OnCreate (bundle);

            // Set our view from the "main" layout resource

            var view = new SpaceView(this);
            SetContentView(view);

            timerHandler = new TimerMessageHandler(view.TimerFunction);
            timerHandler.SendEmptyMessage(0);                            // ①
        }
    }

    internal class SpaceView : Android.Views.View
    {
        public SpaceView(Context context) : base(context)
        {
        }

        internal void TimerFunction(Message msg)
        {
        }
    }

    internal class TimerMessageHandler : Android.OS.Handler
    {
        Action<Message> handle_message;

        public TimerMessageHandler(Action<Message> handler)
        {
            this.handle_message = handler;
        }

        public override void HandleMessage(Message msg)
        {
            SendEmptyMessageDelayed(0, 100);                            // ②
            handle_message(msg);
        }
    }
}

TimerMessageHandler가 메시지를 처리하는 핸들러입니다. 그리고 ①로 표시된 라인이 이 핸들러로 메시지를 보내는 부분입니다.
여기서는 오로지 타이머에 관련된 메시지만 처리하기에 SendEmptyMessage() 함수를 사용했고, 핸들러에서도 메시지를 확인 않고 처리했지만, SendMessage() 함수를 사용해도 됩니다.
SendEmptyMessage()나 SendMessage()에 의해 발생된 메시지에 의해 HandleMessage()함수가 실행됩니다. 여기서는 ②에서처럼 SendEmptyMessageDelayed() 함수에 의해 100밀리초, 즉 0.1초 이후 메시지를 예약합니다. 그리고 delegate로 지정된 함수 - SpaceView.TimeFunction()을 실행하죠. 즉 SpaceView.TimeFunction()함수가 0.1초 간격으로 실행되는 것입니다.


2. BMP 등록
'BMP등록'이라고 했지만, 반드시 bmp파일만 등록할 수 있는 것은 아닙니다. jpg나 png 등 대부분의 그림파일을 등록할 수 있습니다. 내부에서 bmp로 바뀌어 처리됩니다.

MillenniumFalcon.Droid 프로젝트를 보면 다음과 같이 Drawable 폴더가 5개나 있습니다.


하드웨어의 해상도에 따라 자동으로 폴더가 선택되어 BMP파일을 불러옵니다. 만약 필요한 해상도에 파일이 없으면 그와 비슷한 다른 해상도의 파일을 가져옵니다.
여기서는 고해상도인 drawable-xxxhdpi에 넣었지만, 만약 drawable-hdpi에 넣는다면 엄청나게 큰 밀레니엄팔콘이 나옵니다.
만약 그림파일이 하나만 있다면, 해상도에 맞춰 프로그램 내부에서 크기를 조정하면서 일그러지는 현상이 일어나므로, 미리 조정된 크기의 그림을 넣을수 있도록 여러가지로 구분된 것 같습니다.
이렇게 등록된 그림은 "Resource.Drawable.파일이름"으로 등록됩니다. 확장자는 제외되므로, 이를테면 "a.jpg"와 "a.png"를 동시에 등록할 수는 없습니다.


3. BMP 로드
이렇게 drawable에 등록된 그림을 읽어들여야겠죠.
Xamarin에서 그림을 저장하는 객체는 Bitmap입니다. 그러나 그림을 출력하기 위해서는 정보가 더 필요하죠. 특히나 지금처럼 움직이는 우주선을 묘사하기 위해서는 말입니다.

    internal class SpaceView : Android.Views.View
    {
        private class MillenniumFalcon
        {
            private const float speed = 10;

            private Bitmap falcon;

            // 그림 중앙에 맞추기 위해
            private float offX;
            private float offY;

            // 현재위치
            private float curX;
            private float curY;

            // 이동할 위치
            private float goalX;
            private float goalY;

            internal MillenniumFalcon(Android.Views.View view)
            {
                falcon = Android.Graphics.BitmapFactory.DecodeResource(view.Resources,
                                                  Resource.Drawable.MillenniumFalcon); // ①

                // 초기위치 설정
                goalX = curX = offX = falcon.Width / 2;
                goalY = curY = offY = falcon.Height / 2;
            }

            internal void Draw(Canvas canvas)
            {
                canvas.DrawBitmap(falcon, curX - offX, curY - offY, null);   // ②
            }

            internal void MovePoint(float x, float y)
            {
                goalX = x;
                goalY = y;
            }

            internal void TimerFunction()
            {
                float dX = goalX - curX;
                float dY = goalY - curY;
                float distanceToGoal = (float)Math.Sqrt(dX * dX + dY * dY);
                if (distanceToGoal < speed)
                {
                    curX = goalX;
                    curY = goalY;
                }
                else
                {
                    float dx = speed * dX / distanceToGoal;
                    float dy = speed * dY / distanceToGoal;

                    curX += dx;
                    curY += dy;
                }
            }
        }

        MillenniumFalcon falcon;

        public SpaceView(Context context) : base(context)
        {
            falcon = new MillenniumFalcon(this);
        }

        protected override void OnDraw(Canvas canvas)
        {
            falcon.Draw(canvas);
        }

        internal void TimerFunction(Message msg)
        {
            falcon.TimerFunction();           // ④
            Invalidate();
        }

        public override bool OnTouchEvent(MotionEvent e)
        {
            falcon.MovePoint(e.GetX(), e.GetY());     // ③
            return true;
        }
    }

①번이 리소스에서 그림파일을 읽어 비트맵 이미지로 저장하는 부분입니다. 그리고 ②번부분이 캔버스(화면)에 그리는 부분이며, 터치이벤트가 있을 때마다 goal을 새로 설정하고(③), 0.1초마다 goal을 향해 위치를 바꿉니다(④).

실행해보면 알겠지만, 팔콘이 이동은 하는데 방향이 바뀌질 않는군요. 항상 위쪽을 향한채 옆으로 뒤로 움직입니다. 이동방향으로 회전을 할 수 없을까요?

프로그램 내부에서 비트맵을 수정하는 것이 Matrix입니다. 여러가지 행렬변환으로 회전, 축소확대 등 변형된 BMP파일을 새로 만드는 것입니다.

       Bitmap original = Android.Graphics.BitmapFactory.DecodeResource(view.Resources,
                                   Resource.Drawable.MillenniumFalcon);
       Matrix mat = new Matrix();
       mat.SetScale(2, 2);  // x, y방향으로 2배 확대
       mat.PostRotate(30);  // 시계방향으로 30도 회전
       Bitmap newBitmap = Android.Graphics.Bitmap.CreateBitmap(origin, 0, 0,
                                               origin.Width, origin.Height, mat, true);

위 코드는 original 비트맵을 2배 확대한 후 시계방향으로 30도 회전한 새로운 비트맵을 만들어냅니다. 그러므로 여기서도 falcon은 원본으로 남겨두고 falconDraw라는 새로운 비트맵을 만드는 방식으로 수정했습니다.

        private class MillenniumFalcon
        {
            private const float speed = 10;

            private Bitmap falcon;
            private Bitmap falconDraw;

            // 그림 중앙에 맞추기 위해
            private float offX;
            private float offY;

            // 현재위치
            private float curX;
            private float curY;

            // 이동할 위치
            private float goalX;
            private float goalY;

            internal MillenniumFalcon(Android.Views.View view)
            {
                falcon = Android.Graphics.BitmapFactory.DecodeResource(view.Resources,
                                                     Resource.Drawable.MillenniumFalcon);
                Matrix mat = new Matrix();
                mat.SetScale(0.5F, 0.5F);  // 절반으로 축소한 팔콘 만듦
                falconDraw = Bitmap.CreateBitmap(falcon, 0, 0, falcon.Width, falcon.Height,
                                                  mat, true);

                // 초기위치 설정
                goalX = curX = offX = falcon.Width / 2;
                goalY = curY = offY = falcon.Height / 2;
            }

            internal void Draw(Canvas canvas)
            {
                canvas.DrawBitmap(falconDraw, curX - offX, curY - offY, null);
            }

            internal void MovePoint(float x, float y)
            {
                goalX = x;
                goalY = y;

                // 회전
                float dX = goalX - curX;
                float dY = goalY - curY;
                float rad = (float)Math.Atan2(dX, -dY);
                float deg = rad / 0.01745329F;
                Matrix mat = new Matrix();
                mat.SetScale(0.5F, 0.5F);  // 반으로 축소
                mat.PostRotate(deg);       // 회전
                falconDraw = Bitmap.CreateBitmap(falcon, 0, 0, falcon.Width, falcon.Height,
                                                 mat, true);
                offX = falconDraw.Width / 2;
                offY = falconDraw.Height / 2;
            }

            internal void TimerFunction()
            {
                float dX = goalX - curX;
                float dY = goalY - curY;
                float distanceToGoal = (float)Math.Sqrt(dX * dX + dY * dY);
                if (distanceToGoal < speed)
                {
                    curX = goalX;
                    curY = goalY;
                }
                else
                {
                    float dx = speed * dX / distanceToGoal;
                    float dy = speed * dY / distanceToGoal;

                    curX += dx;
                    curY += dy;
                }
            }
        }

대충 되긴 했습니다만, 이것이 완벽하지 않습니다. 터치 관련된 이벤트가 들어올 때마다 falconDraw비트맵을 새로 만들기 때문에 메모리낭비가 심하죠.


이런 Out Of Memory 에러가 나기 쉽습니다.

개선방향으로는 이를테면 5도 간격으로 미리 만들어놓고 각도에 따라 재사용하는 방법이 있습니다만, 그것은 이후의 숙제로 남겨놓고...^^;

Tip - 화면 방향 고정

보통 스마트폰은 방향에 따라 가로형이나 세로형으로 바뀝니다.
그러나 가끔 가로형 또는 세로형중 한가지로 고정시켜서 프로그램을 해야 할 때가 있죠.
안드로이드 프로그램일 경우 다음과 같이 Attribute를 수정하면 됩니다.

using Android.Graphics;
using Android.Content.PM;
using System.Collections.Generic;

namespace TouchTest.Droid
{
    [Activity (Label = "TouchTest.Droid", MainLauncher = true, Icon = "@drawable/icon",
               ScreenOrientation = ScreenOrientation.SensorLandscape)]
    public class MainActivity : Activity
    {

만약 이 부분을 ScreenOrientation.Landscape로 한다면, 스마트폰을 뒤집었을때 다음과 같이 그려집니다.





그러나 ScreenOrientation.SensorLandscape로 한다면 다음과 같이 나오죠





2016년 11월 15일 화요일

Project A - Touch

프로젝트라고 하기에는 민망하지만, 간단하게 화면을 터치하면 그 좌표를 출력하는 앱을 만들어 봅시다.

1. 새로운 뷰 만들기
먼저 기본 뷰를 사용하지 않을 것이므로 새로운 뷰를 만들어야 합니다.

    public class TouchView : Android.Views.View
    {
        public TouchView(Context context) : base(context)
        {
        }
    }

그리고 이 뷰를 앱에 세팅시켜야겠죠.

        protected override void OnCreate (Bundle bundle)
        {
            base.OnCreate (bundle);

            // Set our view from the "main" layout resource
            SetContentView (Resource.Layout.Main);


이 부분을

        protected override void OnCreate (Bundle bundle)
        {
            base.OnCreate (bundle);

            SetContentView(new TouchView(this));


로 바꿉니다. 물론 이렇게 되면 기본적으로 등록되어 있던 오브젝트(버튼 등)들도 동작하지 않으므로 해당 코드를 지워줘야 합니다.

이대로 실행해 보면 다음과 같이 아무것도 없는 화면만이 나타납니다.




2. 터치이벤트 처리
터치이벤트를 처리하는 메소드는 이미 Android.Views.View에 포함되어 있습니다. 우리가 할 일은 이 메소드를 재정의하는 일입니다.

    public class TouchView : Android.Views.View
    {
        public TouchView(Context context) : base(context)
        {
        }

        public override bool OnTouchEvent(MotionEvent e)
        {
            return true;
        }
    }

여기에 브레이크포인트를 걸고 확인해보면 마우스를 클릭할 때마다 이곳으로 들어옴을 알 수 있습니다.

클릭정보는 MotionEvent e를 통해서 들어옵니다. 여러가지 정보가 있지만 중요한 것은 다음과 같습니다.

① e.Action
어떤 이벤트가 일어났는지를 구분하는 enum입니다. 여러가지가 있지만, 지금 당장 필요한 것은 다음과 같은 세개입니다.

- Android.Views.MotionEventActions.Down : 화면을 터치할때 일어나는 이벤트
- Android.Views.MotionEventActions.Up : 화면에서 손을 뗄때 일어나는 이벤트
- Android.Views.MotionEventActions.Move : 화면에서 움직일때 일어나는 이벤트

② e.GetX(), e.GetY()
이벤트가 어디서 일어났는지 알려주는 부분입니다.


3. 화면출력
일단 이벤트가 일어났는데, 이벤트가 일어났는지 안일어났는지 밖에서는 알 수가 없겠죠. 그래서 간단하게 이벤트를 출력하는 부분을 만들어야 합니다.

    public class TouchView : Android.Views.View
    {
        public TouchView(Context context) : base(context)
        {
        }

        protected override void OnDraw(Canvas canvas)
        {
             base.OnDraw(canvas);
        }

        public override bool OnTouchEvent(MotionEvent e)
        {
            return true;
        }
    }

OnDraw()에서는 canvas에 여러가지 작업을 하면 그것이 화면에 나타나게 됩니다.
이 프로젝트에서 하는 일은 화면에 글을 쓰는 일입니다. 그 작업은 다음과 같은 함수로 할 수 있습니다.

        public virtual void DrawText(string text, float x, float y, Paint paint);

이 외에도 몇가지 함수가 더 있는데 우선 이것만 보기로 하죠.

일어난 이벤트 종류와 위치를 화면에 출력하는 TouchView 클래스입니다.



    public class TouchView : Android.Views.View
    {
        private string eventAction;
        private float getX, getY;
        private Paint pen;

        public TouchView(Context context) : base(context)
        {
            pen = new Paint();
            pen.Color = Color.Yellow;
            pen.TextSize = 30;
            eventAction = "";
        }

        protected override void OnDraw(Canvas canvas)
        {
            canvas.DrawText("Touch Event " + eventAction, 10, 100, pen);
            canvas.DrawText("Get " + getX + ',' + getY, 10, 150, pen);
        }

        public override bool OnTouchEvent(MotionEvent e)
        {
            eventAction = e.Action.ToString();
            getX = e.GetX();
            getY = e.GetY();

            Invalidate();
            return true;
        }
    }



4. 핀치(Pinch)
핀치(Pinch)란, 두 손가락을 사용해서 화면을 축소확대하는 기능을 말합니다. 그러기 위해서는 좌표를 두개 받을 수 있어야 합니다.

MotionEvent e의 프로퍼티 PointerCount가, 터치점이 몇개인지 받아오는 프로퍼티입니다. 두 손가락으로 터치하면 2, 세 손가락으로 터치하면 3, ..., 양 손 손가락을 다 동원하면 10도 될 수 있습니다.
그리고 각 손가락의 이벤트 위치는 e.GetX(int p), e.GetY(int p) 함수로 얻어올 수 있습니다.
그러므로 모든 터치입력을 받기 위해서는 다음과 같이 리스트를 사용해야 합니다.

    public class TouchView : Android.Views.View
    {
        private List<string> eventAction = new List<string>();
        private List<float> getX = new List<float>();
        private List<float> getY = new List<float>();
        private Paint pen;

        public TouchView(Context context) : base(context)
        {
            pen = new Paint();
            pen.Color = Color.Yellow;
            pen.TextSize = 30;
            for(int k = 0; k < 10; ++k)
                eventAction.Add("");
        }

        protected override void OnDraw(Canvas canvas)
        {
            int row = 100;
            foreach (var e in eventAction)
            {
                canvas.DrawText("Touch Event [" + e + "] ", 10, row, pen);
                row += 50;
            }

            row += 50;

            for (int k = 0; k < getX.Count; ++k)
            {
                canvas.DrawText("Get " + getX[k] + ',' + getY[k], 10, row, pen);
                row += 50;
            }
        }

        public override bool OnTouchEvent(MotionEvent e)
        {
            eventAction.RemoveAt(0);
            eventAction.Add(e.Action.ToString() + ' ' + e.PointerCount.ToString());

            getX.Clear();
            getY.Clear();

            for (int k = 0; k < e.PointerCount; ++k)
            {
                getX.Add(e.GetX(k));
                getY.Add(e.GetY(k));
            }

            Invalidate();
            return true;
        }
    }

이 앱을 폰으로 옮겨 실행한 결과입니다.


조금 낯선 이벤트가 있네요. Pointer1Up, Pointer2Up 같은 것 말입니다.
폰에서는 손가락을 고정시켜도 Move이벤트가 계속 들어오므로 Move이벤트는 빼고, 차례로 6개의 손가락을 터치했다가 다시 하나씩 뗀 결과입니다.



보시다시피 차례로 Down(손가락 터치), Pointer2Down(두번째 손가락 터치), Pointer3Down(세번째 손가락 터치) 순으로 들어오는 것을 알 수 있습니다. 이후의 숫자들은 Enum이 정의되어 있지 않은 것이고 말입니다.
그리고 손가락을 뗄 때는 몇번째로 터치했던 손가락이 떨어졌는지가 Pointer?Up의 형태로 오는 모양네요.
뭐 스마트폰 쓰면서 3개의 터치도 필요한 적이 없으니 그 이상의 Enum을 정하지 않은 것이겠죠.

Tip - 타이틀바 제거

자마린으로 프로젝트를 만들면 다음과 같이 타이틀바가 생깁니다. 다른 사람들은 모르겠지만 제경우에는 이 타이틀바가 상당히 거슬리더군요.
그래서 이 타이틀바를 없앨 방법을 찾아봤습니다.



- 첫째
MainActivity의 Attribute를 수정하는 방법

namespace App5.Droid
{
    Activity (Label = "App5.Droid", MainLauncher = true, Icon = "@drawable/icon",
              Theme = "@android:style/Theme.NoTitleBar")]
    public class MainActivity : Activity
    {
        int count = 1;

        protected override void OnCreate (Bundle bundle)
        {
            base.OnCreate (bundle);


와 같이 테마를 추가하는 방법이 있습니다.
이것을 실행하면

와 같이 나오죠.

- 둘째
OnCreate에서 함수호출

namespace App5.Droid
{
    [Activity (Label = "App5.Droid", MainLauncher = true, Icon = "@drawable/icon")]
    public class MainActivity : Activity
    {
        int count = 1;

        protected override void OnCreate (Bundle bundle)
        {
             RequestWindowFeature(WindowFeatures.NoTitle);
             base.OnCreate (bundle);


이렇게 해도 실행시에 타이틀바가 사라집니다.

- 세째
AndroidManifest.xml 수정

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
 <uses-sdk android:minSdkVersion="15" />
 <application android:label="App5.Droid"
               android:theme="@android:style/Theme.NoTitleBar">
  </application>
</manifest>

차이가 있다면, 첫째와 세째 방법은 앱 시작부터 타이틀바가 사라지지만 둘째 방법은 앱 시작에는 타이틀바가 보이다가 사라진다는 점...

2016년 11월 14일 월요일

Solution - The layout could not be loaded


이런 에러가 나면서 레이아웃이 열리지 않는 경우가 있더군요.

no exports were found that match the constraint contract name

이와 같은 에러가 나면서 말입니다.

해결법은 간단합니다.

%AppData%\..\Local\Microsoft\VisualStudio\version(ex. 11.0, 12.0)\ComponentModelCache

이 폴더의 내용물을 지우니 그 이후로 잘 되더군요.

Project A - Hello, Xamarin

가장 간단하게, 화면에 Hello, Xamarin을 출력하는 앱입니다.

1. 프로젝트를 만듧시다.
다음과 같이 Templates → Visual C# → Cross-Platform → Blank App을 선택하면 안드로이드 및 iOS, 윈도우폰을 포함하는 멀티플랫폼 프로젝트를 만들 수 있습니다.


만약 안드로이드용 앱만 만들 생각이면 Templates → Visual C# → Andriod로 만들어도 됩니다.
멀티플랫폼으로 만들었다면 다음과 같이 HelloXamarin.Droid를 시작 프로젝트로 설정해야 프로젝트에서 실행 및 디버깅을 할 수 있습니다.


2. 다음에는 레이아웃을 수정해야 합니다.


버전에 따라 다르겠지만, 기본적으로 저런 버튼이 하나 만들어져 있습니다. 이 버튼은 필요없으니 과감히 지워버리면 됩니다. 다만 이 버튼과 관련된 소스도 지워야 합니다.


3. 그리고 글이 들어갈 텍스트박스를 만들어야겠죠.


툴박스에서 텍스트박스를 끌어 레이아웃으로 보내면 다음과 같이 텍스트가 나타납니다. 그리고 내용은 Property(속성)의 Text에 들어가게 됩니다.
여기서 직접 "Hello, Xamarin!"을 입력해도 되지만, 그것은 좋은 방법이 못됩니다. 그 이유는 뒤에서 나옵니다.

4. 문자열을 등록합시다.


문자열은 HelloXamarin.Droid → Resources → values → Strings.xml에서 등록할 수 있습니다. 기본으로 등록되어 있는 줄을 참조해서 두개의 문자열을 등록합니다.

다시 레이아웃으로 가서, 문자열의 Property → Text의 버튼을 누르면, 방금 등록했던 문자열의 리스트가 나타납니다. 만약 나오지 않는다면 String.xml을 저장하면 됩니다.



여기서 문자열을 선택하면 그 문자열이 텍스트박스에 들어가게 됩니다.


5. 위에 몰려있으니 화면 한가운데로 보내 봅시다.


텍스트박스의 Property → Main - Text Format → gravity를 center로 설정하면 위와 같이 문자열이 텍스트박스의 한가운데 위치합니다.

또한 바탕화면의 Property → Main → gravity를 center로 설정하면 다음과 같이 텍스트박스가 화면 한가운데 위치하게 됩니다.



6. 한글 출력
안드로이드에서는 다국어를 설정하기 위한 효율적인 방법을 제공하고 있습니다.
먼저 HelloXamarin.Droid → Resources → values를 복사해서 폴더를 하나 더 만듧니다.


이 복사된 폴더의 이름을 "values-ko"로 바꿔줍니다. 그리고 values-ko → Strings.xml을 수정해서 한국어로 바꿉니다.


하는 김에 일본어도 추가해 봅시다. 똑같은 방법으로 "values-ja"를 만든 후 values-ja → Strings.xml을 일본어로 수정하면 됩니다.


이렇게 하면, 앱이 실행될때 스마트폰의 언어설정을 검사해서 한국어면 values-ko → Strings.xml을, 일본어면 values-ja → Strings.xml을, 어느 쪽도 아니면 values → Strings.xml을 가져오게 됩니다. 그러므로 이것을 실행하면 다른 부분을 수정하지 않아도 다음과 같이 나옵니다.


만약 위와 같이 나오지 않으면 에뮬레이터의 언어설정이 한국어로 되어 있지 않은 것입니다.

7. Deploy
이 앱을 스마트폰에서 직접 실행시켜 보겠습니다.
먼저 Solution Configuration을 Debug가 아닌 Release로 설정합니다. 그리고 Build → Deploy Solution을 실행하면 ..../HelloXamarin.Droid/bin/Release 폴더에 두 개의 apk파일이 생성됩니다.


여기서 HelloXamarin.Droid-Signed.apk가 유효한 설치파일입니다. HelloXamarin.Droid.apk는 설치가 안되더군요.
일단 HelloXamarin.Droid-Signed.apk을 스마트폰으로 복사해서 설치하면 실행할 수 있습니다.


다음은 각각 언어설정을 바꿔가며 실행한 결과입니다.