본문 바로가기
안드로이드

[안드로이드 #5] menu, drawer, toolbar

by algosketch 2021. 5. 23.
  • menu
  • drawer
  • toolbar

 이번 스터디를 위해 새로운 모듈을 만들어서 사용하겠습니다. RecyclerView 때와 동일하게 만들면 됩니다. menu, drawer, toolbar 가 공통점이 있어 한 번에 이야기 하려 합니다. 이 부분은 많이 안 써보기도 했고 특히 toolbar 는 내용이 많아서 제가 싫어합니다. 그나마 가장 좋아하는 건 drawer 입니다.

 - menu

 메뉴를 만드는 방법은 메뉴를 위한 xml 을 만들고 배치하고 이벤트를 연결해 주면 됩니다. 먼저 메뉴 xml 부터 만들어 보겠습니다. 메뉴 xml 파일들을 담기 위한 폴더를 만들어 줍니다. res 폴더에서 우클릭으로 만들 수 있습니다.

Resource type 에 보시면 menu 라는 옵션이 이미 있습니다. 이것을 선택해 줍니다.

 그리고 menu 폴더 우클릭 > New > Menu Resource File 을 통해 메뉴 xml 을 만들어 주면 위와 같은 코드와 화면이 나오게 됩니다. 저는 split 으로 설정했기 때문에 코드와 랜더링 화면이 같이 나옵니다.

 xml 파일이기 때문에 태그를 이용하여 작성합니다. menu 라는 태그로 전체를 감싸고 그 안에 item 이라는 태그로 메뉴의 목록을 만들어 줍니다. 필수 속성으로 title 이 있습니다.

 아이콘을 추가한 뒤 showAsAction 속성에 always 을 주면 위 사진처럼 아이콘이 툴바에 항상 노출됩니다. 그러고 실행해 보면 메뉴가 보이지 않습니다. 이유는 이 메뉴를 툴바에 연결해주지 않았기 때문입니다.

override fun onCreateOptionsMenu(menu: Menu?): Boolean {
    val inflater: MenuInflater = menuInflater
    inflater.inflate(R.menu.menu1, menu)
    return true
}

 MainActivity 에서 onCreateOptionMenu 를 오버라이딩해야 메뉴가 보이게 됩니다. 코드는 위와 같이 작성하시면 됩니다. onCreateOptionMenu 도 onCreate, onStop, onStart, onDestroy 와 같이 Activity 에서 오버라이드 할 수 있는 메소드 중 하나라고 생각하시면 됩니다. 랜더링 관련된 객체로 xxxInflater 라는 이름이 자주 등장하게 됩니다. RecyclerView 에서는 LayoutInflater 가 있었죠. 나중에 보게 될 fragment 에서도 등장합니다.
 메뉴를 화면에 보이는 데 성공했으니 메뉴에 이벤트를 연결해보겠습니다. 이벤트 연결 역시 오버라이딩을 통해 이루어집니다.

override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when (item.itemId) {
            R.id.search -> {
                Toast.makeText(this, "search menu clicked!", Toast.LENGTH_SHORT).show()
                true
            }
            R.id.item2 -> {
                Toast.makeText(this, "item2 menu clicked!", Toast.LENGTH_SHORT).show()
                true
            }
            else -> super.onOptionsItemSelected(item)
        }
    }

 menu 이벤트 자체에 대한 설명은 필요 없어보입니다. 다만, kotlin 문법의 특징이 있습니다. when 은 switch 문과 비슷한데요, kotlin 에서 if 와 when 은 문(statement)이 아니라 식(expression)이라고 부릅니다. a = a + 3 과 같은 식이라는 뜻입니다. when 식에서 각각의 케이스들은 -> 로 람다처럼 표현합니다. 내부 로직이 한 줄이면 else 처럼 {중괄호}를 생략할 수 있습니다. 마지막 명령어가 자동으로 반환됩니다. 위 코드에서는 true 가 반환됩니다.

 메뉴에 대한 기본적인 내용을 다뤄 보았고, 자세한 내용은 https://developer.android.com/guide/topics/ui/menus?hl=ko#kotlin 여기를 참고하시면 됩니다. 툴바에서 제공하는 메뉴 이외에도 화면을 길게 클릭하면 등장하는 컨텍스트 메뉴라는 것도 있습니다.

 - drawer

 drawer 는 서랍이라는 뜻으로 왼쪽 혹은 오른쪽으로 스와이프하여 레이아웃을 가져옵니다. 보통 햄버거 모양 메뉴와 같이 제공됩니다. 양쪽으로도 구현이 가능하지만 UX 상 양쪽 다 drawer 를 만드는 것은 지양하는 것이 좋습니다.

 위 같은 레이아웃은 아래 코드로 구현할 수 있습니다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout
    android:id="@+id/drawer"
    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"
    tools:context=".MainActivity">

	<!-- 메인 -->
    <LinearLayout
        android:id="@+id/main_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:textSize="36dp"
            android:text="Main"
            android:gravity="center" />

    </LinearLayout>

	<!-- Drawer -->
    <LinearLayout
        android:id="@+id/drawer_menu"
        android:layout_width="300dp"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:gravity="center"
        android:background="@color/purple_200"
        android:layout_gravity="left">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:textSize="36dp"
            android:text="Drawer"
            android:gravity="center" />
    </LinearLayout>

</androidx.drawerlayout.widget.DrawerLayout>

 루트 태그로 DrawerLayout 을 사용했습니다. 그리고 child 로 두 개의 Layout 을 사용했습니다. 사실 child 는 Layout 대신 View 를 넣어도 상관없습니다. 내부의 두 개의 Layout 중 하나는 메인 화면이 되고 하나는 Drawer 가 됩니다. 이것을 결정하기 위해서 Drawer 가 될 Layout 에 android:layout_gravity="left" 를 넣어줍니다. 만약 right 로 설정하면 drawer 가 오른쪽에서 나오게 됩니다. Layout 이나 View 를 하나 더 만들어서 right 로 설정하면 왼쪽과 오른쪽 모두 Drawer 를 만들 수 있습니다. 하지만 같은 방향으로 두 개를 둘 수는 없습니다.
 Drawer 를 스와이프를 통해서만 열 수 있다면, 일단 스와이프 자체는 눈에 보이지 않기 때문에 숨은 화면의 존재 자체를 모를 수도 있습니다. 그래서 버튼을 통해서도 열 수 있게 제공되는 경우가 많습니다. 메뉴를 이용해서 이 기능을 추가해 보도록 하겠습니다. 참고로 이번 스터디에서 제가 아이콘을 다운로드 받는 출처는 https://developer.android.com/design/downloads/index.html 여기입니다.

아까와 같은 방식으로 메뉴를 만들고 기존에 있던 메뉴를 대체하였습니다.

아래와 같은 파일의 코드, 선언부

 위 코드의 onCreateOptionsMenu 에서 기존의 메뉴를 주석처리하고 새로운 메뉴를 넣었습니다. 그리고 onOptionsItemSelected 의 when 내부에 item.itemId == R.id.btn_drawer 인 경우를 추가하여 이벤트를 구현했습니다. DrawerLayout 을 drawer 변수에 받고 drawer.openDrawer() 과 drawer.closeDrawer 를 통해 Drawer 가 열리고 닫히는 것을 제어합니다. 인자로는 Gravity.LEFT 를 넣었는데, xml 을 구성할 때 Drawer 가 될 레이아웃에 주었던 값과 동일한 값을 주어야 합니다.

 drawer 는 왼쪽에서 나오는데 메뉴는 오른쪽에 있는 게 이상합니다.

 - toolbar

 AndroidManifest.xml 파일에서 noActionBar 가 포함된 테마를 선택하면 액션바(toolbar)가 사라집니다. 툴바를 사용하지 않는 앱같은 경우 이런 식으로 없애면 됩니다. 커스텀 툴바를 만들 것이기 때문에 이 테마를 적용하도록 하겠습니다.

 기본적으로 툴바, 스테이터스바를 포함한 스타일, 앱 전체의 색깔 등은 res/values/themes 에 담겨 있습니다. Android Studio 4.0.X 버전까지는 themes.xml 파일이 하나였는데 4.1 이후의 버전에서는 night 모드 사용자가 많아져서인지 개발할 때 night 모드도 같이 신경쓰라고 권장하는 것 같네요. 다시 하나로 바꾸실 수도 있습니다. 괄호 안에 (night) 라고 적혀 있는 건, 실제 파일 이름은 themes.xml 인데 night 모드에서는 저 테마가 적용된다는 뜻 같습니다.

 위 화면은 위에서부터 각각 layout 과 values Resource 를 만드는 화면인데요, 왼쪽에 있는 옵션을 >> 버튼으로 추가하면 특정 상황에서만 작동하는 Resource 를 만들 수 있습니다. layout 같은 경우 Landscape 옵션을 넣어 가로모드에서는 해당 레이아웃이 적용되게 만들 수 있습니다. values 에 strings.xml 파일을 지역이나 나라별로 나누어서 만들고, layout 의 text 속성에 텍스트를 직접 넣는 것이 아닌 strings.xml 에 저장한 텍스트의 name 을 전달하면, 휴대폰에 설정된 언어난 한국어냐 영어냐에 따라 다른 언어를 지원하는 것도 가능합니다. 이를 국제화라고 합니다.
 잠깐 다른 얘기로 샜는데 다시 툴바로 돌아와서...

themes/themes.xml

 themes.xml 파일에서 툴바의 색깔이나 앱에서 사용할 텍스트의 기본 색 등을 정할 수 있습니다. 어떤 name 값이 어떤 것의 색깔을 나타내는 지는, 어떤 테마를 상속받았느냐에 따라 다르기 때문에 parent="..." 에서 ... 에 대해 검색하시면 됩니다. 저 기본 테마는 Android Studio 새 버전 업데이트할 때마다 금방금방 바뀝니다.

	<androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:background="#EEEEEE"
            android:elevation="3dp"
            android:layout_centerInParent="true">

            <ImageButton
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:src="@drawable/baseline_menu_24"
                android:background="#EEEEEE"
                android:layout_margin="16dp"/>

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Toolbar"
                android:textSize="36dp"/>

        </androidx.appcompat.widget.Toolbar>

 기존 코드에서 Toolbar 를 추가합니다. 이 때 사용할 툴바는 androidx 를 사용합니다. 이것으로 Toolbar 를 커스텀할 수 있습니다. 기본 방향은 가로 방향입니다. 이대로 실행해보면 딱 위에 보이는 저 화면이 나옵니다. 메뉴에 대한 내용은 날아가게 됩니다. manifests.xml 에서 설정하는 기본 툴바를 사용하지 않았기 때문에 코드상에서 툴바를 툴바라고 알려줘야 메뉴가 정상적으로 적용됩니다.

val toolbar = findViewById<Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)

 MainActivity.kt 에 위 코드를 추가해 주면 메뉴까지 적용됩니다.

 원래 사용했던 앱 이름과 메뉴까지 출력되는 모습입니다. 제목은 다음 라인을 추가하여 코드상에서 없앨 수 있습니다.

supportActionBar?.setDisplayShowTitleEnabled(false)

 메뉴는 두 번째로 만든 햄버거 모양 메뉴가 아니라 처음에 만들었던 메뉴로 바꿔주고, DrawerLayout 과 연결됐던 이벤트는 새로 만든 Toolbar 의 ImageButton 에 연결해주면 훨씬 깔끔해집니다.

class MainActivity : AppCompatActivity() {
    lateinit var drawer: DrawerLayout

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val toolbar = findViewById<Toolbar>(R.id.toolbar)
        setSupportActionBar(toolbar)
        supportActionBar?.setDisplayShowTitleEnabled(false)

        val drawerButton = findViewById<ImageButton>(R.id.btn_drawer)
        drawerButton.setOnClickListener {
            drawer.openDrawer(Gravity.LEFT)
        }

        drawer = findViewById(R.id.drawer)
    }

    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        val inflater: MenuInflater = menuInflater
        inflater.inflate(R.menu.menu1, menu)
        return true
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when (item.itemId) {
            R.id.search -> {
                Toast.makeText(this, "search menu clicked!", Toast.LENGTH_SHORT).show()
                true
            }
            R.id.item2 -> {
                Toast.makeText(this, "item2 menu clicked!", Toast.LENGTH_SHORT).show()
                true
            }
            else -> super.onOptionsItemSelected(item)
        }
    }
}

 툴바에 대한 일부만 얘기했는데, 툴바가 자주쓰이는 건 아니라고 생각해서 이정도만 설명하겠습니다. 유튜브나 페이스북 앱을 보면, 아래로 스크롤했을 때 상단 바가 작아지거나 사라지는 부분이 있습니다. 이런 동작은 AppBarLayout 을 이용하여 구현이 가능합니다.

 이번 주는 과제가 없습니다. 지난 번 과제 열심히 해주시기 바랍니다!!