네비게이션 드로어(Navigation Drawer) 사용하기
예전에 슬라이딩 메뉴 라이브러리를 추천하는 글을 쓴 적이 있다. 이 글을 쓴지 벌써 거의 3년이 다 되어간다. 그 3년 동안 안드로이드 개발에는 많은 변화가 있어왔다. 안드로이드도 버전업이 많이 되었고, 구글은 안드로이드 앱들의 디자인을 자사의 마테리얼(Material) 디자인으로 통일하기 위한 궁리를 많이 해왔다.
슬라이딩 메뉴는 네비게이션 드로어(Navigation Drawer)라는 이름으로 구글의 마테리얼 디자인 가이드에 포함되었으며, 이를 구현하기 위해서 또 많은 이들이 라이브러리를 만들어 공개해왔다. 그러나 그 노력들이 무색하게도, 구글은 서포트 라이브러리(Support Library)에 네비게이션 드로어를 포함시켰다. 덕분에 그 후로 오픈소스 라이브러리들은 서포트 라이브러리를 참조하여 사용하기 편리하도록 코드를 바꿔주는 역할만 하는 방향으로 개발해나가기 시작했다.
하지만 개인적으로는 왠만하면 개발사에서 제공하는 방법을 직접 사용하는걸 추천하고 싶다. 추가적인 라이브러리를 설치해야 하는 부담감도 있고, 직접 다루는 편이 성능 최적화나 커스터마이징을 하기에 좀더 편리하기 때문이다.
본인의 앱 Seeko Mobile에는 이미 구글의 서포트 라이브러리만을 이용하여 네비게이션 드로어를 구현해 놨는데, 아직도 상당수의 앱에는 네비게이션 드로어가 제대로 구현되어 있지 않다. 아직까지도 예전에 내가 작성한 슬라이딩 메뉴 라이브러리 추천글을 찾는 사람들이 많은데, 새로운 “정석적인” 방법을 다들 사용했으면 하는 바람에 이렇게 글을 쓴다.
이하의 글은 구글의 레퍼런스 문서 Creating a Navigation Drawer를 번역한 것이다. 개발자들에게 도움이 되었으면 좋겠다. 일부는 이해를 위해 의역하거나 변경한 부분도 있다.
네비게이션 드로어는 화면 왼쪽에 앱의 주요 운용 선택지(navigation options)를 표시하기 위한 패널이다. 네비게이션 드로어는 일반적으로 숨겨져 있지만, 스크린의 왼쪽 모퉁이에서부터 화면 가운데로 손가락을 밀거나(swipe), 앱의 최상위 화면에서 액션바에 있는 앱 아이콘을 터치할 시에 나타난다.
이 강좌는 서포트 라이브러리에서 제공되는 DrawerLayout
API를 사용하여 어떻게 네비게이션 드로어를 사용할 것인지를 보여주고자 한다.
네비게이션 드로어 디자인
네비게이션 드로어를 앱에 적용하기 전에, 네비게이션 드로어 디자인 가이드에서 정의된 용례와 디자인 원리에 대해서 이해해야 한다.
드로어 레이아웃(Drawer Layout) 작성하기
네비게이션 드로어를 추가하려면, DrawerLayout
객체를 레이아웃 최상위 뷰(view)로써 가지는 유저 인터페이스를 선언해야 한다. DrawerLayout
안에는 각각 화면의 주요 내용을 담을 뷰(드로어가 숨겨져 있을때 보여질 내용)와 네비게이션 드로어가 될 뷰를 추가한다.
다음의 코드는 두개의 자식 뷰를 가지고 있는 DrawerLayout
을 사용하는 예제이다. FrameLayout
이 주요 내용을 담고 있고(런타임때에 Fragment
를 가져온다), ListView
가 네비게이션 드로어로써 작동한다.
이 예제 레이아웃은 중요한 특성 몇가지를 보여준다.
- 주요 컨텐츠 뷰(
FrameLayout
)는DrawerLayout
의 첫번째 자식뷰가 되어야 한다. XML의 순서는 Z축 순서를 나타내며, 드로어는 항상 최상단에 있어야 하기 때문에다. (뒤에 있을수록 나중에 그려지므로 맨 위에 나타나게 된다는 의미) - 주요 컨텐츠 뷰는 부모 뷰의 너비와 높이에 맞춰져야 한다. 왜냐면 네비게이션 드로어가 보이지 않을 때 전체 UI를 보여주는 뷰이기 때문이다.
- 드로어 뷰(
ListView
)는android:layout_gravity
속성값이 반드시 정해져야 한다. RTL(Right-To-Left) 언어를 지원하기 위해서는, 해당 속성값을"left"
대신"start"
로 설정해야 한다. (그렇게 해야 드로어는 RTL 언어에 대응하여 오른쪽에서 나타나게 된다.) - 드로어 뷰는 폭의 크기를
dp
단위로 설정해야 하며, 높이는 부모 뷰에 맞춰야 한다. 드로어 폭은 사용자가 주요 컨텐츠의 일부분을 항상 볼 수 있도록 320dp를 넘어서는 안된다.
드로어 리스트 초기화하기
Activity
에서 가장 먼저 해야하는 것 중 하나는 네비게이션 드로어의 아이템 리스트를 초기화 하는 것이다. 네가 어떻게 초기화 할 것인가는 앱의 컨텐츠에 달려있지만, 네비게이션 드로어는 보통 ListView
를 포함하고 있으므로, 목록은 Adapter
(예를 들어 ArrayAdapter
나 SimpleCursorAdapter
)를 이용해 초기화되어야 한다.
다음은 문자열 배열을 표시하는 네비게이션 리스트를 초기화 하는 방법에 대한 예시이다.
public class MainActivity extends Activity {
private String[] mPlanetTitles;
private DrawerLayout mDrawerLayout;
private ListView mDrawerList;
...
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mPlanetTitles = getResources().getStringArray(R.array.planets_array);
mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
mDrawerList = (ListView) findViewById(R.id.left_drawer);
// Set the adapter for the list view
mDrawerList.setAdapter(new ArrayAdapter(this, R.layout.drawer_list_item, mPlanetTitles));
// Set the list's click listener
mDrawerList.setOnItemClickListener(new DrawerItemClickListener());
...
}
}
이 코드는 또한 setOnItemClickListener()
를 호출하여 네비게이션 드로어에 대한 클릭 이벤트를 받을 수 있게 하고 있다. 다음 차례로, 이 인터페이스를 상속하여 사용자가 목록 아이템을 선택했을 때 컨텐츠 뷰를 변경하는 방법에 대해서 보여주고자 한다.
네비게이션 클릭 이벤트를 다루기
사용자가 드로어 목록의 아이템을 선택했을 때, 안드로이드 시스템은 setOnItemClickListener()
에 의해 주어진 OnItemClickListener
객체의 onItemClick()
함수를 호출한다.
onItemClick() 함수에서 무엇을 해야하는가는 앱의 구조가 어떻게 만들어져 있는가에 따라 달라진다. 다음의 예제는 네비게이션 아이템을 선택했을 때, 주요 컨텐츠 뷰에 클릭한 아이템에 따라 다른 Fragment
를 삽입하는 것을 보여주고 있다. (FrameLayout
은 R.id.content_frame
의 아이디값으로 설정되어 있다.)
private class DrawerItemClickListener implements ListView.OnItemClickListener {
@Override
public void onItemClick(AdapterView parent, View view, int position, long id) {
selectItem(position);
}
}
// Swaps fragments in the main content view
private void selectItem(int position) {
// Create a new fragment and specify the planet to show based on position
Fragment fragment = new PlanetFragment();
Bundle args = new Bundle();
args.putInt(PlanetFragment.ARG_PLANET_NUMBER, position);
fragment.setArguments(args);
// Insert the fragment by replacing any existing fragment
FragmentManager fragmentManager = getFragmentManager();
fragmentManager.beginTransaction()
.replace(R.id.content_frame, fragment)
.commit();
// Highlight the selected item, update the title, and close the drawer
mDrawerList.setItemChecked(position, true);
setTitle(mPlanetTitles[position]);
mDrawerLayout.closeDrawer(mDrawerList);
}
@Override
public void setTitle(CharSequence title) {
mTitle = title;
getActionBar().setTitle(mTitle);
}
열기/닫기 이벤트 감지하기
드로어가 열리고 닫힐 때 발생하는 이벤트를 감지하기 위해, DrawerLayout
의 setDrawerListener()
를 호출하여, DrawerLayout.DrawerListener
를 상속한 객체를 변수로 넘겨주어야 한다. 이 인터페이스는 onDrawerOpened()
와 onDrawerClosed()
와 같은 드로어 이벤트에 대한 콜백 함수를 제공하고 있다.
그러나, 만약에 액션 바를 사용하고 있다면, DrawerLayout.DrawerListener
를 상속하는 것 대신, ActionBarDrawerToggle
클래스를 확장하여 사용하는 것이 좋다. ActionBarDrawerToggle
클래스는 DrawerLayout.DrawerListener
를 상속하고 있으므로, 앞서 말한 콜백함수들을 여전히 오버라이드하여 사용할 수 있다. 또한, 해당 클래스는 액션 바 아이콘과 네비게이션 드로어 사이의 적절한 상호작용을 가능하게 한다. (이는 다음 차례에서 다룰 것이다.)
네비게이션 드로어 디자인 가이드에 따라, 드로어가 보여질때 액션바의 내용을 수정해야만 한다. 메인 컨텐츠와 관련된(contextual to the main content) 타이틀과 액션 아이템을 변경하는 등의 작업이 이뤄져야 한다. 다음의 코드는 ActionBarDrawerTaggle
클래스의 인스턴스에 있는 DrawerLayout.DrawerListener
콜백 함수들을 오버라이드하여 액션바의 내용을 수정하는 방법에 대한 예제이다.
public class MainActivity extends Activity {
private DrawerLayout mDrawerLayout;
private ActionBarDrawerToggle mDrawerToggle;
private CharSequence mDrawerTitle;
private CharSequence mTitle;
...
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
mTitle = mDrawerTitle = getTitle();
mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout,
R.drawable.ic_drawer, R.string.drawer_open, R.string.drawer_close) {
/** Called when a drawer has settled in a completely closed state. */
public void onDrawerClosed(View view) {
super.onDrawerClosed(view);
getActionBar().setTitle(mTitle);
invalidateOptionsMenu(); // creates call to onPrepareOptionsMenu()
}
/** Called when a drawer has settled in a completely open state. */
public void onDrawerOpened(View drawerView) {
super.onDrawerOpened(drawerView);
getActionBar().setTitle(mDrawerTitle);
invalidateOptionsMenu(); // creates call to onPrepareOptionsMenu()
}
};
// Set the drawer toggle as the DrawerListener
mDrawerLayout.setDrawerListener(mDrawerToggle);
}
/* Called whenever we call invalidateOptionsMenu() */
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
// If the nav drawer is open, hide action items related to the content view
boolean drawerOpen = mDrawerLayout.isDrawerOpen(mDrawerList);
menu.findItem(R.id.action_websearch).setVisible(!drawerOpen);
return super.onPrepareOptionsMenu(menu);
}
}
다음 차례에서는 ActionBarDrawerToggle
의 생성자 매개변수들과 액션 바 아이콘과의 상호작용을 통제하기 위해 추가로 필요로 되는 절차들에 대해서 설명할 것이다.
앱 아이콘을 눌러 드로어 열고 닫기
사용자들은 왼쪽/오른쪽으로 화면을 미는(swipe) 제스쳐를 통해 네비게이션 드로어를 열고 닫을 수 있다. 하지만 액션바를 사용한다면, 사용자들이 앱 아이콘의 터치만으로도 드로어를 열고 닫을 수 있도록 해야 한다. 앱 아이콘은 또한 네비게이션 드로어의 개폐 여부를 특별한 아이콘으로 표시할 수 있다. 이러한 모든 작업은 이전 차례에서의 ActionBarDrawerToggle
클래스를 상속하여 구현할 수 있다.
ActionBarDrawerToggle
이 작동하도록 하기 위해서는, 해당 클래스의 생성자를 통해 인스턴스를 생성해야 하며, 이는 다음의 매개변수들을 필요로 한다.
- 드로어를 포함한
Activity
DrawerLayout
- 드로어 상태표시자로 사용될 이미지 리소스(Drawable resource)
표준 네비게이션 드로어 아이콘은 Download the Action Bar Icon Pack에서 다운로드 받을 수 있다. - (접근성을 위하여) “드로어를 여는” 행위를 나타낼 문자열 리소스
- (접근성을 위하여) “드로어를 닫는” 행위를 나타낼 문자열 리소스
ActionBarDrawerToggle
의 하위 클래스를 드로어행위 감지자(drawer listener)로 생성했는지와는 무관하게, Activity
생성주기(lifecycle) 전체에 걸쳐 몇 군대에서 ActionBarDrawerToggle
을 호출할 필요가 있다.
public class MainActivity extends Activity {
private DrawerLayout mDrawerLayout;
private ActionBarDrawerToggle mDrawerToggle;
...
public void onCreate(Bundle savedInstanceState) {
...
mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
mDrawerToggle = new ActionBarDrawerToggle(
this, /* host Activity */
mDrawerLayout, /* DrawerLayout object */
R.drawable.ic_drawer, /* nav drawer icon to replace 'Up' caret */
R.string.drawer_open, /* "open drawer" description */
R.string.drawer_close /* "close drawer" description */
){
/** Called when a drawer has settled in a completely closed state. */
public void onDrawerClosed(View view) {
super.onDrawerClosed(view);
getActionBar().setTitle(mTitle);
}
/** Called when a drawer has settled in a completely open state. */
public void onDrawerOpened(View drawerView) {
super.onDrawerOpened(drawerView);
getActionBar().setTitle(mDrawerTitle);
}
};
// Set the drawer toggle as the DrawerListener
mDrawerLayout.setDrawerListener(mDrawerToggle);
getActionBar().setDisplayHomeAsUpEnabled(true);
getActionBar().setHomeButtonEnabled(true);
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
// Sync the toggle state after onRestoreInstanceState has occurred.
mDrawerToggle.syncState();
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
mDrawerToggle.onConfigurationChanged(newConfig);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Pass the event to ActionBarDrawerToggle, if it returns
// true, then it has handled the app icon touch event
if (mDrawerToggle.onOptionsItemSelected(item)) {
return true;
}
// Handle your other action bar items...
return super.onOptionsItemSelected(item);
}
...
}
네비게이션 드로어의 완성된 예시는 샘플을 다운로드하여 확인할 수 있다.
댓글 4
수많은 글들 중 눈에 띄는 멋진 글입니다.
질문 하나 드려도 될까요?
DrawerLayout에는 layout_margin 속성이 공개되지 않은 모양인데, 마진을 다룰 수 있는 방법이 있을까요? 목적은 DrawerLayout을 끌어내기 전에도 DrawerLayout의 가장자리가 약간 보이게 하는 것입니다. 방법이 있으면 가르쳐 주시기 바랍니다.
감사합니다.
DrawerLayout을 상속하여 직접 구현하시는 것 말고는 따로 방법이 없을것으로 보입니다.
상당히 어렵긴 합니다만, 덕분에 큰 도움이 되었습니다. 감사합니다.
도움이 되었다니 감사합니다. :D