1. 제트팩과 androidx 소개
구글에서는 안드로이드 앱을 개발하는 데 필요한 다양한 라이브러리 모음을 제트팩(Jetpack) 이라는 이름으로 제공합니다. 제트팩은 안드로이드 플랫폼이 기본으로 제공하는 플랫폼 API 외에 따로 추가된 라이브러리입니다.
플랫폼 API
플랫폼 API는 ART(Android runtime)에서 제공하는 안드로이드 앱의 핵심 라이브러리입니다. ART는 대부분 android나 java로 시작하는 패키지명을 사용합니다.
안드로이드 앱은 런타임 때 ART에 의해 실행되므로 ART에서 제공하는 플랫폼 API를 이용해 개발합니다. 하지만 기기 버전에 따른 호환성 문제가 발생할 수 있습니다. 또한 플랫폼 API는 기본 재료일 뿐 실제 앱을 개발할 때는 더 다양한 기능이나 화면을 구현해야 합니다. 따라서 구글은 2018년에 제트팩이라는 라이브러리 모음을 발표했습니다.
제트팩
제트팩은 구글에서 안드로이드 앱 개발용으로 제공하는 다양한 라이브러리 모음으로 androidx로 시작하는 패키지명을 사용합니다. 안드로이드 공식 문서에는 제트팩을 다음처럼 소개합니다.
"제트팩은 개발자가 관심 있는 코드에 집중할 수 있도록 권장 사항 준수, 사용구 코드 제거, 모든 안드로이드 버전과 기기에서 일관되게 작동하는 코드를 작성할 수 있도록 돕는 라이브러리 모음입니다."
결국 제트팩은 구글에서 안드로이드 앱 개발용으로 제공하는 추가 라이브러리라고 보면 됩니다.
제트팩은 API 레벨의 호환성 문제를 해결해줍니다. 예를 들어 툴바를 구현한다고 하면 android.widget.Toolbar 클래스를 사용해야 합니다. 그런데 이 클래스는 API 레벨 21 버전에 추가되었으므로 이를 이용한다면 하위 버전의 기기에서는 오류가 발생합니다. 그런데 제트팩의 appcompat 라이브러리에서 제공하는 androidx.appcompat.widget.Toolbar 클래스를 이용하면 호환성 문제가 발생하지 않습니다. 따라서 목적이 같은 클래스를 제트팩의 라이브러리에서도 제공한다면 대부분 제트팩의 클래스를 이용합니다.
그리고 제트팩에서는 플랫폼 API에서 제공하지 않는 다양한 기능을 제공합니다. 예를 들면 뷰 페이저는 플랫폼 API에서 제공하지 않습니다. 뷰 페이저를 이용하면 스와이프로 화면을 전환하는 기능을 쉽게 구현할 수 있습니다.
2. appcompat 라이브러리 - API 호환성 해결
androidx 라이브러리에서 가장 많이 사용하는 appcompat 라이브러리는 안드로이드 앱의 화면을 구성하는 액티비티를 만들며 API 레벨의 호환성 문제를 해결해 줍니다. appcompat 라이브러리를 사용하려면 그래들 파일의 dependencies 항목(의존성 설정)에 다음처럼 선언해야 합니다. 그런데 이 선언은 안드로이드 스튜디오에서 모듈을 만들 때 자동으로 추가됩니다.
implementation 'androidx.appcompat:appcompat:1.2.0'
appcompat 라이브러리를 이용해서 액티비티를 만들 때는 플랫폼 API의 Activity가 아니라 다음처럼 appcompat의 AppCompatActivity 클래스를 상속받아 작성합니다.
import androidx.appcompat.app.AppCompatActivity
...
class MainActivity : AppCompatActivity() {
}
액션바
액티비티의 구성 요소인 액션바(ActionBar)는 화면 위쪽에 타이틀 문자열이 출력되는 영역을 의미합니다. 액티비티가 출력되는 전체 창은 액션바와 콘텐츠 영역으로 구분됩니다. 액션바 영역에는 기본적으로 타이틀이 출력됩니다. 그리고 콘텐츠 영역에는 setContentView() 함수가 출력하는 내용이 출력됩니다.
안드로이드 앱을 실행하면 기본적으로 아래 그림처럼 액션바가 출력됩니다. 이때 액션바의 색상은 이 앱에 자동으로 적용되는 테마에서 결정됩니다. 테마 스타일은 res/values 디렉터리에 있는 themes.xml 파일에 선언되어 있습니다.
액션바를 숨기고 싶다면 테마를 만들 때 Theme.Meterial3.DayNight.NoActionBar를 상속받으면 액션바가 나오지 않습니다.
<style name="Base.Theme.MyFirstApplication" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your light theme here. -->
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
</style>
업 버튼은 액티비티 화면이 앱의 첫 화면이 아닐 때 이전 화면으로 되돌아가는 기능을 합니다. 물론 안드로이드 폰은 뒤로가기 버튼이 따로 있어서 언제든지 이전 화면으로 되돌아갈 수 있지만, 액션바 왼쪽에 이전 화면으로 되돌아가는 화살표 모양의 업 버튼을 제공할 수 있습니다.
업 버튼은 액티비티가 등록되는 매니페스트 파일에서 설정하는 방법과 액티비티 코드로 설정하는 방법이 있습니다.
<매니페스트 파일에서 업 버튼 설정>
<activity
android:name=".TwoActivity"
android:parentActivityName=".MainActivity"></activity>
매니페스트 파일에서 <activity> 태그에 parentActivityName 속성을 등록하는 것만으로도 액티비티 화면에 업 버튼이 나옵니다. 그래서 사용자가 이 버튼을 누르면 이전 화면으로 되돌아갑니다.
이번에는 매니페스트 파일에 parentActivityName 속성을 선언하지 않고 액티비티 코드로 업 버튼이 나오게 하는 방법을 알아보겠습니다.
class TwoActivity : AppcompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
override fun onSupportNavigateUp(): Boolean {
Log.d("kkang", "onSupportNavigateUp")
onBackPressed()
return super.onSupportNavigateUp()
}
}
3. 프래그먼트 - 액티비티처럼 동작하는 뷰
프래그먼트는 텍스트 뷰나 버튼처럼 액티비티 화면을 구성하는 뷰인데, 그 자체만으로는 화면에 아무것도 출력되지 않습니다. 프래그먼트가 다른 뷰와 다른 점은 액티비티처럼 동작한다는 것입니다. 즉, 액티비티에 작성할 수 있는 모든 코드는 프래그먼트에도 사용할 수 있습니다.
프래그먼트는 androidx.fragment 라이브러리에서 제공합니다.
프래그먼트는 뷰이지만 그 자체로는 화면에 아무것도 출력되지 않습니다. 따라서 먼저 프래그먼트 화면을 구성하는 레이아웃 XML 파일을 작성해야 합니다. 이 작업은 액티비티를 만들 때 레이아웃 XML 파일을 작성했던 것과 차이가 없습니다.
프래그먼트는 Fragment를 상속받아 작성하는 클래스입니다. 프래그먼트 클래스에 최소한으로 작성해야 하는 함수는 onCreateView() 입니다. 이 함수가 자동 호출되며 반환한 View 객체가 화면에 출력됩니다.
import androidx.fragment.app.Fragment
class OneFragment : Fragment() {
lateinit var binding: FragmentOneBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentOneBinding.inflate(inflater, container, false)
return binding.root
}
}
액티비티의 레이아웃 XML에 등록하여 프래그먼트 출력
프래그먼트도 뷰이므로 액티비티의 화면을 구성하는 레이아웃 XML에 등록하여 액티비티 화면에 나오게 할 수 있습니다. 다음 코드는 액티비티의 화면을 구성하는 XML 내용입니다.
<fragment
android:name="com.example.test11.OneFragment"
android:id="@+id/fragmentView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
액티비티 코드에서 프래그먼트 출력
액티비티의 레이아웃 XML에서 프래그먼트를 출력할 수도 있지만 때로는 코드에서 직접 프래그먼트 객체를 생성하여 화면에 출력해야 할 수도 있습니다. 이럴 때는 우선 액티비티의 레이아웃 XML 파일에 프래그먼트가 출력될 뷰를 하나 준비합니다.
<LinearLayout
android:id="@+id/fragment_content
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
</LinearLayout>
이 LinearLayout에 코드에서 동적으로 생성한 프래그먼트를 출력할 수 있습니다. 물론 LinearLayout이 아닌 다른 레이아웃 클래스를 이용해도 됩니다.
그리고 코드에서 프래그먼트를 동적으로 제어하려면 FragmentManager로 만든 FragmentTransaction 클래스의 함수를 이용합니다.
val fragmentManager: FragmentManager = supportFragmentManager
val transaction: FragmentTransaction = fragmentManager.beginTransaction()
val fragment = OneFragment()
transaction.add(R.id.fragment_content, fragment)
transaction.commit()
코드에서 동적으로 프래그먼트를 제어하는 함수는 add() 외에 replace(), remove()가 있습니다.
- add(int containerViewId, Fragment fragment): 새로운 프래그먼트를 화면에 추가합니다.
- replace(int containerViewId, Fragment fragment): 추가된 프래그먼트를 대체합니다.
- remove(Fragment fragment): 추가된 프래그먼트를 제거합니다.
- commit(): 화면에 적용합니다.
프래그먼트 생명주기
프래그먼트는 액티비티처럼 동작하는 뷰입니다. 그러므로 액티비티에 작성하는 코드는 모두 프래그먼트에도 작성할 수 있습니다. 따라서 프래그먼트도 액티비티와 생명주기가 같습니다. 아래 그림은 프래그먼트의 생명주기를 설명한 것입니다.
프래그먼트도 액티비티의 생명주기 함수인 onCreate(), onStart(), onResume(), onPause(), onStop(), onDestroy()를 그대로 가지고 있으며 호출되는 시점도 액티비티와 같습니다. 즉, 액티비티의 onStart() 함수가 호출되는 순간 해당 액티비티가 출력하는 프래그먼트의 onStart() 함수도 호출됩니다.
- onAttach() : 프래그먼트가 액티비티에 포함되는 순간 호출
- onCreateView() : 프래그먼트의 화면을 구성할 때 호출. 이 함수가 반환하는 뷰를 프래그먼트 화면에 출력
- onActivityCreated() : 프래그먼트의 액티비티가 생성된 순간 호출
- onDestroyView() : 프래그먼트가 화면에서 사라진 후 백 스택에 추가될 떄 호출
- onDetach() : 프래그먼트가 액티비티에서 제거될 때 호출
그런데 프래그먼트의 생명주기를 자세히 보면 onResume() 함수까지 호출되어 활성(Active) 상태가 된 후의 생명주기 흐름이 2가지로 나뉩니다. 이는 프래그먼트가 백 스택(back stack)에 포함되었는지에 따라 다릅니다. 백 스택은 프래그먼트가 화면에 보이지 않는 순간 제거하지 않고 저장했다가 다시 이용할 수 있게 하는 기능입니다.
만약 백 스택을 이용하지 않는다면 프래그먼트의 생명주기가 왼쪽 화살표의 흐름대로 진행됩니다. 프래그먼트가 화면에서 제거(remove)되거나 대체(replace)되는 순간 onPause() → onStop() → onDestroyView() → onDestroy() → onDetach() 함수까지 호출됩니다. 이런 과정을 거치면 프래그먼트가 완벽히 제거되므로 뒤로가기 버튼을 눌러도 다시 나오지 않습니다.
그런데 백 스택을 이용한다면 프래그먼트가 화면에서 제거되거나 대체되는 순간 onPause() → onStop() → onDestroyView() 까지만 호출됩니다. 즉, 완벽하게 제거되지 않습니다. 따라서 사용자가 기기의 뒤로가기 버튼을 누르면 다시 onCreateView()부터 활성 상태까지의 함수들이 호출됩니다.
이러한 백 스택을 사용할 것인지는 FragmentTransaction의 addToBackStack() 함수로 설정합니다.
transaction.addToBackStack(null)
4. 리사이클러 뷰 - 목록 화면 구성
일반적으로 앱을 사용하다 보면 여러 가지 항목을 나열하는 목록 화면이 많다는 것을 알 수 있습니다. 리사이클러 뷰는 이러한 목록 화면을 만들 때 사용합니다.
리사이클러 뷰는 목록을 만드는데 RecyclerView 클래스만으로는 화면에 아무것도 출력되지 않습니다. 그러므로 다음과 같은 구성 요소를 이용해야 합니다.
- ViewHolder(필수) : 항목에 필요한 뷰 객체를 가집니다.
- Adapter(필수) : 항목을 구성합니다.
- LayoutManager(필수) : 항목을 배치합니다.
- ItemDecoration(옵션) : 항목을 꾸밉니다.
먼저 뷰 홀더는 각 항목을 구성하는 뷰 객체를 가집니다. 그리고 어댑터는 뷰 홀더에 있는 뷰 객체에 적절한 데이터를 대입해 항목을 완성합니다. 그다음 레이아웃 매니저는 어댑터가 만든 항목들을 어떻게 배치할지 결정하여 리사이클러 뷰에 출력합니다.
리사이클러 뷰를 이용하려면 그래들 파일의 dependencies 항목에 다음처럼 선언해야 합니다.
implementation 'androidx.recyclerview:recyclerview:1.1.0'
그리고 리사이클러 뷰를 레이아웃 XML 파일에 등록합니다.
<activity_recycler_view.xml>
<androidx.recyclerview.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
또한 목록에 표시할 항목을 디자인한 레이아웃 XML 파일도 필요합니다. 여기서는 item_main.xml 파일에 다음처럼 각 항목에 문자열 데이터가 나오게 작성했다고 가정하겠습니다.
<item_main.xml>
<LinearLayout
android:id="@+id/item_root"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp">
<TextView
android:id="@+id/item_data"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:textStyle="bold"
android:textSize="16dp" />
</LinearLayout>
뷰 홀더 준비
각 항목에 해당하는 뷰 객체를 가지는 뷰 홀더는 RecyclerView.ViewHolder 를 상속받아 작성합니다.
<MyViewHolder.kt>
class MyViewHolder(val binding: ItemMainBinding): RecyclerView.ViewHolder(binding.root)
원래는 뷰 홀더에 항목들의 뷰 객체를 선언하고 findViewById로 가져와야 합니다. 하지만 뷰 바인딩 기법을 이용하면 뷰 홀더는 항목 레이아웃 XML 파일에 해당하는 바인딩 객체만 가지고 있으면 됩니다. 이 바인딩 객체에 항목을 구성하는 뷰가 자동으로 선언되었으므로 짧은 코드로 작성할 수 있습니다.
어댑터 준비
어댑터는 뷰 홀더의 뷰에 데이터를 출력해 각 항목을 만들어주는 역할을 합니다. 리사이클러뷰를 위한 어댑터는 RecyclerView.Adapter를 상속받아 작성합니다.
<MyAdapter.kt>
class MyAdapter(val datas: List<String>): RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun getItemCount(): Int {
return datas.size
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
MyViewHolder(ItemMainBinding.inflate(LayoutInflater.from(parent.context), parent, false))
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
Log.d("kkang", "onBindViewHolder : $position")
val binding = (holder as MyViewHolder).binding
// 뷰에 데이터 출력
binding.itemData.text = datas[position]
// 뷰에 이벤트 추가
binding.itemRoot.setOnClickListener {
Log.d("kkang", "item root click : $position")
}
}
}
위 코드에서 이름이 MyAdapter인 어댑터를 만들었으며 MyAdapter 생성자의 매개변수는 액티비티에서 전달받는 항목 수정용 데이터입니다. 어댑터에 재정의해야 하는 함수는 다음과 같습니다.
- getItemCount() : 항목 개수를 판단하려고 자동으로 호출됩니다.
- onCreateViewHolder() : 항목의 뷰를 가지는 뷰 홀더를 준비하려고 자동으로 호출됩니다.
- onBindViewHolder() : 뷰 홀더의 뷰에 데이터를 출력하려고 자동으로 호출됩니다.
리사이클러 뷰 출력
어댑터를 준비했으면 마지막으로 리사이클러 뷰에 어댑터와 레이아웃 매니저를 등록해 화면에 출력합니다.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityRecyclerViewBinding.inflate(layoutInflater)
setContentView(binding.root)
val datas = mutableListOf<String>()
for(i in 1..10) {
datas.add("Item $i")
}
binding.recyclerView.layoutManager = LinearLayoutManager(this)
binding.recyclerView.adapter = MyAdapter(datas)
binding.recyclerView.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
}
}
리사이클러 뷰에 항목이 출력된 후 동적으로 새로운 항목을 추가하거나 화면에 보이는 항목을 제거해야 할 때가 있습니다. 이 작업은 항목을 구성하는 데이터에 새로운 데이터를 추가하거나 제거한 후 어댑터의 notifyDataSetChanged() 함수를 호출하면 됩니다.
datas.add("new data")
adapter.notifyDataSetChanged()
레이아웃 매니저
레이아웃 매니저는 어댑터로 만든 항목을 리사이클러 뷰에 배치합니다. 레이아웃 매니저는 RecyclerView.LayoutManager를 상속받은 클래스로, 라이브러리에서 다음처럼 제공합니다.
- LinearLayoutManager : 항목을 가로나 세로 방향으로 배치합니다.
- GridLayoutManager : 항목을 그리드로 배치합니다.
- StaggeredGridLayoutManager : 항목을 높이가 불규칙한 그리드로 배치합니다.
항목을 가로나 세로 방향으로 배치하고 싶다면 LinearLayoutManager를 사용합니다. 보통 이 레이아웃 매니저를 가장 많이 사용합니다.
binding.recyclerView.layoutManager = LinearLayoutManager(this)
방향을 설정하지 않으면 세로가 기본입니다. 만약 항목을 가로로 배치하고 싶다면 LinearLayoutManager의 orientation값을 LinearLayoutManager.HORIZONTAL 로 지정합니다.
val layoutManager = LinearLayoutManager(this)
layoutManager.orientation = LinearLayoutManager.HORIZONTAL
binding.recyclerView.layoutManager = layoutManager
항목을 그리드로 배치하고 싶다면 GridLayoutManager를 이용합니다.
val layoutManager = GridLayoutManager(this, 2)
binding.recyclerView.layoutManager = layoutManager
GridLayoutManager 생성자의 숫자는 그리드에서 열의 개수를 뜻합니다. 2로 지정하면 2열로, 3으로 지정하면 3열로 구성합니다.
StaggeredGridLayoutManager는 GridLayoutManager처럼 뷰를 그리드 구조를 배치합니다. 그런데 각 뷰의 크기가 다르면 지그재그 형태로 배치합니다.
val layoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
binding.recyclerView.layoutManager = layoutManager
아이템 데커레이션
아이템 데커레이션은 리사이클러 뷰를 다양하게 꾸밀 때 사용합니다. 아이템 데커레이션은 필수가 아니므로 필요하면 리사이클러 뷰에 적용하면 됩니다.
라이브러리에서 제공하는 아이템 데커레이션은 항목의 구분선을 출력해 주는 DividerItemDecoration 뿐입니다. 결국 아이템 데커레이션은 대부분 ItemDecoration을 상속받는 개발자 클래스를 만들고 이 클래스에서 다양한 꾸미기 작업을 합니다.
class MyDecoration(val context: Context): RecyclerView.ItemDecoration() {
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDraw(c, parent, state)
}
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDrawOver(c, parent, state)
}
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
}
}
아이템 데커레이션은 다음과 같은 꾸미기 작업을 하는 함수 3개를 제공합니다.
- onDraw() : 항목이 배치되기 전에 호출됩니다.
- onDrawOver() : 항목이 모두 배치된 후 호출됩니다.
- getItemOffsets() : 개별 항목을 꾸밀 때 호출됩니다.
onDraw() 함수는 항목이 화면에 배치되기 전에 호출됩니다. 이 함수의 매개변수로 전달되는 Canvas 객체로 각종 그림을 그릴 수가 있습니다. onDraw() 함수가 그린 그림 위에 항목이 나타납니다.
onDrawOver() 함수는 모든 항목이 화면에 배치된 후 호출됩니다. 이 함수의 매개변수로 전달되는 Canvas 객체로 그림을 그리며 항목 위에 이 그림이 나타납니다. 다음은 리소스 이미지를 리사이클러 가운데에 그린 예입니다.
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDrawOver(c, parent, state)
// 뷰 크기 계산
val width = parent.width
val height = parent.height
// 이미지 크기 계산
val dr: Drawable? = ResourcesCompat.getDrawable(context.getResources(), R.drawable.kbo, null)
val drWidth = dr?.intrinsicWidth
val drHeight = dr?.intrinsicHeight
// 이미지가 그려질 위치 계산
val left = width / 2 - drWidth?.div(2) as Int
val top = height / 2 - drHeight?.div(2) as Int
c.drawBitmap(
BitmapFactory.decodeResource(context.getResources(), R.drawable.kbo),
left.toFloat(),
top.toFloat(),
null
)
}
getItemOffsets() 함수는 항목 하나당 한 번씩 호출되어 각 항목을 꾸미는 데 사용합니다. 매개변수로 전달되는 Rect 객체는 각 항목을 화면에 출력할 때 필요한 사각형 정보입니다.
override fun getItemOffsets (
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
val index = parent.getChildAdapterPosition(view) + 1
if (index % 3 == )
outRect.set(10, 10, 10, 60) // left, top, right, bottom
else
outRect.set(10, 10, 10, 0)
view.setBackgroundColor(Color.LTGRAY)
ViewCompat.setElevation(view, 20.0f)
}
이렇게 만든 아이템 데커레이션 객체를 리사이클러 뷰에 적용할 떄는 addItemDecoration() 함수를 이용합니다.
bindig.recyclerView.addItemDecoration(MyDecoration(this))
5. 뷰 페이저2 - 스와이프로 넘기는 화면 구성
뷰 페이저는 스와이프(손가락으로 화면을 탭하여 오른쪽이나 왼쪽으로 미는) 이벤트로 화면을 전환할 때 사용합니다. 뷰 페이저는 플랫폼 API에서 제공하지 않으므로 androidx 라이브러리를 이용해 개발해야 합니다. 그런데 개발자가 오랫동안 이용했던 viewpager와 별개로 2019년에 viewpager2를 제공하기 시작했습니다. viewpager 보다 viewpager2 라이브러리의 기능이 더 많으므로 이 책에서는 뷰 페이저2를 사용해서 설명하겠습니다.
뷰 페이저2를 이용하려면 그래들 파일의 dependencies 항목에 다음처럼 선언해야 합니다.
implementation 'androidx.viewpager2:viewpage2:1.0.0'
그리고 레이아웃 XML 파일에 뷰 페이저2를 추가합니다.
<androidx.viewpager2.widget.ViewPager2
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
뷰 페이저2는 화면을 항목으로 봅니다. 각 항목이 순서대로 나열되어 있는데 단지 한 화면에 항목 하나가 나온다는 개념입니다. 따라서 리사이클러 뷰에서 살펴봤던 어댑터를 적용해야 합니다. 이 뷰페이저2에 사용할 수 있는 어댑터는 2가지인데 리사이클러 뷰에서 봤던 Recycler.View.Adapter를 그대로 이용하거나 FragmentStateAdapter를 사용할 수도 있습니다.
리사이클러 뷰 어댑터 이용
RecyclerView.Adapter는 리사이클러 뷰에서 살펴봤던 내용과 차이가 없습니다. 똑같이 작성하되 단지 뷰 페이저2의 어댑터로 적용만 하면 됩니다.
class MyPagerViewHolder(val binding: ItemPagerBinding) : RecyclerView.ViewHolder(binding.root)
class MyPagerAdapter(val datas: MutableList<String>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun getItemCount(): Int {
return datas.size
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = MyPagerViewHolder(ItemPagerBinding.inflate(LayoutInflater.from(parent.context), parent, false))
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val binding = (holder as MyPagerViewHolder).binding
// 뷰에 데이터 출력
binding.itemPagerTextView.Text = datas[position]
when (position % 3) {
0 -> binding.itemPagerTextView.setBackgroundColor(Color.RED)
1 -> binding.itemPagerTextView.setBackgroundColor(Color.BLUE)
2 -> binding.itemPagerTextView.setBackgroundColor(Color.GREEN)
}
}
}
화면 3개를 뷰 페이저 2로 제공하는 어댑터입니다. 각 화면을 구성하는 레이아웃 XML 파일에는 텍스트 뷰가 하나 선언되었다고 가정한 것입니다. 이 어댑터를 뷰 페이저2에 적용하면 됩니다.
binding.viewpager.adapter = MyPagerAdapter(datas)
(왜인지는 모르겠지만 리사이클러 뷰 어댑터를 이용한 뷰 페이저구현시 앱 실행 후 죽는 증상때문에 확인이 안되었습니다... 원인을 아직 확인못했기에 프래그먼트 어댑터를 사용해야 할 것 같아요.)
프래그먼트 어댑터 이용
RecyclerView.Adapter를 이용해 뷰 페이저2를 구현할 수도 있지만 대부분 각 항목(화면)은 복잡하게 작성됩니다. 따라서 각 항목의 내용은 보통 프래그먼트로 작성합니다. 만약 항목을 프래그먼트로 작성했으면 FragmentStateAdapter로 뷰 페이저2를 구현합니다.
<MainActivity.kt>
package com.example.myapplication10
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.example.myapplication10.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.viewpager.adapter = ViewPagerAdapter(this)
}
}
<ViewPagerAdapter.kt>
package com.example.myapplication10
import android.util.Log
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.example.myapplication10.fragments.OneFragment
import com.example.myapplication10.fragments.ThreeFragment
import com.example.myapplication10.fragments.TwoFragment
class ViewPagerAdapter(activity: MainActivity): FragmentStateAdapter(activity) {
val fragments: List<Fragment>
init {
fragments = listOf(OneFragment(), TwoFragment(), ThreeFragment())
Log.d("kkang", "fragments size : ${fragments.size}")
}
override fun getItemCount(): Int = fragments.size
override fun createFragment(position: Int): Fragment = fragments[position]
}
<OneFragment.kt>
package com.example.myapplication10.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.example.myapplication10.databinding.OneFragmentBinding
class OneFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding = OneFragmentBinding.inflate(inflater, container, false)
binding.itemText.text = "첫번째 화면"
return binding.root
}
}
<TwoFragment.kt>
package com.example.myapplication10.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.example.myapplication10.databinding.TwoFragmentBinding
class TwoFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding = TwoFragmentBinding.inflate(inflater, container, false)
binding.itemText.text = "두번째 화면"
return binding.root
}
}
<ThreeFragment.kt>
package com.example.myapplication10.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.example.myapplication10.databinding.ThreeFragmentBinding
class ThreeFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding = ThreeFragmentBinding.inflate(inflater, container, false)
binding.itemText.text = "세번째 화면"
return binding.root
}
}
<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:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<one_fragment.xml>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/item_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="17dp"/>
</LinearLayout>
<two_fragment.xml>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/item_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="17dp"/>
</LinearLayout>
<three_fragment.xml>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/item_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="17dp"/>
</LinearLayout>
앱을 실행하면 뷰 페이저 화면이 동작하는 것을 확인할 수 있다.
6. 드로어 레이아웃 - 옆에서 열리는 화면 구성
드로어 레이아웃은 액티비티 화면에 보이지 않던 내용이 왼쪽이나 오른쪽에서 손가락의 움직임에 따라 밀려 나오는 기능을 합니다. androidx의 라이브러리인 드로어 레이아웃은 마치 서랍(drawer)처럼 열리는 메뉴를 구성할 때 사용합니다.
드로어 레이아웃을 액티비티에 적용하려면 액티비티의 레이아웃 XML 파일 구성이 중요합니다. 레이아웃 XML 파일에서 드로어 메뉴가 출력되어야 하는 부분의 태그를 DrawerLayout 으로 선언합니다. 액티비티 레이아웃 XML 파일의 루트 태그가 꼭 DrawerLayout일 필요는 없습니다. 하지만 대부분은 액티비티의 기본 화면이 보이고 그 위를 덮듯이 나오게 하므로 액티비티 레이아웃 XML 파일의 루트 태그를 DrawerLayout으로 선언합니다. 그리고 DrawerLayout 아래에는 뷰를 2개 선언해야 합니다. 이렇게 선언만 해주면 자동으로 첫 번째 하위 태그 부분을 액티비티 화면에 출력하고 두 번째 하위 태그 부분이 안 보이다가 끌려 나옵니다.
<androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/drawer"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout ... 생략 ...>
(... 생략 ...)
</LinearLayout>
<TextView android:layout_gravity="start" />
</androidx.drawerlayout.widget.DrawerLayout>
최상위에는 <androidx.drawerlayout.widget.DrawerLayout> 태그를, 그 아래에는 <LinearLayout>과 <TextView> 태그를 선언했습니다. 하위 태그는 어떤 태그여도 상관없습니다. 태그가 2개여야 하는 것이 중요합니다. 이렇게 XML 설정만 해도 첫 번째 태그에 해당하는 화면이 알아서 나오고 사용자가 화면 끝을 밀면 두 번째 태그에 해당하는 화면이 나타납니다.
레이아웃 XML만으로 드로어 레이아웃을 이용할 수 있는데, 대부분 툴바 영역에 토글(toggle) 버튼을 함께 제공합니다. 드로어 메뉴 토글 버튼은 ActionBarDrawerToggle 클래스에서 제공합니다.
class DrawerActivity : AppCompatActivity {
lateinit var toggle: ActionBarDrawerToggle
override run onCreate(savedInstanceState: Bundle?) {
(... 생략 ...)
// ActionBarDrawerToggle 버튼 적용
toggle = ActionBarDrawerToggle(this, binding.drawer, R.string.drawer_opened, R.string.drawer_closed)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
toggle.syncState()
}
override run onOptionsItemSelected(item: MenuItem): Boolean {
// 이벤트가 토글 버튼에서 발생하면
if (toggle.onOptionsItemSelected(item)) {
return true;
}
return super.onOptionsItemSelected(item)
}
}
ActionBarDrawerToggle 생성자의 두 번째 매개변수는 토글 버튼으로 여닫는 드로어 객체입니다. 그리고 세 번째와 네 번째 매개변수는 문자열 리소스로, 드로어가 열리거나 닫혔을 때의 상태를 표현한 문자열입니다.
supportActionBar?.setDisplayHomeAsUpEnabled(true) 구문은 토글 버튼으로 사용할 아이콘이 출력되게 합니다. 그런데 원래 액션바 영역의 HomeAsUp 아이콘은 왼쪽 화살표 모양입니다. 만약 드로어를 열기 위해 제공하는 네비게이션 아이콘이 나오게 하려면 toggle.syncState() 함수까지 설정해 줘야 합니다.
토글 버튼을 제어하려면 액션바 메뉴 이벤트를 처리하는 함수인 onOptionsItemSelected()에서 토글 버튼의 이벤트를 처리해 줘야 합니다. 그래야지만 토글 버튼으로 드로어를 제어할 수 있습니다.
<MainActivity.kt>
package com.example.myapplication11
import android.os.Bundle
import android.view.MenuItem
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AppCompatActivity
import com.example.myapplication11.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
lateinit var toggle: ActionBarDrawerToggle
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
toggle = ActionBarDrawerToggle(this, binding.drawer, R.string.drawer_opened,
R.string.drawer_closed)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
toggle.syncState()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if(toggle.onOptionsItemSelected(item)){
return true
}
return super.onOptionsItemSelected(item)
}
}
<activity_main.xml>
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawer"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:elevation="4dp"
app:theme="@style/ToolbarIconColor"
app:titleTextColor="#FFFFFF"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="bold"
android:text="Main Activity!"/>
</LinearLayout>
<TextView
android:layout_width="300dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="#FF0000"
android:fitsSystemWindows="true"
android:gravity="center_horizontal"
android:text="I am Drawer"
android:textColor="#FFFFFF"
android:textSize="20dp"
android:textStyle="bold"/>
</androidx.drawerlayout.widget.DrawerLayout>
<strings.xml>
<resources>
<string name="app_name">My Application</string>
<string name="drawer_opened">Opened Drawer</string>
<string name="drawer_closed">Closed Drawer</string>
</resources>
앱을 실행하면 드로어 레이아웃을 확인할 수 있다.
'Android' 카테고리의 다른 글
[깡쌤의 안드로이드 프로그래밍 with 코틀린][5. 컴포넌트 이해하기] 13. 액티비티 컴포넌트 (0) | 2024.12.25 |
---|---|
[깡쌤의 안드로이드 프로그래밍 with 코틀린][4. 구글의 라이브러리로 화면 구성하기] 12. 머티리얼 라이브러리 (0) | 2024.12.25 |
[깡쌤의 안드로이드 프로그래밍 with 코틀린][3. 앱의 기본 기능 구현하기] 10. 다이얼로그와 알림 이용하기 (0) | 2024.12.25 |
[깡쌤의 안드로이드 프로그래밍 with 코틀린][3. 앱의 기본 기능 구현하기] 9. 리소스 활용하기 (0) | 2024.12.25 |
[깡쌤의 안드로이드 프로그래밍 with 코틀린][3. 앱의 기본 기능 구현하기] 8. 사용자 이벤트 처리하기 (0) | 2024.12.25 |