단편 Android 개발 정리
다음 포스트: 사이드 메뉴(DrawerLayout) 구현하기 (2021년 버전)
ActionBar 대신 Toolbar로 대체하기
2021년 기준 최신 버전의 Android Studio에서 앱의 ActionBar 대신 Toolbar로 대체하는 방법을 단계별로 정리해보겠다.
1 단계. Empty Project 생성하기
Android Studio를 열고 아무 액티비티도 갖지 않는 빈 프로젝트를 하나 생성한다. 프로젝트의 이름와 도메인은 적절하게 입력한다.
그 다음 /res/values/themes/themes.xml
를 열면 style
태그가 ActionBar
로부터 파생되었음을 지정하는 내용이 있을 것이다. 일단 확인만 하고 놔 둔다.
2 단계. 빈 액티비티 추가하기
빈 액티비티를 하나 추가한다. 이름은 적절히 부여한다.
activity_main.xml
아무것도 수정하지 않은 맨 처음 상태의 빈 액티비티라면 이렇게 생겼을 것이다.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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"
tools:context=".MainActivity">
</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity.kt
또한 비하인드 코드는 이렇게 생겼다.
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
3 단계. ActionBar를 지우기
2 단계에서 아무것도 건드리지 않고 그냥 실행을 해 보겠다. 그러면 다음과 같이 상단에 ActionBar를 포함하는 빈 화면이 나타난다.
여기에서부터 하나씩 수정해나가겠다. 앞서 확인했던 /res/values/themes/themes.xml
파일을 다시 열고 앱이 정의하고 있는 기본 테마를 ActionBar
에서 NoActionBar
계열로 바꾼다. 그러면 AndroidManifest.xml
의 application
에서 themes.xml
에서 정의된 테마를 참조하고 그 결과가 application
의 일부인 MainActivity
에도 반영된다.
정리하자면,
themes.xml
이 Theme.MaterialComponents.DayNight.NoActionBar
로부터 파생된 Theme.Testapp
라는 이름의 새 테마를 정의한다.
AndroidManifest.xml
이 Theme.Testapp
를 참조하여 앱의 전반적인 테마를 적용한다.
MainActivity
도 여기에 영향을 받아 ActionBar가 사라진다.
4 단계. ActionBar가 사라진 자리에 Toolbar 넣기
이전 버전의 Android Studio에서는 Toolbar의 이름이 다음과 같이 android.support.v7.widget.Toolbar
였다.
<android.support.v7.widget.Toolbar
android:id="..." />
그러나 2021년 현재 최신 Android Studio에는 이 코드가 오류가 난다. Toolbar의 이름은 다음과 같이 바뀌었다. 물론 gradle에서 의존성을 수정하고 번거로운 세팅을 하면 옛날 이름으로 쓸 수 있겠으나, 이제부터 개발하는 앱은 기본 설정을 사용하는 것으로...
<androidx.appcompat.widget.Toolbar
android:id="..." />
이제 MainActivty
의 비하인드 코드 측에도 레이아웃에 새로 추가된 Toolbar를 ActionBar처럼 사용하라고 지정해 주어야 한다. MainActivty.kt
를 열고 onCreate
메서드의 맨 끝 부분에 다음의 내용을 적는다.
val toolbar = findViewById<Toolbar>(R.id.toolbar)
//setSupportActionBar(toolbar)
onCreate
메소드의 전체적인 내용은 다음과 같아야 할 것이다. super.onCreate
를 호출하고 setContentView
를 실행한 다음에 새로 작성하는 문장들이 등장해야 할 것이다.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
}
그러면 ActionBar처럼 Toolbar에도 앱의 이름이 나타나는 것을 볼 수 있다. 텍스트의 색이 조금 달라 보이는 것은 기분 탓일 것이다.
5 단계. 메뉴 추가하기
ActionBar와 달리 Toolbar는 View의 일종이기 때문에 버튼이나 메뉴 등 다양한 요소들의 기능과 위치를 제어하기가 편리하다.
메뉴 작성하기
프로젝트에 /res/menu
디렉토리를 하나 생성한다. 여기에 메뉴를 정의하는 XML 파일을 생성한다. 이 파일에 Toolbar에 넣을 메뉴들을 적는다.
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/action_item0"
android:title="바나나"
app:showAsAction="ifRoom" />
<item android:id="@+id/action_item1"
android:title="포도"
app:showAsAction="never" />
<item android:id="@+id/action_item2"
android:title="딸기"
app:showAsAction="never" />
<item android:id="@+id/action_item3"
android:title="포도"
app:showAsAction="never" />
</menu>
여기서 app:showAsAction
속성은 메뉴 항목을 Toolbar 우측 [더 보기...] 버튼을 눌러야 나타나게 할 것인지 여부를 지정한다.
never
는 해당 항목을 무조건 [더 보기...] 버튼을 눌러야만 나타나게 한다.ifRoom
은 화면의 폭이 충분할 때는 Toolbar에 직접 나타내고 좁을 때는 [더 보기...] 버튼을 통해 보여지게 한다.always
라고 해서 화면의 폭과 무관하게 무조건 Toolbar에 직접 나타내는 옵션이 있는데 이는 IDE에서 권장하지 않고ifRoom
을 사용하도록 유도하고 있다.
설명보다는 직접 보고 정리하면 되겠다.
레이아웃에 메뉴 적용하기
레이아웃 파일의 androidx.appcompat.widget.Toolbar
로 돌아가서 다음의 속성을 추가하면 Toolbar에 메뉴가 적용된다.
app:menu="@menu/파일.xml"
"바나나" 항목은 app:showAsAction="ifRoom"
으로 지정했으므로 Toolbar에 바로 나타난다.
app:showAsAction="never"
로 지정한 항목은 [더 보기...] 버튼을 눌러야 나타난다.
6 단계. 메뉴 항목마다 이벤트 작성하기
Toolbar와 menu의 겉 모양은 대충 만들었으니 비하인드 코드를 작성할 차례이다. kotlin 소스 코드를 열고 액티비티에 다음과 같이 메소드를 오버라이드한다.
class MainActivity: AppCompatActivity() {
// ...
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
Log.d("menu", "onCreateOptionsMenu")
menuInflater.inflate(R.menu.menu_toolbar, menu)
return super.onCreateOptionsMenu(menu)
}
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
Log.d("menu", "onPrepareOptionsMenu")
return super.onPrepareOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
var result = false
when (item.itemId) {
R.id.action_item0 -> { Log.d("menu", "바나나 선택함"); result = true }
R.id.action_item1 -> { Log.d("menu", "포도 선택함"); result = true }
R.id.action_item2 -> { Log.d("menu", "딸기 선택함"); result = true }
}
return result;
}
}
onCreateOptionsMenu
는 액티비티에 딸려 있는 메뉴를 최초로 구성할 때 1회만 호출된다. 이것을 재정의하여 menuInflater.inflate
를 호출해준다. menuInflater.inflate
메소드가 무엇인가 하니, 리소스로부터 메뉴 XML 파일을 불러와서 메뉴로 띄워주는 역할을 한다. 레이아웃에서 지정했던 app:menu="@menu/파일.xml"
과 동일한 것이다. 다만 레이아웃에서 app:menu="@menu/파일.xml"
을 굳이 적지 않고 menuInflater.inflate
메소드만 호출해도 앱 실행 시 메뉴가 나타났지만, 반대로 menuInflater.inflate
메소드를 호출하지 않고 레이아웃에게 app:menu="@menu/파일.xml"
만을 지정하면 메뉴가 나타나지 않았다. 그냥 그렇다는 거다.
onPrepareOptionsMenu
는 메뉴를 최초로 구성할 때는 물론이고 [더 보기...] 버튼을 클릭하여 메뉴가 펼쳐질 때마다 호출된다. 사용자에게 메뉴 항목을 보여주기 전 뭔가를 메뉴 항목을 업데이트할 때 유용할 것으로 보인다.
onOptionsItemSelected
는 메뉴 항목을 클릭/터치할 때 호출된다. 어떤 메뉴가 클릭/터치되었는지는 매개변수로 넘어온 item: MenuItem
의 menu.itemId
를 통해 식별 가능하다.
7 단계. 햄버거 버튼 만들기
이렇게까지 ActionBar를 굳이 ToolBar로 바꾸어서 버튼들을 추가하는 이유 중 하나는 사이드 바 메뉴, 일명 "햄버거 버튼(hamburger button)"을 만들기 위함일 것이다. 햄버거 버튼을 추가해 보겠다.
햄버거 이미지 준비하기
먼저 햄버거 아이콘을 준비한다. 직접 만들 필요도 없고, 구글링 통해 굳이 직접 구할 필요도 없다. Android Studio의 클립아트에 미리 준비되어 있다. drawable 디렉토리에 새로운 Vector(Image) Asset을 추가해본다.
여기서는 벡터 이미지를 넣어보겠다. 클립아트 옆의 그림을 클릭하면 미리 준비된 아이콘들의 목록이 나타난다. 이 중에서 햄버거 아이콘(이름: menu)을 찾아서 클릭하고, 색상과 크기(일단은 넉넉하게 크기를 지정해 준다)를 적절하게 넣어주면 끝.
그리고 onCreate
메소드로 가서 홈(Home) 버튼이 Toolbar에 생겨나도록 다음과 같이 문장을 작성한다. 앞서 만들어 본 메뉴 항목과 달리 햄버거 아이콘은 대체로 화면의 왼쪽에 위치할텐데, Toolbar의 왼쪽에 생기는 아이콘을 홈 버튼이라 한다.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
// 새로 추가된 두 줄
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_menu)
}
}
setDisplayHomeAsUpEnabled
은 매개변수에 true
를 전달함으로써 Toolbar에 홈 버튼이 나타나게 한다. setHomeAsUpIndicator
은 홈 버튼으로 표시할 아이콘을 지정하는데, 위에서 클립아트를 통해 추가한 햄버거 아이콘의 아이디를 적으면 된다.
이런 모양을 원했던 것이 아니다. 그렇다면 화면 크기에 맞게 이미지 크기를 축소해주어야 한다. 여기서는 벡터 형식으로 아이콘을 추가했으므로 크기 조정이 매우 간편하다. 햄버거 아이콘 파일을 열고 폭(android:width)과 높이(android:height)를 ?attr/actionBarSize
로 수정한다. 그러면 다음과 같이 제법 그럴듯한 아이콘이 만들어진다.
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:height="?attr/actionBarSize"
android:tint="#FFFFFF"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="?attr/actionBarSize">
<path android:fillColor="@android:color/white" android:pathData="M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z" />
</vector>
이것도 크다 하면 마진(margin)을 좀 더 주는 식으로 줄일 수 있다. path
element를 group
으로 감싼 다음 scaleX
와 scaleY
를 1.0
보다 작은 값으로 지정한다. 그러면 path
가 표현하는 햄버거 아이콘이 좀 더 축소된다. 예를 들어 scaleX="0.8" scaleY="0.8"
로 하면 가로와 세로를 본래의 80%로 축소시킨다.
크기는 좀 줄었는데 위치가 애매해졌다. 아이콘을 버튼 영역의 정중앙에 오도록 하려면 pivotX
와 pivotY
를 각각 android:viewportWidthM
의 절반, android:viewportHeight
의 절반으로 지정하면 된다.
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:height="?attr/actionBarSize"
android:tint="#FFFFFF"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="?attr/actionBarSize">
<group
android:scaleX="0.8"
android:scaleY="0.8"
android:pivotX="12"
android:pivotY="12">
<path android:fillColor="@android:color/white" android:pathData="M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z" />
</group>
</vector>
이 버튼을 터치했을 때의 이벤트 처리는 다음과 같이 작성한다.
class MainActivity : AppCompatActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
var result = false
when (item.itemId) {
android.R.id.home -> { Log.d("menu", "햄버거 버튼"); result = true }
// ...
}
return result;
}
}
햄버거 버튼, 홈 버튼의 ID는 android.R.id.home
으로 고정되어 있다.