본문 바로가기
깡샘 코틀린

11-2 appcompat 라이브러리 - API 호환성 해결

by 농농씨 2023. 6. 29.

androidx 라이브러리에서 가장 많이 사용하는 appcompat 라이브러리안드로이드 앱의 화면을 구성하는 액티비티를 만들며 API 레벨의 호환성 문제를 해결해준다. appcompat 라이브러리를 사용하려면 그래들 파일의 dependencies 항목(의존성 설정)에 다음처럼 선언해야 한다. 그런데 이 선언은 안드로이드 스튜디오에서 모듈을 만들 때 자동으로 추가된다.

// appcompat 라이브러리 선언
implementation 'androidx.appcompat:appcompat:1.3.1'

 

appcompat 라이브러리를 이용해서 액티비티를 만들 때는 플랫폼 API의 Activity가 아니라 다음처럼 appcompat의 AppCompatActivity 클래스를 상속받아 작성한다.

// appcompat 라이브러리 사용
import androidx.appcompat.app.AppCompatActivity
(...생략...)
class MainActivity : AppCompatActivity() { // 라이브러리의 클래스를 상속받아 액티비티 만듦
}

 

액티비티 화면의 기본인 액션바, 툴바를 중심으로 appcompat에서 제공하는 다양한 클래스를 살펴보자.

 

액션바

액티비티의 구성 요소인 액션바(ActionBar)는 화면 위쪽에 타이틀 문자열이 출력되는 영역을 의미한다. 액티비티가 출력되는 전체 창은 액션바와 콘텐츠 영역으로 구분된다. 액션바 영역에는 기본으로 타이틀이 출력된다. 그리고 콘텐츠 영역에는 setContentView() 함수가 출력하는 내용이 출력된다.

 

액션바의 기본 구성은 다음과 같다. 타이틀은 보이지 않게 설정할 수도 있으며 그 밖에 내비게이션 아이콘(왼쪽상단에 작대기 세개 있고 다른 메뉴로 이동할 수 있는거??), 액션 아이템(ex. 검색버튼), 오버플로 메뉴(숨겨진 메뉴 보기) 등 다양한 요소를 액션바에 출력할 수 있다.

 

액션바 색상 설정

안드로이드 앱을 실행하면 기본으로 타이틀쪽에 액션바가 출력된다. 이때 액션바의 색상은 이 앱에 자동으로 적용되는 테마에서 결정된다.

테마 스타일은 res/values 디렉터리에 있는 themes.xml 파일에 선언되어 있다. 이 파일의 내용은 다음과 같다.

// 기본으로 작성되 테마 파일의 내용
<resources xmlns: tools="http://shemas.android.com/tools">
	<!-- 기본 테마 -->>
    <style name="Theme.AndroidLab" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
    	// Theme.AndroidLab 주목
    	<!-- 주 색상 -->
        <!-- 부 색상 -->
        <!-- 상태바 색상 -->
        <!-- 기본 테마 색상 -->
    </style>
</resources>

이름이 Theme.AndroidLab인 스타일이 기본으로 선언되어 있다. 이 스타일이 매니페스트 파일에 테마로 설정된다. 스타일 이름에서 Android.Lab은 프로젝트 이름이다.

// 매니페스트 파일의 테마 설정
<application
	(...생략...)
    android.theme="@style/Theme.AndroidLab"

 

themes.xml 파일의 스타일이 앱의 액티비티에 테마로 자동 설정되고 이 테마 파일에 선언된 색상이 액티비티에 적용된다. 지금 예시로 드는 스타일은 머티리얼 디자인에서 제공하는 Theme.MaterialComponents.DayNight.DarkActionBar 를 상속받아 작성된다. 머티리얼 디자인에서 테마의 속성은 각각 다음을 의미한다.

colorPrimary와 colorSecondary는 앱의 브랜드를 표현하는 색상이다. colorPrimary는 액션바와 버튼의 배경색(background color)으로 사용하고 colorSecondary는 활성 상태를 표현한다. 즉, 텍스트 뷰의 링크, 체크박스나 라디오 버튼이 체크되었을 때, 스위치가 켬(on) 상태일 때, 프롤팅 액션 버튼의 배경색 등에 colorSecondary를 사용한다. 그리고 statusBarColor는 상태바의 배경색으로 사용한다.

또한 colorOnPrimary, colorOnSecondary는 colorPrimary, colorSecondary가 적용되는 곳에서 내용의 전경색(foreground color)으로 사용한다. 그리고 colorPrimaryVariant, colorSecondaryVariant는 그림자의 색상으로 사용한다.

// 테마 색상 설정
<resources xmlns:tools="http://schemas.android.com/tools">
	<style name="Theme.AndroidLab" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
    	<item name="colorPrimary">#FF0000</item>
        <item name="colorPrimaryVariant">color/purple_700</item>
        <item name="colorOnPrimary">@color/white</item>
        <item name="colorSecondary">#0000FF</item>
        <item name="colorSecondaryVariant">@color/teal_700</item>
        <item name="colorOnSecondary">@color/white</item>
        <item name="android:statusBarColor">#CC0000</item>
    </style>    
</resources>

 

 

액션바 숨기기 설정

액티비티의 창은 기본을 액션바를 포함한다. 그런데 때로는 액션바를 출력하고 싶지 않을 때도 있다. 액티비티에 액션바를 숨기는 것도 테마 파일에서 설정할 수 있다. 테마를 만들 때 Theme.MaterialComponents.DayNight.NoActionBar를 상속받으면 액션바가 나오지 않는다.

// 액션바 숨기기
<style name="Theme.AndroidLab" parent="Theme.MaterialComponents.DayNight.NoActionBar">
	(...생략...)
</style>

만약 NoActionBar 테마를 상속받아 작성할 수 없는 경우라면 테마의 <item> 속성을 다음처럼 설정하여 액션바가 나타나지 않게 할 수도 있다.

// <item> 속성으로 숨기기
<style name="Theme.AndroidLab" 
	parent="Theme.MaterialComponents.DayNight.DarkActionBar">
    (...생략...)
    <item name="windowActionBar">false</item> // item 속성 설정으로 액션바 안보이게 함
    <item name="windowNoTitle">true</item> // item 속성 설정으로 액션바 안보이게 함2
</style>

 

 

업 버튼 설정

업(up) 버튼은 액티비티 화면이 앱의 첫 화면이 아닐 때 이전 화면으로 되돌아가는 기능을 한다. 물론 안드로이드폰은 뒤로가기 버튼이 따로 있어서 언제든지 이전 화면으로 되돌아 갈 수 있지만, 액션바 왼쪽에 이전 화면으로 되돌아가는 화살표 모양(←)의 업 버튼을 제공할 수 있다.

업 버튼은 1️⃣액티비티가 등록되는 매니페스트 파일에서 설정하는 방법과 2️⃣액티비티 코드로 설정하는 방법이 있다.

// 매니페스트 파일에서 업 버튼 설정
<activity
	android:name=".TwoActivity"
    android:parentActivityName=".MainActivity"></activity> // parentActivityName 속성 등록함

매니페스트 파일에서 <activity> 태그에 parentActivityName 속성을 등록하는 것만으로도 액티비티 화면에 업 버튼이 나온다. 그래서 사용자가 이 버튼을 누르면 이전 화면으로 되돌아간다.

 

그런데 때로는 업 버튼을 눌렀을 때 이전 화면으로 돌아가기 전에 특별한 로직을 실행하고 싶을 때가 있다. 이때는 액티비티에 onSupportNavigateUp() 함수를 재정의한다. 이렇게 재정의하면 사용자가 업 버튼을 누를 때 onSupportNavigateUp() 함수가 자동으로 호출된다.

// 업 버튼 클릭 시 자동으로 호출되는 함수 재정의
override fun onSupportNavigateUp(): Boolean { // 함수 재정의
	Log.d("kkang", "onsupportNavigateUp")
    return super.onSupportNavigateUp()
}

 

 

이번에는 매니페스트 파일에 parentActivityName 속성을 선언하지 않고 액티비티 코드로 업 버튼이 나오게 하는 방법을 알아보자.

// 액티비티 코드에서 업 버튼 생성
class TwoActivity : AppCompatActivity() {
	override fun onCreate(savedInstanceState: Bundle?) {
    	(...생략...)
        supportActionBar?.setDisplayHoe=meAsUpEnabled(true) // 주목!
    }
    ovdrride fun onSupportNavigateUp(): Boolean {
    	Log.d("kkang", "onSupportNavigateUp")
        onBackPressed() // 이전 화면으로 되돌아가는 코드
        return super.onSupportNavigateUp()
    }    
}

supportActionBar?.setDisplayHomeAsUpEnabled(true) 구문으로 액션바에 업 버튼이 나온다. 이때에도 업 버튼을 클릭하면 onSupportNavigateUp() 함수가 자동으로 호출된다. 그런데 매니페스트 파일의 parentActivityName 속성이 설정되어 있지 않으면 자동으로 이전 화면으로 되돌아가지 않는다. 따라서 앞의 코드처럼 onSupportNavigateUp() 함수에서 onBackPressed() 구문 등으로 이전 화면으로 되돌아가는 코드를 직접 작성해줘야 한다.

 

 

메뉴 구성

메뉴는 액션바의 중요한 구성요소로 액티비티 화면에서 사용자 이벤트를 사용할 수 있도록 한다.

액티비티에 메뉴를 추가하면 액션바 오른쪽에 오버플로 버튼(더보기 버튼 느낌)이 나타난다. 사용자가 이 오버플로 버튼을 누르면 메뉴가 아래로 확장되어 나타난다. 그리고 오버플로 메뉴 중에서 몇몇은 액션바에 아이콘으로 나오게 할 수 있다. 이를 액션 아이템(혹은 액션 버튼)이라고 한다. 결국 오버플로 메뉴와 액션 아이템 모두 메뉴이다.

액티비티에 메뉴를 추가하고 싶다면 onCreateOptionsMenu()onPrepareOptionMenu() 함수를 이용한다. 두 함수는 액티비티의 메뉴를 구성할 때 자동으로 호출되는데, 차이점은 호출되는 시점이 다르다는 것이다. onCreateOptionsMenu() 함수는 액티비티가 실행되면서 처음에 한 번만 호출된다. 그러나 onPrepareOptionsMenu() 함수는 액티비티가 실행되면서 한 번 호출된 후 오버플로 메뉴가 나타날 때마다 반복해서 호출된다.

 

onCreateOptionsMenu() 함수는 액티비티에 정적인 메뉴를 구성할 때 사용하며 onPrepareOptionsMenu() 함수는 메뉴가 화면에 나올 때마다 동적으로 구성하고 싶은 경우에 선택한다. 그런데 액티비티의 메뉴는 주로 사용자 이벤트를 처리하려고 사용하는 것이므로 대부분 액티비티 내에 정적으로 제공하면 된다. 따라서 액티비티의 메뉴는 대부분 onCreateOptionsMenu() 함수를 이용해 구성한다.

 

// 메뉴 구성 함수
ovdrride fun onCreateoptionsMenu(menu: Menu?): Boolean { // onCreateOptionsMenu() 함수로 메뉴 추가
	val menuItem1: MenuItem? = menu?.add(0, 0, 0, "menu1")
    val menuItem2: MenuItem? = menu?.add(0, 1, 0, "menu2")
    return super.onCreateOptionMenu(menu)
}

onCreateOptionsMenu() 함수의 매개변수로 전달되는 Menu 객체를 메뉴바로 생각하면 되고, 이 Menu 객체에 메뉴를 추가할 때 다음과 같이 add() 함수를 이용한다.

  • fun add(groupId: Int, itemId: Int, order: Int, title: CharSequence!): MenuItem!

add() 함수의 두 번째 매개변수는 메뉴의 식별자이다. 메뉴는 이벤트 처리가 목적이므로 어떤 메뉴를 클릭했는지 식별할 때 사용한다. 네 번째 매개변수는 메뉴의 문자열이다. add() 함수의 반환값은 MenuItem 객체이며 이 객체가 메뉴 하나를 의미한다.

 

onCreateOptionsmenu() 함수로 메뉴를 구성하면 액션바에 오버플로 버튼이 나오고 이 버튼을 누르면 오버플로 메뉴가 나타난다. 이 메뉴를 사용자가 선택했을 때의 이벤트 처리는 onOptionsItemSelected() 함수를 이용한다. 이 함수의 매개변수는 이벤트가 발생한 메뉴 객체인 MenuItem이다. MenuItem의 itemId 속성으로 이벤트가 발생한 메뉴 객체의 식별값을 얻어서 이벤트를 처리한다.

// 메뉴 선택 시 이벤트 처리
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
	// 오버플로 메뉴의 메뉴가 선택됐을 때 이벤트처리를 담당하는 onOptionItemsSelected()함수
    // when 구문 써서 매개변숫값에 따라 결과 반환
	0 -> {
    	Log.d("kkang", "menu1 click")
        true
    }
    1 -> {
    	Log.d("kkang", "menu2 click")
        true
    }
    else -> super.onOptionsItemsSelected(item)
}

 

리소스로 메뉴 구현

앞에서 살펴본 것처럼 onCreateOptionsMenu() 함수에서 Menu 객체의 add() 함수로 메뉴를 구성할 수도 있지만, 액티비티의 메뉴는 대부분 정적으로 제공되므로 코드가 아니라 리소스 XML 파일로 구성한다. 메뉴를 구성하는 XML 파일은 res 폴더 아래 menu 디렉터리에 만든다.

// 메뉴 XML
<menu xmlns:android="http://schemas.android.com/apk/res/android"
	xmlns:app="http://schemas.android.com/apk/res-auto">
    <item // 메뉴하나 의미
    	android:id="@+id/menu1" // id 값은 메뉴 식별에 사용
        android:title="menu1" />
    <item
    	android:id="@+id/menu2"
        android:icon="android:drawable/ic_menu_add"
        android:title="menu2"
        app:showAsAction="always" /> // 항상 액션 아이템으로 출력
    <item
    	android:id="@+id/menu3"
        android:icon="android:drawable/ic_menu_search"
        android:title="menu2"
        app:showAsAction="ifRoom" /> // 공간 있으면 액션아이템으로, 없으면 오버플로 메뉴로 출력
</menu>

메뉴 XML의 <item> 태그 하나가 메뉴 하나에 해당한다. id 속성은 레이아웃 XML에서 뷰의 id값과 마찬가지로 메뉴를 식별하는 데 사용한다. title 과 icon 속성은 메뉴 문자열과 아이콘을 지정한다. 메뉴는 기본으로 오버플로 메뉴로 나오는데, 만약 액션바에 아이콘으로 나타나게 하려면 showAsAction 속성을 이용한다. showAsAction 속성값에는 다음 3가지 가운데 하나를 지정하면 된다.

  • never(기본): 항상 오버플로 메뉴로 출력한다.
  • ifRoom: 만약 액션바에 공간이 있다면 액션 아이템으로, 없다면 오버플로 메뉴로 출력한다.
  • always: 항상 액션 아이템으로 출력한다.

XML을 이용해서 메뉴를 작성했다면 이제 이 XML을 액티비티 코드에 적용해 줘야 한다.

// 액티비티 코드에 메뉴 XML 적용
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
	menuInflater.inflate(R.menu.menu_main, menu)
    return super.onCreateOptionsMenu(menu)
}

menuInflater는 MenuInflater 타입의 속성이며 이 객체의 inflate() 함수에 매개변수로 메뉴 XML 파일을 명시하면 액티비티에 메뉴가 적용된다.

 

 

액션 뷰 이용

액션 뷰(ActionView)는 액션바에서 특별한 기능을 제공하며 대표적으로 androidx.appcompat.widget.SearchView가 있다. 서치 뷰는 액션바에서 검색 기능을 제공한다.

// 서치 뷰 사용
<item android:id="@+id/menu_search"
	android:title="search"
    app:showAsAction="always"
    app:actionViewClass="androidx.appcompat.widget.SearchView" /> // 서치 뷰

액션 뷰를 메뉴에 적용할 때는 actionViewClass 속성을 이용한다. 이 속성에 이용할 액션 뷰 클래스를 등록하면 된다. 이렇게 등록하는 것만으로도 액션바에 검색 버튼이 생기고 버튼을 클릭하면 검색어를 입력받는 뷰가 나온다.

 

코드에서 검색과 관련된 기능을 구현하려면 SearchView 객체를 얻어야 한다. 서치 뷰가 메뉴로 제공되므로 SearchView를 등록한 MenuItem 객체를 얻고 MenuItem 객체를 얻고 MenuItem 객체에 등록된 SearchView 객체를 구하면 된다.(?)

// 서치 뷰 검색 기능 구현
override fun onCreateOptionsMenu(menu: Menu): Boolean {
	val inflater=menuInflater
    inflater.inflate(R.menu.menu_main, menu)
    val menuItem = menu?.findItem(R.id.menu_search) // MenuItem 객체 얻음
    val searchView = menuItem?.actionView as SearchView // MenuItem 객체에 등록된 SearchView 객체를 구함
    searchView.setOnQueryTextListener(object: SearchView.onQueryTextListener { // SearchView의 함수를 이벤트 핸들러로 지정...?
    	override fun onQueryTextChange(newText: String?): Boolean {
        	// 검색어 변경 이벤트
        	return true
        }
        override fun onQueryTextSubmit(query: String?): Boolean {
        	// 키보드의 검색 버튼을 클릭한 순간의 이벤트
            return true
        }
    })
    return true
}

MenuItem 객체는 findItem() 함수의 매개변수에 MenuItem의 식별값을 주어 얻는다. MenuItem에 등록된 액션 뷰는 actionView 속성으로 얻는다. 그리고 검색과 관련된 이벤트를 처리할 때는 SearchView의 setOnQueryTextListener() 함수로 이벤트 핸들러를 지정한다.

 

 

툴바

툴바(ToolBar)를 사용하는 목적은 액션바와 같다. 그런데 액션바는 액티비티 창이 자동으로 출력하는 액티비티의 구성 요소지만, 툴바는 개발자가 직접 제어하는 뷰라는 데 차이점이 있다.

툴바를 이용한 예시 그림을 보면(책) 액션바를 이용한 예와 달리, 액티비티 창이 출력되면서 액션바를 출력하지 않는다. 이렇게 하면 화면 전체가 콘텐츠가 된다. 그리고 콘텐츠 상단에 툴바로 화면을 구성했다. 결국 개발자가 레이아웃 XML 파일에 툴바를 직접 작성해야 한다. 툴바는 androidx.appcompat.widget.Toolbar 클래스를 이용하면 된다.

❓결과 화면이 액션바를 사용했을 때와 같은데 굳이 툴바를 이용하는 이유?

❗️툴바는 개발자가 직접 제어할 수 있어서 액션바보다 다양한 기능을 제공할 수 있기 때문

 

툴바의 목적이 액션바와 같으므로 툴바를 사용하려면 앞에서 살펴본 것처럼 먼저 액티비티 테마 설정에서 액션바가 화면에 출력되지 않게 해주어야 한다. 그리고 액티비티의 화면을 구성하는 레이아웃 XML 파일에 다음처럼 Toolbar를 등록한다.

// 레이아웃 XML에 툴바 등록
<androidx.appcompat.widget.Toolbar // 주목
	android:id="@+id/toolbar"
    android:layout_width="match_parent"
    androdi:layout_height="wrap_content"
    style="@style/Widget.MaterialComponents.toolbar.Primary">

툴바를 준비했으면 코드에서 액션바의 내용이 툴바에 적용되도록 지정해줘야 한다. 이때는 setSupportActionBar(binding.toolbar) 구문을 이용한다. 

// 액션바의 내용을 툴바에 적용
class MainActivity : AppCompatActivity() {
	override fun onCreate(savedInstancesState: Bundle?) {
    	(...생략...)
        setSupportActionBar(binding.toolbar) // 주목
    }
}

 

 

호환성을 고려한 기본 뷰 클래스

플랫폼 API에서 제공하는 기본 뷰를 appcompat 라이브러리에서도 제공한다. 예를 들어 플랫폼 API에서 문자열을 출력하는 TextView 클래스를 appcompat 라이브러리에서는 AppCompatTextView라는 클래스명으로 제공한다. 두 클래스는 사용 목적이나 기능이 대부분 같다. 그리고 AppCompatTextView는 TextView를 상속받았다.

appcompat 라이브러리는 AppCompatTextView 이외에도 AppCompatImageView, AppCompatEditText, AppCompatButton, ApppCompatCheckBox, AppCompatRadioButton 등 기본 뷰에 해당하는 뷰를 제공한다.

이처럼 플랫폼 API에서 제공하는 클래스를 appcompat 라이브러리에서도 제공하는 이유는 호환성 문제를 해결하기 위해서이다. TextView를 사용하다 보면 문자열의 줄 높이를 지정하는 setLineHeight() 라는 함수가 있는데, 이 함수는 API 레벨 28에서 추가되었다. 따라서 setLineHeight() 함수를 사용하려면 호환성을 고려해 다음처럼 작성해야 한다.

// API 레벨 호환성을 고려한 예
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
	binding.platformTextView.lineHeight = 50
}

그런데 만약 TextView를 사용하지 않고 appcompat 라이브러리의 AppCompatTextView 클래스를 이용한다면 API 레벨 호환성 문제를 알아서 처리해주므로 다음처럼 간단하게 작성할 수 있다. 이처럼 appcompat 라이브러리는 호환성을 고려한 기본 뷰 클래스를 제공한다.

// API 레벨 호환성을 고려하지 않아도 되는 예
binding.appcompatTextView.lineHeight = 50