2016-04-22

네비게이션 드로어(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가 네비게이션 드로어로써 작동한다.

이 예제 레이아웃은 중요한 특성 몇가지를 보여준다.

드로어 리스트 초기화하기

Activity에서 가장 먼저 해야하는 것 중 하나는 네비게이션 드로어의 아이템 리스트를 초기화 하는 것이다. 네가 어떻게 초기화 할 것인가는 앱의 컨텐츠에 달려있지만, 네비게이션 드로어는 보통 ListView를 포함하고 있으므로, 목록은 Adapter(예를 들어 ArrayAdapterSimpleCursorAdapter)를 이용해 초기화되어야 한다.

다음은 문자열 배열을 표시하는 네비게이션 리스트를 초기화 하는 방법에 대한 예시이다.

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를 삽입하는 것을 보여주고 있다. (FrameLayoutR.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);
}

열기/닫기 이벤트 감지하기

드로어가 열리고 닫힐 때 발생하는 이벤트를 감지하기 위해, DrawerLayoutsetDrawerListener()를 호출하여, 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이 작동하도록 하기 위해서는, 해당 클래스의 생성자를 통해 인스턴스를 생성해야 하며, 이는 다음의 매개변수들을 필요로 한다.

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

님께 답글 취소
댓글 등록 요청
스팸 댓글을 줄이기 위해 Akismet을 사용하고 있습니다.