ANDROID 프로그래밍/LAYOUT

안드로이드 드로어레이아웃. (Android DrawerLayout)

뽀따 2017. 10. 13. 17:10


1. 화면의 특정 영역에서 동적으로 열리고, 닫히는 사용자 인터페이스.

통상적으로, "안드로이드 레이아웃"이라고 하면, 화면의 전체 또는 일부에 자신의 영역을 확보한 다음, 정해진 규칙에 따라 자식(Children) 뷰 위젯들을 배치하는 역할을 하는 요소를 말합니다. 


한 방향으로 뷰(View) 위젯을 나열하는데 사용되는 [리니어레이아웃(LinearLayout)], 뷰(View) 위젯 간 상대적 위치에 따라 자식(Children)들을 정렬하는 [렐러티브레이아웃(RelativeLayout)], 액자 형태로 뷰(View) 위젯을 바꿔가면서 표시하기 위한 [프레임레이아웃(FrameLayout)], 그리고 표 형태의 레이아웃을 구성할 때 사용하는 [테이블레이아웃(TableLayout)] 등, 여러 종류의 레이아웃들이 바로 그런 역할을 수행합니다.


그런데 앞에서 살펴본 요소들처럼 고정 영역에 레이아웃이 표시되는 형태가 아니라, 사용자의 액션에 따라 화면의 일부분이 나타나거나 사라지게 만드려면 어떻게 해야 할까요? 마치 책상에서 "서랍"이 열리고 닫히는 것처럼, 평소에는 화면의 한 쪽에 숨겨져 있다가, 사용자가 화면을 스와이프하거나 메뉴 버튼을 선택하면 화면에 표시되는 형태 말이죠.


물론, 여러 레이아웃 중 하나를 사용하여 화면을 구성한 다음, 사용자 입력 이벤트 핸들러를 작성하여, 레이아웃의 visibility를 조절하는 방법을 선택할 수 있을 것입니다. 거기에 더해 레이아웃이 나타나고 사라지는 과정의 애니메이션 효과까지 구현해주면 더 좋겠죠.


하지만 개발자가 하나하나 모두 구현해야 하는 수고를 들이지 않아도 "서랍"처럼 화면이 열리고 닫히는 기능을 수행할 수 있게 만들어 주는 레이아웃이 있습니다. 바로 DrawerLayout입니다.

2. 안드로이드 DrawerLayout

DrawerLayout의 "Drawer"는 사전적으로 "서랍"을 의미하는 단어입니다.


Drawer라는 단어가 뜻하는 "서랍"이 열리고 닫히는 것처럼, DrawerLayout"평소에는 화면의 한쪽에 숨겨져 있다가 사용자가 액션을 취하면 화면에 나타나는 기능을 만들 수 있게 해주는 레이아웃"입니다.



하지만 DrawerLayout에 대한 설명을 보고, DrawerLayout 자체가 화면에 나타나거나 사라지는 동작을 수행하는 것으로 오해하면 안됩니다. DrawerLayout에 추가된 자식(Children)이 DrawerLayout의 영역 안에서 "Drawer(서랍)"와 같은 동작을 수행하도록 만들어주는 것이죠.


그리고 DrawerLayout에 추가된 모든 자식(Children)들이 Drawer로 동작하는 것이 아니라는 사실에도 주의해야 합니다. 자식(Children)들 중 layout_gravity 속성 값을 가지지 않은 자식(Child)은 기본적으로 표시되는 주화면으로 취급되고, layout_gravity 속성 값을 가진 자식(Child)만이 Drawer로써 동작하는 것이죠. 또한 Drawer가 어느 방향에서 열릴지는 layout_gravity에 지정된 값(left or right)에 의해 결정됩니다.


음, 글로 설명한 내용만으로는 쉽게 이해가 되지 않죠? 하지만 넘 머리 아파하지 마세요. 아래에서 좀 더 자세한 설명과 예제를 통해 DrawerLayout을 어떻게 사용하는지 완벽히 이해하실 수 있을 것입니다.


그럼, 지금부터 DrawerLayout의 사용 방법 및 동작, 그리고 제약 사항들에 대해 알아보겠습니다.

2.1 DrawerLayout의 기본 레이아웃 구성.

DrawerLayout을 사용하여 화면이 열리고 닫히는 기능을 구현하기 위해서는 DrawerLayout에 최소 두 개의 자식(Children)이 추가되어야 합니다. 하나는 Drawer 역할을 수행할 화면이고, 다른 하나는 Drawer가 닫혀있는 상태에서 표시될 주화면입니다.


어떤 화면이 Drawer로 사용될 것인지는, 레이아웃 리소스 XML에 기록된 순서와 layout_gravity 속성의 유무로 결정됩니다. 즉, DrawerLayout의 첫 번째 자식(Child)이 주 화면으로 표시되고, 그 아래 layout_gravity 속성을 가지는 자식(Child)이 Drawer로 사용됩니다.


DrawerLayout에서 Drawer가 어느 방향에서 열리고 닫힐지는 layout_gravity에 지정된 값에 의해 결정되며, 가로 방향에 대해서만 지정할 수 있습니다. 즉, left(start) 또는 right(end) 값만 사용할 수 있습니다.


DrawerLayout에 하나의 Drawer만 사용하는게 일반적이긴 하지만, 그렇다고 반드시 하나의 Drawer만 사용할 수 있는 것은 아닙니다. 정확히 말하면, 각 방향에 하나씩의 Drawer를 지정할 수 있죠. 그러므로 (실제로 잘 쓰이진 않지만) 아래와 같은 경우도 가능한 것입니다.


2.2 DrawerLayout에서 Drawer의 크기

DrawerLayout의 Drawer는 layout_gravity 값에 따라 왼쪽 또는 오른쪽 방향으로 열리고 닫히기 때문에, Drawer의 너비는 DrawerLayout보다 작은 고정(fixed) 값으로, Drawer의 높이는 DrawerLayout의 높이와 같은 값으로 지정하는 게 일반적입니다.


그리고 Drawer가 아닌 주화면은 DrawerLayout 전체 영역에 표시됩니다. 그래서 DrawerLayout 주화면으로 사용되는 자식(Children)은 지정된 layout_width, layout_height 값과 관계없이, match_parent로 동작합니다.

2.3 DrawerLayout 기본 동작.

최초 DrawerLayout이 화면에 표시될 때는, Drawer 화면이 표시되지 않습니다. 주화면, 즉, Drawer가 아닌 자식(Child)의 화면만 표시되죠. 이 때 left에 위치한 Drawer가 열리게 만들기 위해서는 화면 왼쪽 가장자리에서 시작하여 오른쪽 방향으로 쓸어넘기는 스와이프(Swipe) 동작을 수행하면 됩니다. right에 위치한 Drawer는 오른쪽 가장자리에서 시작하여 왼쪽 방향으로 스와이프(Swipe)하면 됩니다. 그리고 Drawer를 닫으려면 열기 방향의 반대로 스와이프(Swipe) 동작을 수행하거나, DrawerLayout 상의 Drawer 외 영역을 클릭하면 됩니다.


하지만 누군가에게는 스와이프(Swipe) 동작이 어렵게 느껴질 수도 있고, 현재 화면에서 Drawer를 사용할 수 있다는 것을 모를 수도 있습니다. 이 때, 스와이프(Swipe)가 아닌 버튼 클릭 등을 통해 Drawer가 표시되게 만들 수 있는데, DrawerLayout에서 제공하는 함수를 통해 Drawer를 열거나 닫을 수 있습니다.

2.4 코드를 통해 Drawer 열기 및 닫기.

DrawerLayout의 Drawer 화면이 기본적으로 스와이프(Swipe) 동작에 의해 열리지만, DrawerLayout에서 제공되는 함수를 호출하면 외부 버튼 또는 메뉴를 통해 열리거나 닫히게 만들 수 있습니다. Drawer 열기 및 닫기, 즉, openDrawer() 함수와 closeDrawer() 함수를 사용하면 자바 코드에서 Drawer를 열고 닫는 게 가능합니다.


아래 표는 파라미터의 종류 및 갯수에 따라 구분된 openDrawer() 함수를 나타낸 것입니다.


리턴 타입 함수 설명
void openDrawer(View drawerView, boolean animate) drawerView 열기. animate에 따라 애니메이션 결정. (API Level 24.0.0 이상)
void openDrawer(View drawerView) 애니메이션과 함께 drawerView 열기.
void openDrawer(int gravity) 지정된 Drawer를 gravity 방향에서 애니메이션과 함께 열기.
void openDrawer(int gravity, boolean animate) 지정된 Drawer를 gravity 방향에서 열기. animate에 따라 애니메이션 여부 결정. (API Level 24.0.0 이상)


closeDrawer() 함수의 종류는 아래와 같습니다.


리턴 타입 함수 설명
void closeDrawer(View drawerView) 애니메이션과 함께 drawerView 닫기.
void closeDrawer(int gravity) gravity 방향에 있는 Drawer를 애니메이션과 함께 닫기.
void closeDrawer(View drawerView, boolean animate) drawerView를 애니메이션과 함께 닫기. (API Level 24.0.0 이상)
void closeDrawer(int gravity, boolean animate) 지정된 Drawer를 gravity 방향으로 열기. animate에 따라 애니메이션 여부 결정. (API Level 24.0.0 이상)
void closeDrawers() 현재 열려 있는 모든 Drawer를 애니메이션과 함께 닫기.


openDrawer() 및 closeDrawer() 함수를 사용하여 Drawer를 열고 닫는 예제 코드는 아래와 같습니다.

    // left에 지정된 Drawer 열기.
    drawer.openDrawer(Gravity.LEFT) ;

    // left에 지정된 Drawer 닫기.
    drawer.closeDrawer(Gravity.LEFT) ;

2.5 Drawer 잠그기(Lock).

앞서 DrawerLayout의 동작에 대해 설명했듯이, DrawerLayout에서 Drawer를 열기 위해서는 Drawer가 닫혀있는 방향의 테두리에서 스와이프(Swipe) 액션을 수행하면 됩니다. 그런데 어떤 경우에는, 스와이프(Swipe)에 의해 Drawer가 열리고 닫히는 것을 막아야 하는 경우도 있습니다. 예를 들어 메뉴로 동작하는 Drawer가 열린 상태로 고정되거나, 현재 컨텐츠 화면에서 Drawer가 표시될 필요가 없는 경우가 바로 그런 경우죠.


이렇게 Drawer를 열린 상태로 고정 또는 닫힌 상태로 고정시키는 것을 Drawer 잠그기(Lock)라고 하는데, DrawerLayout의 setDrawerLockMode() 함수를 통해 Drawer 잠그기(Lock) 기능을 활성화 또는 비활성화 할 수 있습니다.

    void setDrawerLockMode(int lockMode) ;

setDrawerLockMode() 함수의 파라미터인 lockMode에는 아래의 값 중 하나를 지정할 수 있습니다.


lockMode Hex 값 설명
LOCK_MODE_UNLOCKED 0x00000000 잠그기(Lock) 기능 비활성화. 스와이프(Swipe)에 의해 열리고 닫힘.
LOCK_MODE_LOCKED_CLOSED 0x00000001 Drawer가 닫힌 채로 잠김(Locked). 만약 Drawer가 열려 있는 상태였다면 자동으로 닫힘.
LOCK_MODE_LOCKED_OPEN. 0x00000002 Drawer가 열린 채로 잠김(Locked). 만약 Drawer가 닫혀 있는 상태였다면 자동으로 열림.


참고로, setDrawerLockMode() 함수에 의해 Drawer가 잠김(Locked) 상태가 되면 스와이프(Swipe)에 의한 열기 또는 닫기는 불가능해지지만, openDrawer() 함수 또는 closeDrawer() 함수에 의한 열기 또는 닫기는 여전히 가능합니다.

3. DrawerLayout 사용하기

그럼 이제부터, 예제를 통해 DrawerLayout의 사용법에 대해 살펴보겠습니다.

DrawerLayout 사용 예제는 아래 그림과 같은 레이아웃으로 구성됩니다.


3.1 MainActivity 레이아웃 작성하기.

앞서 설계한 화면 레이아웃에 따라 MainActivity의 레이아웃 리소스 XML 파일을 작성합니다.

[STEP-1] "activity_main.xml" - MainActivity 레이아웃 리소스 XML 파일
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context="com.recipes4dev.examples.drawerlayoutexample.MainActivity"
    tools:showIn="@layout/activity_main">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:text="Open"
        android:id="@+id/open" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintLeft_toRightOf="@id/open"
        android:text="Close"
        android:id="@+id/close" />

    <CheckBox
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintLeft_toRightOf="@id/close"
        app:layout_constraintBaseline_toBaselineOf="@id/close"
        android:text="Lock"
        android:id="@+id/lock" />

    <android.support.v4.widget.DrawerLayout
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toBottomOf="@id/open"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        android:id="@+id/drawer">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:textSize="48sp"
            android:text="CONTENTS"
            android:background="#00BCD4"/>

        <TextView
            android:layout_width="400dp"
            android:layout_height="match_parent"
            android:gravity="center"
            android:textSize="48sp"
            android:text="DRAWER"
            android:background="#009688"
            android:layout_gravity="left" />
    </android.support.v4.widget.DrawerLayout>
</android.support.constraint.ConstraintLayout>

3.2 "Open" 버튼 클릭 이벤트 처리.

"Open" 버튼이 클릭되면, Drawer를 여는 코드를 작성합니다.

[STEP-2] "MainActivity.java" - onCreate() 함수에서 "Open" 버튼 클릭 이벤트 작성
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // 코드 계속 ...

        Button buttonOpen = (Button) findViewById(R.id.open) ;
        buttonOpen.setOnClickListener(new Button.OnClickListener() {
            @Override
            public void onClick(View v) {
                DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer) ;
                if (!drawer.isDrawerOpen(Gravity.LEFT)) {
                    drawer.openDrawer(Gravity.LEFT) ;
                }
            }
        });

        // ... 코드 계속
    }
}

3.3 "Close" 버튼 클릭 이벤트 처리.

"Close" 버튼이 클릭되면, 열려 있는 Drawer를 닫는 코드를 작성합니다.

[STEP-3] "MainActivity.java" - onCreate() 함수에서 "Close" 버튼 클릭 이벤트 작성
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        // 코드 계속 ...

        Button buttonClose = (Button) findViewById(R.id.close) ;
        buttonClose.setOnClickListener(new Button.OnClickListener() {
            @Override
            public void onClick(View v) {
                DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer) ;
                if (drawer.isDrawerOpen(Gravity.LEFT)) {
                    drawer.closeDrawer(Gravity.LEFT) ;
                }
            }
        });

        // ... 코드 계속
    }
}

3.4 "Lock" 체크박스 클릭 이벤트 처리.

"Lock" 체크박스가 클릭되면, Drawer를 잠그는 코드를 작성합니다.

[STEP-4] "MainActivity.java" - onCreate() 함수에서 "Lock" 체크박스 클릭 이벤트 작성
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
      
        // 코드 계속 ...

        CheckBox checkboxLock = (CheckBox) findViewById(R.id.lock) ;
        checkboxLock.setOnClickListener(new CheckBox.OnClickListener() {
            @Override
            public void onClick(View v) {
                DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer) ;
                if (((CheckBox)v).isChecked()) {
                    if (drawer.isDrawerOpen(Gravity.LEFT)) {
                        drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_OPEN);
                    } else {
                        drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
                    }
                } else {
                    drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
                }
            }
        });
    }
}

4. 실행 화면

예제 코드를 작성하고 실행하면, 아래 그림과 같은 화면이 표시됩니다.


화면의 왼쪽 가장자리에서 오른쪽으로 스와이프(Swipe)하면 Drawer 화면이 열리고, 반대 방향으로 스와이프(Swipe)하거나 Drawer 외 공간을 클릭하면 Drawer가 닫히는 것을 확인할 수 있습니다.


그리고 화면 위의 "Open", "Close" 버튼을 클릭하면 Drawer가 열리고 닫히는 것을 확인할 수 있습니다.


마지막으로 "Lock" 체크박스를 체크한 상태에서는, 화면을 스와이프(Swipe)해도 Drawer가 열리거나, 닫히지 않은 것을 확인할 수 있습니다. 대신, "Open", "Close" 버튼을 클릭하는 경우, Drawer는 정상적으로 동작합니다.


아래 동영상을 통해, DrawerLayout 예제의 전체적인 동작 과정을 확인할 수 있습니다.


5. DrawerLayout 사용 시 주의 사항

5.1 Drawer로 동작하는 자식(Child)의 layout_gravity는 left(start) 또는 right(end)만 지정 가능.

DrawerLayout에서 Drawer가 위치하는 방향은 왼쪽 또는 오른쪽만 지정 가능합니다. 즉, Drawer의 layout_gravity 속성에 지정할 수 있는 값은 "left"(또는 "start")나 "right"(또는 "end")만 사용할 수 있는 것이죠. 만약 layout_gravity 값을 "top" 또는 "bottom"을 사용하면 아래와 같은 에러가 발생합니다.


java.lang.IllegalStateException: Child android.support.v7.widget.AppCompatTextView{d880155 V.ED..... ......ID 0,0-0,0} at index 2 does not have a valid layout_gravity - must be Gravity.LEFT, Gravity.RIGHT or Gravity.NO_GRAVITY

5.2 layout_gravity 속성 값이 동일한 자식(Child)을 추가할 수 없음.

보통 DrawerLayout에 하나의 Drawer를 만들지만, 오직 하나의 Drawer만 사용할 수 있는 것은 아닙니다. 정확히 말하면, layout_gravity에 지정되는 방향 별로 하나의 Drawer만 지정할 수 있죠. 그래서 만약 layout_gravity 속성 값이 동일한 Drawer가 둘 이상 있으면 에러가 발생합니다.

    <android.support.v4.widget.DrawerLayout ...>
        <TextView ...
            android:text="CONTENTS" />
        <TextView ...
            android:text="DRAWER 1"
            android:layout_gravity="left" />
        <TextView ...
            android:text="DRAWER 2"
            android:layout_gravity="left" />
    </android.support.v4.widget.DrawerLayout>


java.lang.IllegalStateException: Child drawer has absolute gravity LEFT but this DrawerLayout already has a drawer view along that edge

5.3 Drawer가 존재하지 않는 방향의 layout_gravity를 여는 경우는 에러 발생.

앞서 "2.4 코드를 통해 Drawer 열기 및 닫기." 에서, DrawerLayout의 Drawer를 열고 닫을 때, openDrawer()와 closeDrawer() 함수를 사용하는 방법에 대해 설명하였습니다. 그리고 openDrawer()와 closeDrawer() 함수의 파라미터로 gravity가 사용되는 것을 확인했었죠.


그런데 만약, Drawer가 존재하지 않는 방향에 대해, openDrawer() 함수를 사용하여 Drawer 열기를 시도하면 어떻게 될까요?

    <android.support.v4.widget.DrawerLayout ...>
        <TextView ...
            android:text="CONTENTS" />
        <TextView ...
            android:text="DRAWER 1"
            android:layout_gravity="left" />
    </android.support.v4.widget.DrawerLayout>
    DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer) ;
    drawer.openDrawer(Gravity.RIGHT) ;

그럼, 아래와 같은 에러가 발생합니다.

java.lang.IllegalArgumentException: No drawer view found with gravity RIGHT

6. 참고.

.END.