2014년 8월 12일 화요일

안드로이드 019. Menu 구현하기




1. 안드로이드 Menu 종류

안드로이드 메뉴는 현재 화면에 보이는 Activity가 제공하는 Option Menu
Activity내의 개별 view들이 제공할 수 있는 Context Menu 두 가지가 있다.


A. Option Menu
현재 작동중인 Activity를 위한 menu이며 디바이스의 menu키를 누르면 나타난다.
Option menu는 처음 option menu를 호출 시 activity:onCreateOptionMenu(Menu) callback에서 초기화된다. onCreateOptionMenu(Menu)는 해당 activity가 생성돼서 처음 option menu가 호출 됐을 때 한번만 실행된다.
public boolean onCreateOptionsMenu (Menu menu)
parameter
menu: 시스템에 의해 생성된 menu 객체. 이 객체에 Menu:add(…) 메소드, MenuInflater 등을 사용하여 menu item을 추가함
return
true: 메뉴가 화면에 표시됨
false: 메뉴가 화면에 표시되지 않음

onCreateOptionMenu(Menu)를 통해 한번 초기화된 option menu의 아이템을 추가/삭제 하거나, 설정을 변경 (보이지 않게 설정하는 것 등) 하려면 option menu가 화면에 표시되기 바로 전에 항상 호출 되는 Activity:onPrepareOptionMenu(Menu) callback을 사용 한다.
public boolean onPrepareOptionsMenu (Menu menu)
parameter
menu: 본 메소드를 호출한 Activity의 option menu이다. 이 메소드가 완료되면 화면에 보여질 메뉴 객체임으로 화면에 보여지기 전에 변경 작업이 필요하면 이 메소드 내부에서 수정
return
true: 메뉴가 화면에 표시됨
false: 메뉴가 화면에 표시되지 않음


Option Menu는 다음 두 가지 menu로 구분 된다.
Icon Menu
디바이스의 menu 버튼을 눌렀을 때 안드로이드 스크린 밑 쪽에서부터 올라오며, 최대 여섯 개까지의 text + icon(옵션) 형태의 메뉴를 표시한다. option menu의 총 항목 개수가 6개를 초과하면 마지막 6번째 icon menu 버튼은 more로 표시된다.

Expanded Menu
option menu의 총 항목 개수가 6개를 초과해서 마지막 6번째 icon menu 버튼이 more로 표시되었을 때 이를 사용하면 나오는 text기반 list형식의 메뉴이다.



B. Context Menu
Activity 내부의 view가 제공할 수 있는 text기반의 list 메뉴이며, context menu를 제공하는 view를 약 2초 정도 터치&홀드 하면 화면 중앙에 출력된다. Activity에 포함된 모든 view가 개별적인 context menu를 가질 수 있음으로 여러 개의 context menu를 구현 해야 하는 경우도 있다. 예를 들어 TextView기반 view의 context menu는 text의 size에 대한 menu를 제공 하고, ImageView기반 view의 context menu는 image에 tint를 입히도록 menu를 구성 할 수 있다. (포스트 마지막의 예제 참조)

먼저 특정 view가 Context Menu를 제공하게 하려면 Context Menu 제공 view임을 Activity에 알려야 하는데, Activity:onCreate(Bundle) callback 내부에서 Activity:registerForContextMenu(View) 메소드를 사용한다.
public void registerForContextMenu (View view)
parameter
view: Context Menu를 제공할 view를 지정

그 다음 context mmenu의 초기화는 Activity:onCreateContextMenu(...) callback 내부에서 한다. Context Menu는 Option Menu와는 다르게 화면에 표시될 때 마다 항상 onCreateContextMenu(…)가 실행된다. (Option menu는 초기에만 한번 onCreateOptionMenu가 실행되고 그 이후에 option menu가 필요할 때는 onPrepareOptionMenu가 호출됨)
public void onCreateContextMenu (ContextMenu menu,
                                              View v,
                                              ContextMenu.ContextMenuInfo menuInfo)
parameters
menu: 시스템에 의해 생성된 Context Menu 객체이며, 이 객체에 add(…) 메소드나 MenuInflator를 이용하여 menu아이템을 추가 하는 등의 작업을 함
v: Context Menu를 소유한 view 인스턴스
menuInfo: context menu가 표현할 menu item에 대한 추가 정보를 전달 하는 역할을 하며, 어떤 종류의 view가 context menu를 호출 했는지에 따라 내용이 다름

다음 그림은 2개의 다른 view에 각각의 Context Menu가 구현된 예이다.







2. Menu 초기화 하기 (Populating Menu)

안드로이드에서는 system이 제공(callback 메소드의 인자로)하는 빈 Menu 객체에 Submenu와 Menu item을 자유롭게 추가해 원하는 형태의 메뉴를 생성한다.

Menu item은 메뉴에서 사용자가 조작할 수 있는 menu요소를 말하며,

Submenu는 어떠한 기준(기능, 목적 등)을 바탕으로 그룹화된 메뉴 아이템들을 수용하는 컨테이너 역할을 한다. 예를 들어 PC어플리케이션의 경우 보통 File, Edit등의 메뉴가 자신의 이름과 관련 있는 child 메뉴들(new, save, save as 등등)를 포함하는데, 이 File, Edit가 안드로이드의 Submenu에 해당한다. 서브메뉴는 Option menu 또는 Context menu에서 모두 사용 가능하나, submenu 가 또 다른 submenu를 포함하는 구조는 허락되지 않는다.


다음은 TextView가 보여주는 Context Menu의 아이템을 submenu로 구성한 예이다.
그림에서는 3개의 텍스트 크키에 관한 menu item 3개가 그룹을 이뤄 'Text Size 설정 >' submenu를 형성하고 있다.



안드로이드는 Menu 초기화를 위해
Menu:add(…) 메소드를 이용하는 방법
XML & MenuInflater를 이용하는 방법을 제공한다.

A. Menu:add(…) method를 이용한 메뉴 초기화
이 방법은 onCreateOptionMenu(Menu) 나 onCreateContextMenu(Menu)에서 인자로 제공된 Menu객체와 Menu:add(…)/addSubmenu(…) 메소드를 이용해 소스코드 내부에서 직접 메뉴를 디자인하는 방법이다. 프로그램 로직과 디자인이 합쳐져 코드가 복잡해지는 단점이 있어 권장되지 않는다.
Menu 객체는 여러 형태로 오버로딩 된 add(…) 메소드를 제공하는데, 그 중 가장 많이 사용되는 메소드는 다음과 같다.
abstract MenuItem add(int groupId, int itemId, int order, CharSequence title)
parameters
groupId: 메뉴 아이템 그룹을 int 형으로 지정하며, 같은 그룹으로 묶인 item들은 Menu클래스의 그룹 속정 지정 메소드(ex. Menu:setGroupCheckagle(), setGroupEnabled(), setGroupVisible() 등)를 적용 가능하다. 그룹 지정을 하지 않으려면 pre-define된 상수 Menu:NONE이나 숫자 0을 전달하는 것이 일반적이다.
itemId: 코드 상에서 메뉴 아이템간의 구분을 위한 구분자(identifier). 당연히 아이템 마다 유일한 0-based integer를 부여해야 한다.
order: 메뉴 아이템이 표시될 순서를 지정. 0이 가장 처음 표시되며 숫자가 클수록 나중에 표시됨. 모두 같은 숫자로 지정되면 메뉴에 추가된 아이템 순으로 보여짐.
title: 메뉴 아이템이 표현할 text값을 지정.
return
생성한 MenuItem 객체를 리턴


또, Menu클래스는 SubMenu 추가를 위해 다음과 같은 메소드도 제공한다.
(일반적으로 submenu는 이벤트 처리가 필요 없기 때문에 addSubMenu(CharSequence title)도 많이 사용된다)
abstract SubMenu addSubMenu(int groupId, int itemId, int order, CharSequence title)
parameters
위에 설명한 MenuItem add(…) 메소드의 parameter와 동일
return
생성한 SubMenu 객체를 리턴


그럼 위의 메소드가 onCreate…Menu(Menu) callback내부에서 어떻게 사용되는지 살펴보자.
소스 펼치기


위와 같이 생성된 option menu는 실행 시 다음과 같은 형태로 표현된다.
Blue, Green, Red는 item이고 Size는 Submenu임으로 추가의 아이템을 화면에 표시한다.



B. XML과 MenuInflator를 이용한 메뉴 초기화
이 방법은 XML 기반의 문서(\res\menu\ 폴더에 저장 됨)에 메뉴의 형태를 기술 하고, 소스 코드 내부에서 XML 파일을 inflate하여 메뉴의 초기 모양을 구성하는 방법이다. 어플리케이션 로직과 디자인을 분류함으로 간결한 코드를 만들 수 있음으로 권장되는 방식이다. 또, menu XML 문서는 메뉴 생성 visual tool (eclipse ADT plug-in도 제공하는)들을 사용해 쉽게 만들거나 수정 할 수 있다는 장점도 있다.

menu XML문서는 다음 세 가지 element를 이용하여 작성 한다.
<menu>
menu xml 문서의 root element이다. <item>과 <group> element를 포함(nest)할 수 있으며, submenu구성을 위해 <item> element 밑에 포함되기도 한다. 관련 attribute는 없다.

<group>
item들을 그룹으로 묶기 위해 사용되는 element이며, <item> element를 포함한다. 지원 attribute는 다음과 같다.
  • android:id – 그룹별로 유일한 구분자(identifier)를 설정하는데 사용.
  • android:menuCategory – 그룹의 우선순위를 지정한다. 사용 할 수 있는 값은 alternative, secondary, container, system (이상 높은 우선순위 순서 정렬)이며, 각각의 값은 Menu클래스에 pre-define된 상수 CATEGORY_ALTERNATIVE, CATEGORY_SECONDARY, CATEGORY_CONTAINER, CATEGORY_SYSTEM에 대응한다. 또, 같은 category에 속하고 item 별 우선 순위가 부여되지 않은 item들은 선언된 순서대로 표현된다. (왜 이런 이름을 사용해 우선 순위를 정했는지 알 수 없음. 단지 코드에서 test해본 결과 이런 순서로 메뉴가 표시됨, 또 이 attribute를 지정하지 않은 group이 최 우선 순위로 배치됨. 결국 category없음 > alertnative > secondary > container > system로 우선순위가 결정됨)
  • android:orderInCategory – 같은 category 안에서의 우선순위를 0-based integer로 설정.
  • android:checkableBehavior – group 내부의 item들이 check가능한지 설정. 유효 값은 none, all(checkbox처럼 각 아이템이 동시에 check될 수 있는 그룹), single(radio button처럼 한번에 하나만 check되는 그룹)
  • android:visible – 그룹의 시각적 활성화 여부를 결정. 유효 값은 truefalse
  • android:enabled – 그룹의 활성화 여부를 결정. 유효 값은 truefalse

<item>
item은 메뉴에서 사용자가 선택 가능한 element를 나타낸다. group, menu(submenu를 구성하기 위해) element를 포함 할 수 있다. 다음은 지원하는 attribute들 이다.
  • android:id – item별 유일한 구분자(identifier)를 설정.
  • android:menuCategory - <group> element의 menuCategory와 동일.
  • orderInCategory - 같은 category 안에서의 item간 우선순위를 0-based integer로 설정.
  • android:title – item이 화면에 보여지는 이름을 설정.
  • android:titleCondensed – 일반 타이틀이 너무 길어 화면에 온전히 표시 할 수 없을 경우 대신 사용되는 간결한 title. Option Menu는 titie대신 이 속성을 사용한다.
  • android:icon – item의 icon을 설정. (예. "@drawable/icon" )
  • android:alphabeticShortcut – item의 쇼트키를 영문자 중 지정. 유효 값은 영문 알파벳 대소문자 구분 없이 한 글자\n(enter키), \b(delete키).
  • android:numericShortcut – item의 쇼트키를 숫자 중 지정. 유효 값은 0~9.
  • android:checkable – item이 check가능한지 설정. 유효 값은 truefalse.
  • android:checked – 메뉴가 처음 표시될 때 item을 check상태로 표시 할 지 여부 지정. 유효 값은 truefalse.
  • android:visible – 메뉴 item의 시각적 활성화 여부 지정. 유효 값은 truefalse.
  • android:enable – 메뉴 item의 활성화 여부 지정, 유효 값은 truefalse


설명한 3가지 element를 이용하여 A. Menu:add(…) method를 이용한 메뉴 초기화에서 구현했던 것과 같은 메뉴를 구성해보자.
my_menu.xml
소스 펼치기

완성된 my_menu.xml을 \res\menu 폴더에 저장하고 java코드의 onCreateOptionMenu(Menu)에서 다음과 같이 inflate 한다. 코드상에서 직접 메뉴를 디자인 했을 때보다 코드가 훨씬 간결해 졌음을 확인 할 수 있다.
XML inflating code
소스 펼치기


실행 시켜보면, add() 메소드를 사용해 코드 내부에서 menu를 디자인 했을 때와 완전히 동일한 메뉴가 생성된다.



  



3. Menu item 꾸미기

A. Icon 추가

icon은 Option Menu의 icon menu 에서만 사용 가능하다. (Option menu의 Expanded menu에서도 icon표시 안됨)
자바 코드에서의 설정 예
소스 펼치기


XML에서의 설정 예
소스 펼치기


다음 Icon을 적용한 Option Menu 이다.



B. 단축키 지정

Option menu의 item에는 알파벳 또는 숫자로 단축키를 지정할 수 있다. (Context  Menu는 단축키 지정 불가능) Expanded Menu의 경우 기본으로 "menu+단축키" 정보를 titie 밑에 표시하여 해당 item의 단축키 정보를 알려준다. 반면 Icon Menu영역에서는 기본적으로 단축키 정보가 표시되지 않으며 menu키를 누르고 홀드하면 item title과 단축키 정보를 번갈아 가면서 보여진다.

*확인할 사항* (혹시 글을 보시는 분 중 아시는 분이 계시면 뎃글 꼭 부탁 드립니다.)
  • AVD에서 알파벳 단축키는 ALT+단축키 입력으로 입력된다.
  • 숫자 단축키는 12 key (핸드폰 키패드) 키보드를 이용해 입력가능(API Reference의 MenuItem:setNumerticShortkey() 참조) 하다고 나와있는데 AVD에서는 입력 방법을 찾지 못했다.
  • 알파벳 단축키는 단축키 정보를 보여주지만 숫자 단축키는 단축키 정보를 보여주지 않는다.
  • Menu:setQwertyMode(boolean)를 사용해도 변화 없음.

자바 코드에서의 설정 예
소스 펼치기


XML에서의 설정 예
소스 펼치기


단축키를 적용한 Option Menu 이다. (Icon Menu의 경우 menu버튼을 홀드 하고 있으면 단축키 정보를 보여준다)



C. Menu group지정

기능, 성격별로 구분된 메뉴 item들을 그룹으로 묶어 자바 코드나 XML문서에서 다음 속성들을 지정 할 수 있다.
  • Menu:setGroupCheckable() / android:checkableBehavior
  • Menu:setGroupVisibable() / android:visible
  • Menu:setGroupEnabled() / android:enabled

자바 코드에서의 설정 예
소스 펼치기


XML에서의 설정 예
소스 펼치기


위의 xml을 Activity:onCreateContextMenu에서 inflate 시키면 자바 코드 내부에서 구현한 위 예제와 동일한 결과를 얻는다.




4. Menu 사용 이벤트 처리

Menu에서 발생하는 item 선택 이벤트는 어떤 메뉴에서 이벤트가 발생했느냐에 따라
다음과 같은 두 개의 callback에서 따로 처리한다.


A. Option Menu의 item이 발생하는 이벤트 처리

다음과 같은 Activity 클래스가 제공하는 callback 메소드를 오버라이딩하여 이벤트를 처리한다.
public boolean onOptionsItemSelected(MenuItem item)
parameter
item: Option Menu객체 내부에 등록된 MenuItem 인스턴스 중 선택 이벤트를 발생 시킨 MenuItem인스턴스를 전달.
return
true: 본 메소드에서 이벤트가 처리됐음을 뜻함.
false: 본 메소드에서 처리 되지 못한 이벤트를 일반적인 방법(item이 포함한 Runable객체를 call하던가, 적절한 이벤트 handler로 메시지 전송)을 이용해 처리함.


일반적으로, switch~ case~ 문을 이용하여 선언된 Item id와 인자로 전달된 item인스턴스의 id를 비교 하여 어떤 item인지를 판단 후 그에 맞는 처리 루틴을 구현한다.
switch case 를 이용한 메뉴 사용 이벤트 처리 예 
소스 펼치기





B. Context Menu의 item이 발생하는 이벤트 처리

Context Menu의 Item은 다음의 Activity 클래스가 제공하는 callback을 오버라이딩 하여 이벤트를 처리한다.
public boolean onContextItemSelected (MenuItem item)
parameter
item: Context Menu객체 내부에 등록된 MenuItem 인스턴스 중 이벤트를 발생 시킨 MenuItem인스턴스를 전달.
return
true: 본 메소드에서 이벤트가 처리됐음을 뜻함.
false: 본 메소드에서 처리 되지 못한 이벤트를 일반적인 방법(item이 포함한 Runable객체를 call하던가, 적절한 이벤트 handler로 메시지 전송)을 이용해 처리함.


onContextItemSelected(…) 메소드에서도 switch~case~ 명령을 사용해 이벤트를 처리하는 것이 일반적이다.




5. Android Menu 예제
그럼 지금까지 설명한 내용을 바탕으로 예제를 파악해보자.
예제는 2개의 view(TextView, ImageView)로 구성된 Activity이며
Activity는 option menu를 제공하고 각 view는 view성경에 맞는 context menu를 제공한다.



A. Menu 예제 1 - Menu:add(…) method이용

main.xml
소스 펼치기

MyMenuUsingCode.java
소스 펼치기




B. Menu 예제 2 - XML & MenuInflator 이용

main.xml
소스 펼치기

option_menu.xml
소스 펼치기

context_menu_tv.xml
소스 펼치기


context_menu_iv.xml
소스 펼치기

  
MyMenuUsingXMLInflater
접기
001package com.holim.test;
002 
003import android.app.Activity;
004import android.graphics.Color;
005import android.graphics.PorterDuff;
006import android.graphics.Typeface;
007import android.os.Bundle;
008import android.view.ContextMenu;
009import android.view.Menu;
010import android.view.MenuInflater;
011import android.view.MenuItem;
012import android.view.View;
013import android.widget.ImageView;
014import android.widget.TextView;
015 
016public class MyMenuUsingXMLInflater extends Activity {
017     
018    TextView tv;
019    ImageView iv;
020     
021    /** Called when the activity is first created. */
022    @Override
023    public void onCreate(Bundle savedInstanceState) {
024        super.onCreate(savedInstanceState);
025        setContentView(R.layout.main);
026         
027        tv = (TextView)findViewById(R.id.textView);
028        iv = (ImageView)findViewById(R.id.imageView);
029         
030        iv.setImageResource(R.drawable.tiger);
031         
032        // tv, iv가 context menu를 제공하도록 설정
033        // view 영역을  터치하고 hold하면(약 2초) context menu 호출
034        registerForContextMenu(tv);
035        registerForContextMenu(iv);
036    }
037     
038    // Option menu에 아이템 채움.
039    // Option menu 호출 시 한번말 실행됨.
040    @Override
041    public boolean onCreateOptionsMenu(Menu menu) {
042        boolean result = super.onCreateOptionsMenu(menu);
043                 
044        new MenuInflater(this).inflate(R.menu.option_menu, menu);
045                 
046        return result;
047    }    
048     
049    // Context menu에 item 채움.
050    // Context menu 호출 시 마다 실행됨.
051    @Override
052    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
053         
054        // context menu를 호출시킨 view에 따라 다른 menu 생성
055        if (v == iv) {
056            // context_menu_iv.xml을 menu로 inflate
057            getMenuInflater().inflate(R.menu.context_menu_iv, menu); 
058  
059            menu.setHeaderIcon(R.drawable.tiger);
060            menu.setHeaderTitle("ImageView의 Context Menu");
061        }
062        else if (v == tv) {
063            // context_menu_tv.xml을 menu로 inflate
064            getMenuInflater().inflate(R.menu.context_menu_tv, menu);       
065        }
066    }
067     
068    // Option menu가 화면에 표시되기 전 항상 호출됨
069    // 한번 초기화된 Option Menu의 내용을 동적으로 바꾸거나 할때 사용.
070    @Override
071    public boolean onPrepareOptionsMenu(Menu menu) {
072        boolean result = super.onPrepareOptionsMenu(menu);
073        return result;
074    }
075     
076    // Option menu item이 선택되었을때 발생한는 event의 handler
077    // 발생한 event가 이 메소드나 super 클래스의 원래 메소드에서 처리 되지 않으면 false리턴
078    @Override
079    public boolean onOptionsItemSelected(MenuItem item) {
080         
081        switch (item.getItemId()) {
082        case R.id.ID_COLOR_RED:
083            tv.setTextColor(Color.RED);
084            return true;
085        case R.id.ID_COLOR_GREEN:
086            tv.setTextColor(Color.GREEN);
087            return true;
088        case R.id.ID_COLOR_BLUE:
089            tv.setTextColor(Color.BLUE);
090            return true;
091        case R.id.ID_STYLE_NORMAL:
092            tv.setTypeface(Typeface.DEFAULT, Typeface.NORMAL);
093            item.setChecked(true);
094            return true;
095        case R.id.ID_STYLE_BOLD:
096            tv.setTypeface(Typeface.DEFAULT, Typeface.BOLD);
097            item.setChecked(true);
098            return true;
099        case R.id.ID_STYLE_ITALIC:
100            tv.setTypeface(Typeface.DEFAULT, Typeface.ITALIC);
101            item.setChecked(true);
102            return true;   
103        }
104        return (super.onOptionsItemSelected(item));
105    
106     
107    // Context menu의 item 선택 시 발생하는 event의 handler
108    @Override
109    public boolean onContextItemSelected(MenuItem item) {
110         
111        switch (item.getItemId()) {
112        case R.id.ID_SIZE_10SP:
113            tv.setTextSize(10);
114            return true;
115        case R.id.ID_SIZE_20SP:
116            tv.setTextSize(20);
117            return true;
118        case R.id.ID_SIZE_30SP:
119            tv.setTextSize(30);
120            return true;
121        case R.id.ID_IMAGE_TINT_NORMAL:
122            iv.clearColorFilter();
123            return true;
124        case R.id.ID_IMAGE_TINT_RED:
125            iv.clearColorFilter();
126            iv.setColorFilter(Color.RED,  PorterDuff.Mode.LIGHTEN);
127            return true;
128        case R.id.ID_IMAGE_TINT_BLUE:
129            iv.clearColorFilter();
130            iv.setColorFilter(Color.BLUE,  PorterDuff.Mode.LIGHTEN);
131            return true;
132        }
133        return (super.onOptionsItemSelected(item));
134    }   
135}
접기


두 예제의 구현 방법은 다르지만 완전히 같은 결과를 보여준다.




  예제 프로젝트 소스 다운로드


댓글 없음:

댓글 쓰기