Sunday, December 23, 2012

도대체 어느 API 레벨에 맞춰 개발해야하는 거야?




보통 범하기 쉬운 실수가 '2.1에서 젤리빈(Jelly Bean)까지, 어느 플랫폼에서든 돌아가는 앱을 만들어야지' 생각하면서 타겟을 떡하니 2.1로 잡는 것이다. 결론부터 말하자면 '넌 잘못 생각해도 너~~~~무 잘못 생각한겨'

자 지금부터 version을 지정하는 몇 개의 값들을 하나 하나 살펴보면서 어떻게 설정해서 작업을 해야할지를 생각해 보자.

Thursday, December 20, 2012

iOS와 Android에서 Cocos2D-X 사용하기

작성에 바탕이 된 것들
Mac OS X   Mountain Lion
Xcode 4.5.2
Cocos2d-2.0-x-2.0.4
Eclipse for Mobile Developer (juno)
iPhone4S, Galaxy Nesus(Jelly Bean)


Cocos2D는 그 성능뿐만 아니라 사용의 용이성으로 인기가 있는 프레임워크이지만 Objective-C로 작성이 되어있어서  iOS나 Mac에서 동작하는 게임을 만드는데 쓰입니다. 안드로이드에서도 역시나 동일하게 동작하는 앱을 Cocos2D API를 이용해서 아주 쉽게 만들수 있다면 얼마나 좋겠어요?

그래서, Cocos2D-X 게임 프레임워크가 있는 것이지요.  Cocos2D-X는 C++로 작성된 Cocos2D API로 보시면 됩니다. C++로 작성되어 있기 때문에 안드로이드나 윈도우즈 리눅스 같은 다른 플랫폼에서도 동작할 수 있도록 해주는 것이죠.


준비물이 필요하죠.

먼저 Cocos2D-X의 최신 버전을 구합니다.
http://www.cocos2d-x.org/projects/cocos2d-x/wiki/Download


Xcode Template 설치

압축파일 형태로 제공되니 압축을 푼 다음에 원하는 위치로 이동시키시면 됩니다. 그리고, 그 폴더 내부에 보면 install-templates-xcode.sh 파일이 있으니, 이것을 터미널에서 실행시켜서 Xcode용 템플릿을 설치하시면 되요.
명령은 다음과 같습니다.

sudo ./install-templates-xcode.sh





Xcode를 실행해서 File > New > Projects... 로 이동해서 새로운 프로젝트를 만들려다보면 템플릿이 올라와 있는 것을 확인하실 수 있어요.




안드로이드 개발 환경 설정

다음으로는 자바 쪽에서의 확인과 준비를 해줘야 겠네요.
이클립스를 켜신 후 Help > Install New Software ,



 제가 설치한 이클립스 버전은 Eclipse for Mobile Developer 버전입니다 (click here)
각자의 이클립스 버전을 확인하시고, 거기에 아래 details에 나오는 C/C++ Development Tools 가 설치되어 있는지 확인하세요. 설치가 안되어 있으면 위 그림에서 선택하셔서 설치하시면 될테구요.




저 처럼 이미 설치가 되어 있다면 이제
Android NDK (Native Development Toolkit)을 셋업할 때 입니다.

앞에서 잠깐 말씀드렸다시피, cocos2d-x는 C++로 작성이 됩니다. 그래서, 자바에서는 바로 그걸 사용할 수가 없죠.
하지만, 우리에겐 NDK가 있습니다.

아시다시피 안드로이드 애플리케이션은 자바로 작성이 되기 때문에 거의 모든 앱은 자바로 개발이 되어있죠. 그렇다고 모두가 그런건 아닙니다. 구글이 NDK라는 걸 내놓았거든요. 이게 뭐냐...
JNI(Java Native Interface)라고하는 기술(원래 자바에  있었고 사용하던 기술입니다)을 통해 C/C++로 작성된 컴포넌트도 사용할 수 있게 해주는 툴킷입니다.

cocos2d-x는 c/c++로 프로그램 소스코드를 작성하고, 자바 어플리케이션에서는 NDK를 통해 그 소스코드에 접근한다는 거죠.

NDK를 먼저 구해야겠죠?
안드로이드 사이트에 가셔서 우선 NDK 최신 버전을 다운로드(click here) 하신 후에, 압축 파일을 여러분이 원하는 위치에 풀어줍니다.

안드로이드 개발 환경에 NDK를 도입하면서 추가적으로 알아야할 한 가지는 C/C++ 컴파일을 위한 '툴체인(toolchain)'입니다.  툴체인은 컴파일과 링크(GCC 4.4.3 컴파일러을 이용하는)을 깔끔하게 처리해주고, (언제든 설치가능한) 인증된 APK 패키지로 빌드해 주죠.

툴체인을 사용함으로써,  외부의 C/C++ 라이브러리(Cocos2d-x같은)를 이클립스에서 사용할 수 있게 되고,  이 라이브러리들(APK 내부에 외부 동적 라이브러리로 컴파일된)은 JNI를 통해 안드로이드 아키텍쳐와 상호 통신하게 된다는 겁니다.

컴파일 툴체인은 두 가지 방법으로 사용됩니다.

  • stand alone mode : 사용자가 작성한 make file 내에서  arm-linux-androideabi-g++ 을 직접 사용합니다. 이렇게 하면 프로젝트의 복잡도를 증가시키므로 다른 대안이 없을 경우에만 고려하시면 됩니다.
  • integrated mode : NDK에 있는 ndk-build 쉘 툴을 이용하는 방법입니다. 이 툴은 NDK 라이브러리 빌드에 최적화된 make file 이니 이 방법을 택하는 게 더 낫겠죠?




Xcode에서 샘플 어플리케이션 만들기


샘플 어플리케이션을 만드는 건 어렵지 않아요. 특별한 기능을 만들어 넣을 것도 아니구요. 아래의 순서대로 따라가기만 하면 됩니다.




프로젝트 이름은 Cocos2DxFirstIosSample이라고 작성하시면 되고...



실행을 시키시면 아래와 같은 화면이 나올 겁니다.






Cocos2D-X & Android

iOS에서 했던 것과 같은 'Hello World' 를 안드로이드 플랫폼에서도 할 건데, 이클립스에는 Cocos2D-X 템플릿이 없기 때문에 명령창에서 뭔가를 해줘야 해요.


1. 안드로이드 프로젝트를 생성하기

Cocos2D-X가 설치된 디렉토리를 보면 create-android-project.sh 라는 쉘 스크립트가 있는데 이것을 이용해서 안드로이드 프로젝트를 만들 수 있습니다. 하지만, 그 쉘 스크립트에는 여러분이 설치한 NDK와 Android SDK에 대한 정보가 없으므로 쉘 스크립트를 수정해서 그 안에 NDK와 Android SDK가 설치된 위치를 알려줘야 합니다.

NCK_ROO_LOCAL과 ANDROID_SDK_ROOT_LOCAL 값을 여러분이 설치한 정보에 맞게 수정해주세요.


create-android-project.sh 수정


이제 수정한 쉘 스크립트를 터미널을 열어서 실행하시면 됩니다.
./create-android-project.sh


아래 그림처럼, 패키지 이름을 입력하라고 나오면 적절한 패키지 이름을 정해서 입력해주고...
그러면, 사용가능한 안드로이드 API 종류들과 그에 따른 id 를 보여줄 것입니다. 아래 그림 보면 id 가 1부터 차례로 보이죠?

<패키지명을 입력해 줍니다>



그럼, 그 중에서 내가 개발할 API를 선택해서 다시 입력해 주고, 프로젝트 이름도 입력해 줍니다.

< 타겟 플랫폼의 API를 지정합니다 >


모두 제대로 입력을 했다면 아래처럼 디렉토리와 파일들을 생성하고 업데이트 했슴을 보여주고 마칩니다.




위 로그에서 디렉토리와 파일이 추가되고 생성되는 위치를 잘 보세요. 어디에 생성되나요?
cocos2d-x가 설치된 디렉토리 밑(그러고보니, cocos2d-x가 설치된 디렉토리이름이 cocod2d-x로 되어 있네요.이런...)에 바로 생성되는 군요....(그렇다고, 만들어진 디렉토리를 다른데로 옮기지는 마세요. 아직은요. 이 다음에 수행할 작업을 할 때 수행이 안될 수도 있으니까요)




2. 안드로이드 프로젝트를 빌드하기

이제 프로젝트를 빌드할 때인데요. 두 과정으로 이루어집니다. 명령 스크립트를 통해 C++코드를 컴파일하고, 이클립스로 자바 코드를 컴파일하거든요.


첫 번째 단계:

C++코드를 컴파일 하기 위해서, 프로젝트 폴더(여기서는 cocos2d-x/samplecocos2dxandroid )에 보면 proj.android 폴더가 있습니다. 그 안에 build_native.sh 스크립트 파일이 있는데요. 이걸 실행시키면 되거든요.

근데 NDK_ROOT 가 지정되어 있지 않아서 아래처럼 될거예요. 스크립트를 직접 수정해서 NDK_ROOT를 지정해준다음에 해도 되겠지만 여기서는  .bash_profile에 환경변수를 따로 지정해두고 쓰도록 하겠습니다.

 아래 그림처럼 사용자 루트로 가서 .bash_profile을 수정해주세요






그리고나서 다시 build_native.sh 쉡 스크립트를 실행시켜주시면 아래 그림처럼 컴파일이 됩니다.



위 과정을 통해 C++ Cocos2D-X 라이브러리도 빌드되었고, 여러분이 작성중인 프로젝트를 위한 C++코드도 빌드되었습니다.





두 번째 단계:

이큽립에서 방금 수행한 결과로 만들어진 프로젝트를 읽어오도록 하겠습니다.
File > New > Project.. >Android > Android Project from Existing Code



다음 화면에서는 앞에서 준비한 프로젝트 폴더를 지정해줍니다.
/[cocos2dx설치된 폴더]/[프로젝트 이름]/proj.android




프로젝트를 불러오면, 에러가 발생할 수 있습니다.
아래그림처럼 말이죠.



프로젝트의 Properties > Android 항목을 보면 다음과 같은 걸 볼 수 있습니다. 참조하는 라이브러리가 X표시가 되어 있네요.



필요한 라이브러리 프로젝트를 import 해주면 해결이 됩니다.
File > Import



Browse... 버튼을 클릭하셔서 cocos2d-x 가 설치된 폴더 내에서 아래 그림 처럼 라이브러리를 찾아줍니다
.../cocos2d-x/cocos2dx/platform/android/java 네요


자, 그럼 소스코드에 발생하던 에러는 사라지죠?

나머지 하나는 AndroidManifest.xml 파일에 기분나쁜, 빨간 x 표시가 있죠? xml 파일을 열어보시면 아이콘 파일로 @drawable/icon 이라고 지정되어 있는데.. 프로젝트의 어디에도 drawable의 이름이 icon인것은 없어서 그렇습니다. 기본 템플릿으로 만들어지고 제공되는 아이콘이 ic_launcher.png 파일이므로 그걸로 바꿔주시던지 아니면 icon.png라는 이름의 (여러분이 직접 디자인한) 아이콘 이미지파일이 있다면 그것을 drawable 폴더에 복사해 넣어주세요.


빌드하고 실행시켜 보세요. HelloWorld 기본 템플릿으로 만들어진 앱이 구동이 될겁니다
아직까지는 이전 아이폰앱 프로젝트와 병합하지 않은 상태이기 때문에,  스크립트로 자동 생성되면서 포함된 이미지를 사용해서 같은 기능만 구현하고 있습니다. 오른쪽 하단 버튼도 그렇구요.






아이폰에서 작성한 라이브러리를 안드로이드쪽에서 사용하려면?
관심이 있던 부분이 이거죠. 한쪽 플랫폼에서 작성한 라이브러리를 다른 한쪽에서 재사용하는 것. 우선 두 개의 프로젝틀 를 비교해 보면 비슷한 구석이 있습니다.

프로젝트 내의 Classes 폴더를 보면, 동일한 네 개의 파일이 있는 걸 알 수 있어요. 실제 각각의 파일을 열어봐도 아주 동일합니다 (둘다 템플릿으로 만들어진 것들이니 당연하겠죠?)




보통 작업하는 순서가... iOS에서 Cocos2D-X를 이용해 작업을 하고 제대로 동작하면 안드로이드에서도 확인하는 하니까 우리도 그렇게 해보겠습니다. Xcode를 열어서 HelloWorldScene.cpp파일을 아래처럼 여러분이 원하는 문구로 수정해보세요.



그리고, 빌드한 후에 실행시켜서 변경사항을 확인하시고...




Xcode  프로젝트 폴더에서 Classes 폴더와 Resources 폴더를 복사해서, 안드로이드 프로젝트 폴더에 덮어쓰기를 하세요.
그리고, ./build_native.sh를 이용해서 다시 빌드하셔서 이클립스로 가져와 다시 실행해 보세요.




성공하셨나요?


이 글은 www.raywenderlich.com에 있는 Cocos2D-X iOS and Android : Getting Started 를 토대로 작성되었습니다.

Monday, December 17, 2012

AndEngine GLES2.0 개발환경설정

예전에 만들었던 앱이 있는데,  게임 엔진을 이용해서 다시 만들면 어떨까 싶어서 AndEngine GLES2.0을 찾아보기 시작했지요. 근데, 이게 튜토리얼이나 그 외의 문서가 아주 빈약하드라구요.



조근조근 보다보니 그럴만도 하다 싶긴 하네요. 현재도 개발이 진행중이고 꽤나 많은 장점을 가지고 있어서 안드로이드 앱을 개발하는 사람들에게 충분히 매력적인 엔진이라는 생각이 들지만,  최초 개발자인 Nicolas Gramlich 이외에 개발에 참여하는 풀이 다른 오픈소스 프로젝트에 비해 작지 않나 싶고, 그때문에 문서화까지도 신경쓰지 못하는 게 아닐까 싶어요.

실제 엔진을 이용코자 세팅하는 과정에서도 보면... AndEngine이나 Example, 그 이외의 Extension을 가져오면 몇 개의 소스코드 에러가 보입니다. 한쪽에서는 변경이 이루어졌는데 그것을 이용하는 다른 쪽에서는 수정하지 않아서 생기는 문제더라구요. 개인적인 바람이라면 좀 더 많은 개발자들이 참여해서 AndEngine을 이용하는 개발자들에게 필요한 부분을 채워줬으면 하네요.

아무튼...

우선 몇 가지만 체크하고 넘어가자면... GLES 2.0은 안드로이드 2.2 이상을 지원합니다. 그리고, ADT 또한 적어도 17이상은 되어야 한다고 합니다. 설치되어있는 API 레벨과 ADT를 확인해 주세요.

그리고, 여러분이 자바 개발을 해오셨거나, 안드로이드 개발을 해오셨던 분이라면 외부 라이브러리를 사용할 때 jar 형태로 프로젝트에 포함시켜서 작업하셨을 거예요. AndEngine도 GLES 1.0은 andengine.jar 파일만 프로젝트의 빌드 패스에 포함시켜주면 됩니다. 하지만,  GLES 2.0은 그렇질 않아요. 라이브러리 프로젝트(library project)로 포함시켜줘야만 합니다.

그래서, AndEngine의 GIT repository로부터 AndEngine이나 확장 기능들에 대한 라이브러리 프로젝트를 받아오기 위해 이클립스 GIT 애드온이 설치되어야 있어야 합니다. ( Eclipse Indigo 버전부터(?)는 EGit이 포함되어 배포되는 것 같으니 확인만 해보시면 될 듯 해요)

(아래에서 보이는 화면 내용은 Eclispe for Mobile Developers에 안드로이드 개발용 플러그인을 별도 설치했고  ADT는 r21 입니다. 글 쓰는 시점에는 최신 버전입니다. 2012년 대선 투표일 며칠 전입니다. 일때문에 분당에서 일산까지 이사를 왔는데 주소 이전을 좀 늦게 하는 바람에 투표는 분당까지 가서 해야하네요. 하지만, 꼭 할 겁니다. 여러분은 하셨나요?)


여기서는 GLES 2.0 관련 라이브러리 프로젝트만을 받아옵니다. 기타 확장 라이브러리까지 모두 받아오고 예제 프로젝트까지도 받아올 거라서 숫자가 좀 많습니다.  하나 하나 내려받아 보겠습니다.

우선 AndEngine의 github url 입니다. https://github.com/nicolasgramlich
링크를 클릭해서 어떤 프로젝트들이 진행 중인지도 살펴보세요.


자 그럼 이제 이클립스에서 위의 github의 필요한 프로젝트를 import 해줘야 겠죠?
file > import > Git > Projects from Git




계속 아래 그림 순서대로 진행하시면 됩니다.

윈도우즈에 설치시 참고사항:

[DEC 29 2012] 윈도우즈에서도 작업을 도우려고 설치하다보니, 임포트하고 리빌드하는 도중 PermGen Space 관련 에러가 발생하면서 그냥 멈춰서버리고 이클립스가 꺼져버리는 경우도 있더군요. Eclipse for Mobile Developer버전에 ADT만 설치하고 진행해봤는데 계속 그랬습니다. JDK 1.7버전이었는데, 이클립스 기본 컴파일러를 1.6으로 변경해주고 진행하니 되네요. 정확한 이유는 모르겠습니다. 

처음엔 1.6 버전이었습니다. ini 파일 수정해서 MaxPermSize 크게 해주고 별짓거리를 다해도 계속 먹통. 1.7로 변경하면 해결되기도 한다는 어느 댓글에 1.7로 올리고 해도 먹통. 그냥 맥에서만 하라는 얘긴가 싶어 싸그리 지워버렸다가 오기가 발동해서 이클립스 컴파일러를 1.6으로 변경하고 했더니 import가 제대로 되네요.

확실한 해결책은 아닌듯 합니다만(한번 더 설치해볼까 싶기도 합니다) 참고해서 설치해보세요.

추가: JDK를 1.6으로 내리고 임포트하는데, 문제없이 되네요. 어이없어 정말..ㅋ





아래 보면 branch를 선택하도록 하는데, master 가 GLES 1.0 이므로, 여러분은 GLES 2.0을 선택하시면 됩니다. GLES2-AnchorCenter는 향후 합쳐질 부분에 대한 것이라고 합니다.







위에 나열된 순서대로 하나씩 하나씩 확장 라이브러리들까지도 받으려고 합니다. 아래에 첨부된 그림의 목록대로 여러분도 하나씩 선택하셔서 받아오도록 하세요.



물론, 여기서 보이는 모든 라이브러리를 받지 않아도 됩니다. 필요한 것만 가져와도 되겠죠. 여기서 이렇게 많은 라이브러리 프로젝트를 가져온 것은  AndEngineExamples 프로젝트를 위해서 입니다. 앞에서도 말씀 드렸다시피 엔진에 대한 관련 문서가 부족하다보니 가장 좋은 방법은 예제를 보면서 하는 건데(다행스럽게도 다양한 예제가 포함되어 있습니다) 이 예제들이 제대로 실행되어야 하겠죠? 

아래 그림처럼 프로젝트 > properties > Android 를 선택해서 보시면 아래에 library 섹션이 있습니다. 프로젝트마다 연관된 라이브러리가 체크되어 있으니 그것도 확인하시고, 나중에 실제 라이브러리를 사용할 경우에도 그 섹션에서 찾아서 등록해주면 되니 기억해두세요.







하나더! 개발용 안드로이드 디바이스가 있으시다면 구글 앱스토어에서 AndEngine으로 검색을 하시면 예제가 포함된 앱이 있을 것입니다. 이걸 실제 디바이스에 설치하신다면 많은 참고가 되기도 하겠죠.


여기서 잠깐!! Errors!!

모두 내려받은 후에 보면 AndEngineExamples 프로젝트의 몇 개의 클래스 파일에 소스코드 에러가 발생할 수도 있습니다.  앞에서도 말씀드렸지만 엔진 개발자들이 너무도 바쁜 관계로.. 그리고 지금도 부분부분 진행되어 가고 있어서인지 제때제때 수정되지는 않는 것 같아요?  에러가 너무 많다면 필요한 라이브러리들을 제대로 내려받은건지 부터 확인하시고, 다행스럽게도 라이브러리는 모두 import 한 상태에서 몇개의 클래스 파일에만 소스코드 에러가 발생한 것이라면 아래처럼 수정해 주세요. (아래의 수정부분은 제대로 갖춰진 상태에서 발생할 수 있는 - 적어도 제가 겪은- 4개의 소스코드 에러에 대한 해결 방법일 뿐입니다.)

HullAlgorithmExample.java



TextBreakExample.java



BoundCameraExample.java



SplitScreenExample.java






자 그럼 이제 라이브러리가 준비된 것 같으니, 실제 만들어가며 이야기를 더 해보겠습니다.
(하지만, 제가 제 일이 어느정도 끝나야 포스팅될 거라는 거..... )







Friday, August 31, 2012

Android: failed to find style 'mapviewstyle' in current theme ?

It seems that the default style for MapView is specified with 'mapViewStyle', and the style 'mapViewStyle'  is defined  in Android public.xml file. But Android style 'mapViewStyle' is not defined in android styles.xml file. So I guess that the problem occurred.

So, I tried to solve that problem, as follows...

public.xml and styles.xml : You can find them in 'android-sdk/platforms/<android-api-level>/data/res/values folder.


1.GO-TO : android-sdk/platforms/ <android-api-level> /data/res/values 
2.In public.xml,  check the mapViewStyle's definition and id value.











3. In styles.xml,  insert 'mapViewStyle' item element as shown below.
(Note : 'mapViewStyle's id values' and insert point )



***  If you find anything that you think is wrong or unclear, please let me know by dropping a line in the comments section.



Sunday, August 26, 2012

Android Location Startegies



Location Strategies

이 문서는 안드로이드 개발자 노트의 일부를 내맘대로 번역한 것임을 밝힙니다.
출처:http://developer.android.com/guide/topics/location/strategies.html


휴대 단말기 사용자가 자신이 어디에 있는지를 알게하면 너님이 만든 애플리케이션이 더 똑똑해 보이기도 하고 사용자에게 더 나은 정보를 전달할 수도 있지. 위치 정보를 다루는 애플리케이션을 안드로이드에서개발할 때 GPS나 네트워크 위치 제공자(Android's Network Location Provider)를 이용할 수가 있는데, GPS가 더 정확하지만 GPS는 밖에 나가야만 사용할 수가 있어.  게다가 GPS는 배터리를 무쟈게 소모시킬 뿐더러 사용자가 원하는 만큼 재빠르게 위치 정보를 넘겨주지도 않아.

그에 반해, Android's Network Location Provider는 무선 통신 타워나 Wi-Fi 신호를 이용해서 위치를 잡기 때문에 건물 내부에서나 외부에서나 모두 사용할 수가 있고, 반응 속도도 빠르고, 배터리 소모도 적어.

아무튼 사용자의 위치정보를 얻기위해서 위에 언급한 두 가지를 모두 사용할 수도 있고, 그냥 하나만 선택해서 사용할 수도 있다는 건 기억해두자.


Challenges in Determining User Location


휴대 단말기로부터 사용자의 위치정보를 얻어오는건 어찌보면 복잡한 고민이 따르는 일이라고 봐. 왜냐면, 개발자인 너님을 고민에 빠지게 하는 원인들이 많거든.  정보를 받지 못할 수도 있고 기껏 받아온 게 정확하지 않은 정보라면 난감하지 않겠어?

그럼 그런 상황을 초래 하는 이유가 뭐냐. 아래와 같이 정리해 볼 수 있지.

위치 정보를 제공하는 소스가 많다 : GPS, Cell-ID, Wi-Fi가 각각 사용자 위치를 가늠할 수 있는 정보를 주는데 어떤 정보를 사용할지 또 어떤 것이 믿을 만한 정보인지를 결정하는 건  정확도, 속도 배터리에는 어떤 영향을 주는지 사이에서 고민한 후에 결정할 것이라서 그래.

사용자는 계속해서 움직인다 :  사용자는 계속 해서 움직이기 때문에, 개발자인 너님은 이런 상황을 가정하고, 계속해서 반복적으로 자주 다시 계산해야하거든.

정확함이라는 게 뭔지 : 각각의 위치정보 제공자에게 받아온 위치 정보들이라는 게 말이야. 그 제공자들 각자가 원래 가지고 있는 정확도라는게 모두 달라. 일 분 전에 A라는 소스에서 받아온 정보가 방금 B라는 곳에서 받아온 정보보다 더 정확할 수 있다는 거야.

이러한 이유로 사용자에게 신뢰를 줄만한 위치 정보를 얻는다는게 아주 힘들지. 이 문서에서는 그러한 고민을 해결하기 위한 것이야. '어떻게 해야 그나마 신뢰할만한 위치정보를 얻을 수 있을까'에 대한 답이랄까? 너님이 개발하는 앱의 사용자에게 좀 더 정확한 위치정보를 재빠르고 정확하게 제공하려고 하는 너님의 고민에 좋은 아이디어가 되길 바라.


Requesting Location Updates


위에 언급한 문제들을 해결하기 전에 위치정보를 획득하는 방법에 대해서 먼저 알아야겠지? 안다면 다음 소제목으로 패스.

안드로이드에서 사용자 위치정보를 얻는 건 콜백(callback)에 의해 이루어져. LocationManager 클래스의 requestLocationUpdates() 메소드를 호출하면서 '나 위치정보를 업데이트 하고 싶어요'라고 알릴 수 있고, 그럼 업데이트된 위치정보를 LocationListener 에게 전달하지. 이 LocationListener는 너님이 구현해줘야하는 부분인데, 이 인터페이스에는 미리 정의된 몇 개의 콜백 메소드가 있어서 사용자 위치가 변화했을 때나 서비스 상태가 변화할 때 마다 Location Manager가 그 메소드들을 호출하거든.

아래 예제 코드를 봐.
   // 시스템의 위치관리자(Location Manager)에 대한 레퍼런스를 획득한다
   LocationManager locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);

   // 위치정보 업데이트시 반응할 리스너를 정의한다
   LocationListener locationListener = new LocationListener() 
   {
        public void onLocationChanged(Location location) {
             // 네트워크 위치정보 제공자가 새로운 위치정보를 찾게되면 호출되는 메소드임
              makeUseOfNewLocation(location);
        }
        public void onStatusChanged(String provider, int status, Bundle extras) {}
        public void onProviderEnabled(String provider) {}
        public void onProviderDisabled(String provider) {}
   };

   // Location Manager에게 리스너를 등록해준다
   locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, locationListener);
requestLocationUpdate() 메소드의 첫 번째 파라메터는 위치 제공자의 타입이야. 여기서는 무선 통신 타워나 Wi-Fi 기반의 위치 정보를 위한 Network Location Provider로 제공자를 설정한 거지. 두 번째와 세 번째 파라메터는 위치 정보 전달을 위한 최소시간과 최소 거리를 의미해. 여기서는 모두 0으로 설정했으니 가능한 자주 알려달라는 거지. 마지막으로 네 번째 파라메터는 너님이 구현한 Listener 객체를 알려달라는 거고.

GPS 위치 정보 제공자로부터 받고 싶다면 NETWORK_PROVIDER를 GPS_PROVIDER로 바꾸면 되고, 양쪽 모두에서 받고 싶다면 requestLocationUpdates()를 각각에 대해 총 두 번 호출해주면 되지. (물론 LoationManager의 getAllProviders()메소드를 이용해서 모든 provider 정보를 탐색한 후에 몇 몇 조건을 따져서 그 중 가장 나은 하나를 선택할 수도 있어. 이 부분은 이 블로그의 다른 포스팅을 참고해바바바)

Requesting User Permissions


NETWORK_PROVIDER든 GPS_PROVIDER든 위치 정보를 업데이트 받고 싶다면, ACCESS_COARSE_LOCATION이나 ACCESS_FINE_LOCATION 권한을 안드로이드 메니페스트 파일에 추가해 줘야해. 권한이 없으면 위치 정보 업데이트를 요청할 때 멈추게 될끄야.

<manifest ... >
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    ...
</manifest>
그리고, NETWORK_PROVIDER와  GPS_PROVIDER 모두를 사용하려 한다면 ACCESS_FINE_LOCATION 권한 하나만 추가해줘도 돼. 왜냐면, 요게 두 종류 PROVIDER에 대한 권한 모두를 포함하고 있거든, ACCESS_COARSE_LOCATION은 NETWORK_PROVIDER에 대한 권한만을 가지고 있고.


Defining a Model for the Best Performance


위치 기반 애플리케이션이라는 게 이제 뭐 흔하디 흔하지만, " 떨어지는 정확도, 사용자의 잦은 이동, 너무도 다양한 위치 정보 소스, 그 놈의 배터리"  이런 문제거리들을 모두 해결하면서 애플리케이션을 작성하는 게 그리 만만치는 않지. 배터리 소모를 최소화 하면서 사용자가 만족할만한 위치정보를 얻어오려면 먼저 너님은 '위치 정보를 얻는 방법에 대한 효과적인 모델'을 먼저 정의해야 해.  너가 작성한 LocationListener가 허구헌 날 계속 대기모드로 업데이트를 기다린다고 생각해봐. 소개팅한 여자를 미리 수소문해서 예약해 놓은 맛집에 데려가기도 전에 너님의 스마트폰은 방전되어 버릴테고 결국 근처 김밥헤븐에서 다 말라비틀어진 김밥을 먹은 후 그 소개팅녀의 스맛폰에서는 너님의 전화번호가 지워져 버릴거거든. 무섭지?

그래서, 그 모델에는 언제 업데이트에 대한 리스닝을 시작할지, 언제 끝낼지, 언제 수집된 위치정보를 사용할지에 대한 정의도 포함되어야 하지.



Flow for obtaining user location

자 이제 사용자 위치 정보를 획득하는 가장 일반적인 흐름을 볼까?

  1. 애플리케이션을 시작한다
  2. 잠시 뒤, 위치 정보 제공자들로 부터 업데이트 정보를 수신하도록 리스닝을 시작한다
  3. 새로운 위치 정보가 잡히더라도 덜 정확한 정보를 걸러내면서 최상의 위치 장보를 유지한다
  4. 위치 정보 업데이트에 대한 리스닝을 멈춘다
  5. 최상의 위치 정보로 추정되는 정보를 이용한다


아래 그림은 시간대별로, 애플리케이션이 업데이트에 귀기울이는  과정에서 일어나는 일들을 표현해 본 것이야. 자세히 보면 너님이 너님의 앱에서 위치 정보를 사용함에 있어  너님 스스로 결정해야할 시점이 다양하게 존재한다는 걸 볼 수가 있지.



















Deciding when to start listening for updates

애플리케이션이 시작하자마자 리스닝을 시작하게 하거나 아니면 사용자 선택한 특정 시점에만 시작하게 구현하고 싶을텐데, 하나 기억할 것은 위치 정보를 결정하기 꽤 긴 과정을 거치게 되면 그만큼 배터리 수명이 떨어져 버린다는 거다. 그렇다고 짧게 잡으면 좋으냐 그것도 아니지.. 정확한 위치 정보를 잡기가 힘들어 지거든.(도대체 어쩌라구)


Getting a fast fix with the last known location

그래서!! 맨 처음 위치 정보를 잡을 때까지의 시간이 꽤 길어질 수가 있으니까,  더 정확한 위치 정보가 리스너에 전달되기 전까지는 최근에 저장되어 있는 쓰면 어떠냐는 거지. 그런 정보도 getLastKnownLocation(String) 메소드로 알아낼 수가 있거든. 인자로 location provider를 알려주면 해당 제공자의 최근 위치를 가져오는 거지 ( API를 보면 알겠지만, 이 메소드는 단말기기 꺼진 상태에서 전혀 다른 장소로 이동한 상태라도, 꺼지기 이전에 저장된 정보를 알려주게 되므로 받은 정보가 전혀 쓸모 없는 정보일 수도 있다는 거야)

String locationProvider = LocationManager.NETWORK_PROVIDER;
// Or use LocationManager.GPS_PROVIDER

Location lastKnownLocation = locationManager.getLastKnownLocation(locationProvider);


Deciding when to stop listening for updates

획득한 위치 정보가 더 이상 필요없어 질 시점을 결정하는 로직도 역시 애플리케이션에 따라 아주 간단하기도 하고, 아주 복잡해지기도 하지. 중요한 건 위치 정보를 획득하는 시점과 그것이 사용되는 시점 간의 갭이 아주 작아야 추정한 위치 정보의 정확도도 올라간다는 거야.  오랫동안 리스닝을 하게 되면 배터리 소모가 심하다는 건 항상 기억하고 있지? 그래서 너님이 필요하다 생각했던 위치 정보를 얻었다면 리스닝하는 작업을 바로 끝내줘야 해. removeUpdates(PendingIntent) 메소드를 호출해서 말이야.

// Remove the listener you previously added
locationManager.removeUpdates(locationListener);


Maintaining a current best estimate

가장 최근에 얻어진 위치 정보가 가장 정확한 값이라고 판단하기가 쉽지만, 사실은 그렇지 않아. 수집된 위치 정보마다 그 정확도가 아주 다양하거든. 그렇기 때문에 가장 정확한 위치 정보를 선별하기 위한 로직을 가져야만 하지. 그런 선별에도 일반화된 기준이 있어야 하겠지? 물론, 애플리케이션에 따라 다르고 실제 필드에서 어떻게 테스트하느냐에 따라 달라지겠지만 말이야.

하지만, 아래와 같은 질문들을 해보며 위치정보의 정확도를 체크해보면 어때?

  • 가져온 위치 정보가 이전에 획득한 추정 위치값들에 비해  뚜렷하게 새로운 정보인가?
  • 위치 정보의 정확도가  이전에  작성된 값들에 비해서는 더 나은가? 아니면 떨어지는가?
  • 새로운 위치 정보를 보내온 제공자는 누구고 이게 이전것들보다 더 믿을만 한가?


위의 체크 사항들을 하나하나 로직으로 작성해 보면 아마도 아래의 소스코드와 같을 거야.

private static final int TWO_MINUTES = 1000 * 60 * 2;

/** 어떤 Location 정보가 현재 Location fix보다 더 나은 값인지를 결정하는 메소드
  * @param location  너님이 평가하고 싶어하는 위치 정보
  * @param currentBestLocation  현재의 Location fix, 새로운 값을 이 값과 비교할 것임
  */
protected boolean isBetterLocation(Location location, Location currentBestLocation) {
    if (currentBestLocation == null) {
        // 위치 정보가 없는 것보다는 있는 게 더 나으므로
        return true;
    }

    // 새로 들어온 위치 정보가 현재의 fix값보다 새것인지 오래된 것인지부터 검사
    long timeDelta = location.getTime() - currentBestLocation.getTime();
    boolean isSignificantlyNewer = timeDelta > TWO_MINUTES;
    boolean isSignificantlyOlder = timeDelta < -TWO_MINUTES;
    boolean isNewer = timeDelta > 0;

    // 새로 들어온 위치 정보가 현재 정보보다 2분 이상 더 새로운 것이라면 그냥 그걸 쓰자
    // 2분이면, 우사인 볼트의 경우 달나라까지 갔다올 시간이잖아.
    if (isSignificantlyNewer) {
        return true;
    // 위와 같은 이유로, 현재 정보보다 2분이나 더 오래된 정보라면 무시해도 될거 같아.
    } else if (isSignificantlyOlder) {
        return false;
    }

    // 이제는 새로운 정보가 현재의 정확도보다 더 나은건지 아닌지를 검사해보자
    int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation.getAccuracy());
    boolean isLessAccurate = accuracyDelta > 0;
    boolean isMoreAccurate = accuracyDelta < 0;
    boolean isSignificantlyLessAccurate = accuracyDelta > 200;

    // 혹시, 동일한 Provider에서 제공한 정보인지도 알면 판단하기가 더 쉽겠지?
    boolean isFromSameProvider = isSameProvider(location.getProvider(),
            currentBestLocation.getProvider());

    // 두 정보를 시간과 정확도를 함께 비교하면서 판단해본다
    if (isMoreAccurate) {
        return true;
    } else if (isNewer && !isLessAccurate) {
        return true;
    } else if (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) {
        return true;
    }
    return false;
}

/** 이 메소드는 인자로 넘겨진 두 개의 provider가 동일한 것인지를 판단한다  */
private boolean isSameProvider(String provider1, String provider2) {
    if (provider1 == null) {
      return provider2 == null;
    }
    return provider1.equals(provider2);
}


Adjusting the model to save battery and data exchange

간단한 애플리케이션을 만들어서 테스트 해보면, 위의 코드만으로는 뭔가 좀 부족하다 싶을 거야. 약간의 조정이 필요하지. 아래에 적어놓은 두 가지 상황을 비교해 가면서 두 요건들 사이에서 밸런스를 찾아가야 할거야.

위치 정보를 받아들일 범위를 좁힌다.
신호를 받아들이는 범위를 좁혀버리면 이전보다 더 적은 수의 GPS와 네트워크 위치 서비스 하고만 교신하면 될 뿐더러 배터리 수명도 유지하기가 편할거야. 하지만,  그만큼 좋은 정보를 찾기위한 리소스가 부족해지기도 하지

위치 제공자한테 더 적은 빈도로 업데이트 정보를 보내라 한다.
새로운 업데이트를 보내는 빈도를 줄여버리면 배터리 효율을 개선할 수가  있지만, 정확도 측면에서는 손해를 볼 수 밖에 없지. 배터리 효율이냐 정확도 사이에서의 판단은, 어떤 앱에서 어떻게 사용되느냐에 따라 많이 달라지겠네 그치? 업데이트 빈도를 줄이려면 requestLocationUpdates() 메소드의 interval time, minimun distance 값을 올려주면 돼.

Provider의 종류를 제한해버린다.
앱을 사용하는 환경에 따라 정확도를 요구하는 수준도 달라지잖아. 그런 상황에 맞춰서,  네트워크 위치 제공자나 GPS 둘 중 하나만을 쓸 수가 있을거야. 그런 서비스들 중 하나만 사용함으로써 (정확도 감소라는 비용을 지불해야할 상황이 있을 수도 있지만 - 반드시 수반되는 비용은 아니라는 의미로 이해해) 배터리 사용을 감소시킬 수가 있지.



Common application cases

너님이, 너님의 앱에서 위치 정보가 필요한 이유야  내 월급이 한달에 1억은 되어야하는 이유 만큼이나 많겠지만, 아래의 시나리오를 잘 참고한다면 아마도 왠만큼은 너님의 앱이 좀 더 나은 모습을 변화하도록 위치 정보를 잘 활용할 수 있을 거라 생각해. 각각의 시나리오는 너님이 리스닝을 언제 시작하고 끝내야 할지에 결정하는데 좋은 예가 될테고, 질 좋은 위치 정보를 획득하면서도 배터리 사용은 더 효율적으로 하는 앱을 위한 길라잡이가 될 수도 있겠지.


사용자가 만드는 컨텐츠 내에 위치 정보가 태그로 삽입되는 경우

여행 정보를 만든달지, 음식점에 대한 리뷰를 포스팅한달지 이런 경우의 시나리오야. 이런 경우에는 사용자가 컨텐츠를 작성하는 동안(트위터를 작성하는 등의)에 위치 정보를 결정하면 되겠지? 컨텐츠 작성을 시작할 때나 해당 앱이 시작했을 때 리스닝을 시작하면 될테고, 글을 저장하거나 포스팅하면 리스닝을 끝내야겠지.  물론, 그 작업이 이루어지는 평균적인 시간에 대한 예측, 그리고 그 사이에 리스닝을 지속한다면 초래될 여러 조건들도 너님이 판단해야하는 건 당연한 거고...

주변의 레스토랑이나 쇼핑센터를 찾고자 한다면?

추천 항목을 제공하면서도 사용자가 이동을 하면 추천 항목도 당연히 변화해야 하겠지?
리스닝을 시작한 이후로 조금 더 정확한 위치 정보를 획득할 때마다, 거기에 맞는 추천 항목리스트도 업데이트 해줘야 할거야. 그러다, 추천 항목이 안정화되면 리스닝도 멈춰 버리면 되겠지
그런 앱에서 사용자가 관심 있는건 주변의 추천 항목들이지, 자기 자신의 위치가 아니잖아.




















원문에는 Mock 위치 정보를 사용하는 법이 이후로도 설명되어 있지만, 그 정도는 금방 알 수 있는 부분이라서 여기서 난 끝내고자 한다. 도움이 되었기를.......끝!









Wednesday, August 22, 2012

Android Layout Tricks #1

효율적인 레이아웃 구성에 대한 이야기



다음 글은 아래의 글을 내맘대로 번역한 것임을 밝힙니다.
원본출처: http://android-developers.blogspot.kr/2009/02/android-layout-tricks-1.html

안드로이드 UI 툴킷은 사용하기 편한 몇 개의 레이아웃 매니저를 제공하며, 대부분의 경우에는 이 레이아웃 매니저의 기본적인 특성들만을 이용해서 UI를 개발하게 되지. 하지만, 그런 기본적인 특성을 고집하면 효과적으로 UI를 구성할 수가 없어. 대표적인 예가 LinearLayout을 과도하게 사용하는 것인데, 뷰 계층내에 쓸데 없이 많은 뷰가 존재하게 하지.

모든 뷰는 그만큼의 댓가를 요구해. 초기화를 해야하잖아. 레이아웃을 구성하고 그리고 하는 작업이 꽤나 느려지는 건 당연하지 않겠어?  weight 파라메터를 사용하는 LinearLayout을 몇 개 사용하게 되면 ( 자식 노드(child)들의 크기가 다시 한번 계산되어야 하기 때문에) 더더욱 그렇게 되겠지.

레이아웃과 관련된 아주 간단하면서도 일반적인 예를 들어 설명해볼께. 아래 그림 처럼 만들어볼 건데, 왼쪽에는 아이콘이, 오른쪽 위에는 제목을 아래에는 추가적인 설명을 붙이려고 해


위의 모습처럼 나타나게 하려면 하나의 ImageView와 두 개의 TextView로 레이아웃을 구성해야 한다는 정도는 알지? HierachyViewer로 보면 아래와 같은 모습일거야.



LinearLayout 을 이용한 레이아웃 구성

이 정도의 레이아웃은 LinearLayout을 이용해서 아주 쉽게 구현할 수가 있어. Horzontal LinearLayout 내부에 ImageView와 Vertical LinearLayout을 포함시키고, 내부에 포함된 Vertical LinearLayout은 다시 두 개의 TextView를 포함하면 되지.  코드를 볼까?

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="?android:attr/listPreferredItemHeight"
    
    android:padding="6dip">
    
    <ImageView
        android:id="@+id/icon"
        
        android:layout_width="wrap_content"
        android:layout_height="fill_parent"
        android:layout_marginRight="6dip"
        
        android:src="@drawable/icon" />

    <LinearLayout
        android:orientation="vertical"
    
        android:layout_width="0dip"
        android:layout_weight="1"
        android:layout_height="fill_parent">

        <TextView
            android:layout_width="fill_parent"
            android:layout_height="0dip"
            android:layout_weight="1"
                    
            android:gravity="center_vertical"
            android:text="My Application" />
            
        <TextView  
            android:layout_width="fill_parent"
            android:layout_height="0dip"
            android:layout_weight="1" 
            
            android:singleLine="true"
            android:ellipsize="marquee"
            android:text="Simple application that shows how to use RelativeLayout" />
            
    </LinearLayout>
</LinearLayout>

이 레이아웃은 아주 잘 동작할거야 하지만, 이것을 ListView의 리스트 아이템으로 사용한다면 자원 낭비가 심할 수도 있다는 거지. 이 레이아웃을 RelativeLayout을 이용해서 다시 작성하면 이전 구성보다 (리스트 아이템 하나당)하나의 View를 절약할 수 있고 그만큼 뷰 계층도 복잡도가 덜 해져.  구현 소스코드도 아주 심플해지는 건 덤?

RelativeLayout을 이용한 레이아웃 구성


<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="?android:attr/listPreferredItemHeight"
    
    android:padding="6dip">
    
    <ImageView
        android:id="@+id/icon"
        
        android:layout_width="wrap_content"
        android:layout_height="fill_parent"
        
        android:layout_alignParentTop="true"
        android:layout_alignParentBottom="true"
        android:layout_marginRight="6dip"
        
        android:src="@drawable/icon" />

    <TextView  
        android:id="@+id/secondLine"

        android:layout_width="fill_parent"
        android:layout_height="26dip" 
        
        android:layout_toRightOf="@id/icon"
        android:layout_alignParentBottom="true"
        android:layout_alignParentRight="true"
        
        android:singleLine="true"
        android:ellipsize="marquee"
        android:text="Simple application that shows how to use RelativeLayout" />

    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        
        android:layout_toRightOf="@id/icon"
        android:layout_alignParentRight="true"
        android:layout_alignParentTop="true"
        android:layout_above="@id/secondLine"
        android:layout_alignWithParentIfMissing="true"
                
        android:gravity="center_vertical"
        android:text="My Application" />
</RelativeLayout>


RelativeLayout을 이용해서 구현한 것이나 이전 것이나 동일하지만 다른 점이 하나 있어.
첫 번째나 두번째나 두 개의 TextView을 가지고 있잖아? 만약에 추가 설명이 리스트 아이템에 주어지지 않아서 제목만 출력해야한다면 어떻게 할 수 있을까? 이러한 상황이라면 두 번째 TextView의 Visibiility 속성을 Gone으로 설정해서 해당 항목을 제외시킬 수가 있을거야.

여기서 중요한 건, 이게 LinearLayout에서는 완벽하게 동작하지만 RelativeLayout을 사용하면 그렇지 않다는 거지.




RelativeLayout, alignWithParentIfMissing

RelativeLayout에서는  부모 뷰나 RelativeLayout 자신, 혹은 다른 뷰를 기반으로 해서 정렬이 되잖아? 앞의 예시 코드에만 봐도 추가 설명은 RelativeLayout의 아래(bottom)에, 제목은 추가설명위, 부모 뷰의 윗쪽에 고정되도록 구성했지. 근데 여기서, 추가 설명을 GONE으로 해버리면 RelativeLayout은  제목에 해당하는 TextView의 bottom 부분을 어디에 맞춰야 할지를 모르게 되어 버린다는 거야.

아주 다행스럽게도 이러한 문제는  alignWithParentIfMissing 이라는 아주 특별한 속성을 사용해서 해결할 수가 있어. 이 파라메터 값는 boolean 값으로 지정할 수가 있는데, RelativeLayout에게 '혹시라도 네 모습을 결정할 근거가 되는 객체가 없을 경우엔  네 자신(RelativeLayout)을 기준으로 삼아라'라고 말해주는 거라 생각하면 돼.

예를 들어 설명해볼까? 앞의 예제에서는 제목 TextView를 선언하면서 '아이콘 그림의 오른쪽에, 부모 뷰의 오른쪽 위에, secondLine(추가설명)의 위에' 위치한다고 정의했잖아? 근데 제목의 bottom값으로 정해야할, 아래에 당연히 있어야 할 추가설명 TextView가 없으니, 자기 자신(Relativelayout)의 바닥(bottom)을 제목의 bottom으로 지정해버리는 거지.






이제 우리가 작성한 레이아웃이 아주 잘 동작할거야. 일부 예외 적인 상황에서도 말이야. 게다가 뷰 계층도 아주 심플해졌어. LinearLayout의 weight도 사용하지 않았으니 더 효율적이고 말이야. 두 가지 구현 방법의 차이점은 아래 뷰 계층도를 보면 더 분명해질거야.

다시한번 말하지만,  이 두 구현방법의 차이점은 '(예로) ListView의 리스트 아이템으로 사용할 레이아웃을 구성해야 할 때'와 같은 상황에서라면 아주 중요하게 고려할 부분이라는 거야. 레이아웃을 구성하는 방법을 보여주는 아주 간단한 예제였지만 이것을 통해서 사용자 인터페이스를 효과적으로 구성하는 방법을 배웠길 바라. 끝!