구글안드로이드프로그래밍 GUI 설계, 위젯과레이아웃 QVGA급컬러 LCD 가대세가되어버린최근의휴대폰환경에서는 GUI 도모바일애플리케이션개발의매우중요한요소로자리잡았다. 이번달에는안드로이드플랫폼의 GUI 프레임워크를살펴보도록하자. 5 연재순서 1 회 2008. 1 애플리케이션구조분석 2 회 2008. 2 GUI 설계, 위젯과레이아웃 3 회 2008. 3 액티비티와인텐트, 그리고서비스 4 회 2008. 4 리소스와컨텐트프로바이더 5 회 2008. 5 웹인터페이스와구글맵연동 6 회 2008. 6 XMPP 기반 P2P 서비스 배준현 joonion@gmail.com 언젠가개방형휴대폰플랫폼에서자유롭게모든모바일서비스를개발할수있는날이올것이라고믿는낙관론자. 개방형모바일소프트웨어컨설팅전문기업인 ( 주 ) 케이마루를창업했다. 휴대폰에서 GUI를논하는것은그다지오랜역사를가지고있지않다. 불과몇년전까지만해도휴대폰에서는 MMI(Man Machine Interface) 라는이름의아주단순한사용자인터페이스를다루고있었다. 휴대폰의 LCD가넓어지고, QCIF급을지나 QVGA급의컬러 LCD가대중화되면서부터어느사이엔가휴대폰에서도화려한 GUI의구성이필수가되었고, 그에따라 GUI 구현을위한프레임워크가절실히필요하게되었다. 초창기에등장한자바ME는 lcdui라는저수준의 API를설계했고, 브루플랫폼역시초보적인수준의 UI 프레임워크를제공했으므로현재대부분의휴대폰제조사는자사만의독특한 UI 프레임워크를설계해독자적으로애플리케이션 UI를개발하고있다. 안드로이드플랫폼의 GUI 프레임워크는최근의이러한휴대폰 UI 발전을충분히고려한애플리케이션프레임워크를가지고있다. 적어도초창기자바의 AWT와는비슷하거나그이상의수준을유지하는다양한위젯과레이아웃을제공한다. 또한 XML 기반의 GUI 설계를가능하게함으로써 UI 설계와구현을위해과도한프로그래밍작업을배제할수있도록개발자를친절히배려하고있다. 짧은지면을통해안드로이드플랫폼의방대한위젯과레이아웃을모두다루는것은이미 SDK에포함된개발자문서와예제가 있으므로무의미한일이될것이다. 여기서는사용자인터페이스정복을위한지름길을찾아보는데초점을맞추도록하겠다. 뷰와뷰그룹안드로이드에서사용자인터페이스를이루는기본요소는뷰와뷰그룹이다. < 그림 1> 을보자. 스크린을구성하는 UI 컴포넌트들은트리형태의자료구조를가진다. 잎새노드에해당하는컴포넌트는 android.view.view 클래스를상속하는위젯으로구성되고, 위젯을포함하는상위노드들은 android.view. ViewGroup을상속하는레이아웃으로구성된다. 이것은이미기존의많은 GUI 프레임워크들이사용해오던방식이므로크게낯설지않다. ViewGroup ViewGroup View View View View View < 그림 1> 스크린의트리구조 226 m a s o
위젯과레이아웃위젯은 View 클래스를상속해화면디스플레이와이벤트처리를할수있도록구현된스크린구성의최소단위를말한다. android.widget 패키지에는여러유형의위젯들이포함되어있다. TextView, ImageView, Button, ImageButton 등은가장간단한위젯들이다. < 그림 2> 는안드로이드프레임워크에포함된모든위젯들에대한클래스다이어그램이다. 레이아웃은 Viewgroup 클래스를상속해트리구조의자식노드에해당하는뷰가자신의레이아웃내에서어떤위치에어떻게배치되는지를정의하는클래스를말한다. 레이아웃들도 android.widget 패키지에포함되어있으며 LinearLayout, RelativeLayout, TableLayout 등의다양한레이아웃들이있다. < 그림 3> 은프레임워크내의레이아웃들에대한클래스다이어그램이다. 한가지더주목할점은 < 그림 3> 에서보듯이 View Group 역시 View를상속하고있다는점이다. 다양하고복잡하게얽혀있는각각의위젯과레이아웃을어떻게자유자재로다룰수있는가에관한것이다. 이것은많은인내를요구하는시간과의싸움이될것이다. 여기서는이싸움을조금덜지루하게만들기위한간단한테스트애플리케이션을만들어보자. UI 테스트애플리케이션다음달에좀더자세히다루겠지만안드로이드애플리케이션의구성요소에는액티비티, 콘텐트수신자, 서비스, 콘텐트제공자이렇게네가지가있다. 이중액티비티는애플리케이션의사용자인터페이스를처리하기위한애플리케이션구성요소에해당한다. 즉어떤화면이 < 그림 1> 처럼트리구조의화면구성을갖고있을때, 이화면을실제로 LCD에뿌려주고이벤트를수신해해당하는위젯에전달하거나포커스를이동하는등의다양한 UI 활동을담당하는것이액티비티라고할수있다. Activity.setContentView() 는액티비티의콘텐트를 XML로구성된레이아웃리소스로부터지정해주는메소드다. 이메소드를활용해서 XML 파일을 res/layout 디렉토리에추가하기만하면, 해당 XML 레이아웃을디스플레이하는액티비티를호출할수있을것이다. 이것이지금부터만들어볼 UI 테스트애플리케이션의필요성과요구사항이다. 우선이클립스에서새로운안드로이드프로젝트를생성한다. - 프로젝트이름 : Maso02 < 그림 2> 위젯다이어그램 - 패키지이름 : com.kmaru.maso02 - 액티비티이름 : UITest - 애플리케이션이름 : UserInterfaces < 리스트 1> UITest.java package com.kmaru.maso02; import java.lang.reflect.field; < 그림 3> 레이아웃다이어그램 import android.app.listactivity; import android.content.intent; import android.os.bundle; import android.view.view; import android.widget.arrayadapter; import android.widget.listview; XML 기반 GUI 설계뷰와뷰그룹클래스, 이클래스들을상속하는위젯과레이아웃의관계에대해이해했다면마침내안드로이드 GUI 정복의첫걸음을뗀것이다. 이제남은일은 < 그림 2> 와 < 그림 3> 에서보듯이 public class UITest extends ListActivity { public void oncreate(bundle icicle) { super.oncreate(icicle); m a s o 227
5 _ GUI 설계, 위젯과레이아웃 intent.putextra( layout, layout); // R.layout 클래스의모든필드명을문자열배열로생성 Field[] fields = R.layout.class.getFields(); String[] examples = new String[fields.length]; for (int i=0; i<fields.length; i++) { examples[i] = fields[i].getname(); // 리스트뷰의어댑터지정 ArrayAdapter<String> adapter = new ArrayAdapter<String> ( this, android.r.layout.simple_list_item_1, examples); setlistadapter(adapter); protected void onlistitemclick(listview l, View v, int position, long id) { // 리스트아이템이선택되었을때새로운액티비티실행 // 이때인텐트에레이아웃의이름을넘겨준다. String layout = (String)l.obtainItem(position); Intent intent = new Intent(UITest.this, UIView.class); intent.putextra("layout", layout); startactivity(intent); super.onlistitemclick(l, v, position, id); UITest 액티비티는 ListActivity를상속하는액티비티로정의했다. 리스트액티비티는리스트뷰를포함하고있는데, setlistadapter() 메소드를통해리스트뷰에데이터를바인딩할수있다. 여기서는다음과같이자바리플렉션을통해 R.layout 클래스에속하는모든필드의이름을가져와서 ArrayList를구성한후에리스트뷰의데이터로넘겨줬다. startactivity(intent); startactivity() 메소드는새로운액티비티를실행해주는메소드다. 파라미터로앞에서생성한인텐트를넘겨주면액티비티매니저가액티비티스택에새로운액티비티를추가한다. 이때주의할점은 UIView라는이름의액티비티가 AndroidManifest. xml 파일에추가되어있어야한다는점이다. <activity class=.uiview android:label= @string/app_name > </activity> 레이아웃뷰어액티비티만들기 UIView는 < 리스트 2> 에서와같이 Activity 클래스를상속하는액티비티로정의한다. 액티비티가생성될때 oncreate() 메소드가호출될것이므로, 여기서레이아웃의값을가져온다. 레이아웃의값을가져오기위해서는이액티비티를실행시킨인텐트에서 getextra() 메소드를호출해저장된레이아웃문자열을가져오면된다. getintent() 는액티비티가수신한인텐트를가져오는메소드다. Intent intent = getintent(); String layout = (String)intent.getExtra( layout ); getlayoutvalue() 메소드는자바리플렉션을이용해서 R.layout 클래스의필드명으로해당필드의정수값을리턴받도록구현한메소드다. 이메소드를통해전달받은레이아웃리소스값을 setcontentview() 메소드의인자로넘겨주어레이아웃리소스를지정해주면된다. setcontentview(getlayoutvalue(layout)); Field[] fields = R.layout.class.getFields(); onlistitemclick() 메소드는리스트뷰의특정아이템이선택되었을때발생하는이벤트리스너다. 여기서는리스트뷰의 obtainitem() 메소드로해당레이아웃문자열을가져와서새로생성한인텐트객체에 putextra() 메소드로레이아웃문자열을추가했다. Intent intent = new Intent(UITest.this, UIView.class); < 리스트 2> UIView.java package com.kmaru.maso02; import java.lang.reflect.field; import android.app.activity; import android.content.intent; import android.os.bundle; public class UIView extends Activity { 228 m a s o
protected void oncreate(bundle icicle) { super.oncreate(icicle); // 수신된인텐트에서레이아웃의이름을가져온다. Intent intent = getintent(); String layout = (String)intent.getExtra("layout"); // 인텐트에서지정한레이아웃을콘텐트뷰로설정 setcontentview(getlayoutvalue(layout)); private int getlayoutvalue(string name) { // R.layout 클래스에서필드명을검색해값을리턴 Field[] fields = R.layout.class.getFields(); for (int i=0; i<fields.length; i++) { if (fields[i].getname().equals(name)) { try { return fields[i].getint(fields[i]); catch (Exception e) { showalert(" 알림 ", e.getmessage(), " 확인 ", false); return -1; XML 기반 GUI 에친숙해지기 이제모든준비가끝났다. 남은것은 < 그림 2> 와 < 그림 3> 에나오는모든위젯과레이아웃들을안드로이드 API 문서를참조하면서 XML 파일로하나씩만들어보는것이다. 만들어진 XML 파일을 res/layout 디렉토리에추가하기만하면 < 화면 1> 에서보듯이추가된 XML 파일을우리가만든테스트애플리케이션에서확인할수있다 ( 이것은독자여러분들의몫으로남겨둔다 ). 을열어아무기능이나실행해보면, 이런화면의 UI 기능을안드로이드의기본위젯과레이아웃만으로만들수없음을느낄수있다. 그렇다면이제커스텀뷰를생성하는법을알아보도록하자. 커스텀뷰만들기일반적으로커스텀뷰는위젯이나레이아웃을상속할수도있지만, 여기서는뷰의최상위베이스클래스인 View 클래스를상속받도록하자. public class CustomView extends View { View 클래스를상속하면생성자를오버라이딩해야한다. setfocusable() 메소드는뷰가사용자의키이벤트를수신해처리할수있도록포커스를주는메소드이다. public CustomView(Context context, AttributeSet attrs, Map inflateparams) { super(context, attrs, inflateparams); //... setfocusable(true); View 클래스를상속했으므로, 현재는자체적으로화면에아무것도디스플레이하지않는다. 화면에무엇인가를뿌려주기위해서는 ondraw() 메소드를오버라이딩해직접디스플레이에그려줘야한다. ondraw() 의파라미터로넘겨받은 Canvas 객체는그래픽스처리를위한그리기관련함수를모두포함하고있다. 여기서는안드로이드아이콘이미지를지정된좌표에뿌려주도록구현했다. protected void ondraw(canvas canvas) { super.ondraw(canvas); < 화면 1> 다양한레이아웃테스트 커스텀뷰와그래픽스비록재사용가능한다양한기본위젯과레이아웃이있다고하더라도, 커스텀컴포넌트를만들어야할일은매우많다. 특히게임을만들기위해서는직접그래픽스를제어할수있는자신만의뷰를별도로생성하는것이일반적이다. 또한지금한번휴대폰 //... icon.setbounds(left, top, right, bottom); icon.draw(canvas); 이벤트처리키이벤트의처리는 onkeydown() 이벤트를오버라이딩해구 m a s o 229
5 _ GUI 설계, 위젯과레이아웃 현하면된다. 키값은 keycode 파라미터를통해넘겨받는다. 현재는키입력이있으면아이콘의이동방향을설정할수있도록구현했다. 미지로지정해주었다. android:background= @drawable/background_01 public boolean onkeydown(int keycode, KeyEvent event) { switch (keycode) { case KeyEvent.KEYCODE_DPAD_LEFT: direction = GO_LEFT; 실행결과는 < 화면 2> 와같다. 키입력에따라안드로이드아이콘이화면안에서움직이도록구현된것을알수있다. 디스플레이가변경되었을때뷰를다시그리기위한방법은 invalidate() 메소드를호출하는것이다. invalidate() 메소드는현재뷰를무효화시키고화면에다시그리도록 draw() 를호출한다. invalidate() 는 UI 스레드내에서만호출할수있으므로다른스레드에서 UI 스레드의뷰를다시그리기위해서는 post Invalidate() 메소드를호출해주어야한다. public void run() { while (true) { switch (direction) { case GO_LEFT: if (currentx - SPEED >= 0) currentx -= SPEED; //... postinvalidate(); < 리스트 3> 은여기서구현한 CustomView 클래스의전체소스다. 이커스텀뷰를 < 리스트 4> 에서와같이 custom_view_01.xml 파일에추가하도록하자. 기본위젯과마찬가지로커스텀뷰도클래스명과같이레이아웃파라미터를사용할수있다. <com.kmaru.maso02.customview android:layout_width= fill_parent android:layout_height= fill_parent /> 여기서만든커스텀뷰는 FrameLayout의하위뷰로추가되었고, 프레임레이아웃의백그라운드이미지를 background_01 이 < 화면 2> 커스텀뷰의실행 < 리스트 3> CustomView.java package com.kmaru.maso02; import java.util.map; import android.content.context; import android.graphics.canvas; import android.graphics.drawable.drawable; import android.util.attributeset; import android.view.keyevent; import android.view.view; public class CustomView extends View implements Runnable { public static final int SPEED = 1; public static final int STOP = 0; public static final int GO_LEFT = 1; public static final int GO_RIGHT = 2; public static final int GO_UP = 3; public static final int GO_DOWN = 4; Drawable icon; private int currentx; private int currenty; private int direction; public CustomView(Context context, AttributeSet attrs, Map inflateparams) { super(context, attrs, inflateparams); icon = getresources().getdrawable(r.drawable.android_icon_small); currentx = currenty = 0; 230 m a s o
{ direction = STOP; Thread thread = new Thread(this); thread.start(); setfocusable(true); protected void ondraw(canvas canvas) { super.ondraw(canvas); int left = currentx; int top = currenty; int right = left + icon.getintrinsicwidth(); int bottom = top + icon.getintrinsicheight(); // canvas.drawcolor(color.black); icon.setbounds(left, top, right, bottom); icon.draw(canvas); public boolean onkeydown(int keycode, KeyEvent event) SPEED SPEED currentx -= SPEED; case GO_RIGHT: if (currentx + icon.getintrinsicwidth() + <= this.getwidth()) currentx += SPEED; case GO_UP: if (currenty - SPEED >= 0) currenty -= SPEED; case GO_DOWN: if (currenty + icon.getintrinsicheight() + <= this.getheight()) currenty += SPEED; try { Thread.sleep(50); catch (InterruptedException e) { e.printstacktrace(); switch (keycode) { case KeyEvent.KEYCODE_DPAD_LEFT: direction = GO_LEFT; case KeyEvent.KEYCODE_DPAD_RIGHT: direction = GO_RIGHT; case KeyEvent.KEYCODE_DPAD_UP: direction = GO_UP; case KeyEvent.KEYCODE_DPAD_DOWN: direction = GO_DOWN; case KeyEvent.KEYCODE_DPAD_CENTER: direction = STOP; return super.onkeydown(keycode, event); postinvalidate(); < 리스트 4> custom_view_01.xml <?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com /apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="@drawable/background_01" > <com.kmaru.maso02.customview android:layout_width="fill_parent" android:layout_height="fill_parent" /> </FrameLayout> public void run() { while (true) { switch (direction) { case STOP: case GO_LEFT: if (currentx - SPEED >= 0) 지금까지안드로이드플랫폼의 GUI 구조에대해살펴봤다. 기존휴대폰자체의 UI 프로그래밍이나브루, 위피, 자바ME 기반의프로그래밍경험이있는독자들이라면아마안드로이드플랫폼의 GUI 구조가프로그래밍하기에매우편리하다는점에충분히감동했을것이라믿는다. m a s o 231