리사이클러 뷰 기초 사용법
리사이클러 뷰는 목록 화면을 만들 때 사용한다.
RecyclerView 클래스만으로는 화면에 아무것도 출력되지 않는다. 다음과 같은 구성 요소가 필요하다.
- ViewHolder(필수): 항목에 필요한 뷰 객체를 가진다.
- Adapter(필수): 항목을 구성한다. 뷰 홀더에 있는 뷰 객체에 적절한 데이터를 대입해 항목을 완성한다.
- LayoutManager(필수): 항목을 배치한다. 어댑터가 만든 항목들을 어떻게 배치할지 결정하여 리사이클러 뷰에 출력한다.
- ItemDecoration(옵션): 항목을 꾸민다.
예를 들어 카카오톡 채팅방 목록 화면을 리사이클러 뷰로 만든다 할 때,
뷰 홀더는 각 뷰가 들어갈 자리? 틀? 느낌이고, 어댑터는 각각의 뷰에 이미지뷰, 텍스트 뷰 등등 어떤거 들어가는지 구성하는 역할이고, 레이아웃 매니저로 그런 뷰들을 배치하고, 일르 리사이클러 뷰에 출력함.
❓리스트 뷰가 아닌 리사이클러 뷰를 쓰는 이유?
❗️androidx 라이브러리에서 제공하는 리사이클러 뷰로 리스트 뷰로 만들기 어렵거나 복잡한 목록을 만들 수 있다.
(개정판 삭제 내용)
리사이클러 뷰를 이용하려면 그래들 파일의 dependencies 항목에 다음처럼 선언해야 한다.
// 리사이클러 뷰 선언
implementation 'androidx.recyclerview:recyclerview:1.2.1'
*안드로이드 스튜디오 4.1버전부터 머티리얼 디자인 라이브러리를 자동으로 추가해주고 있다. 머티리얼 라이브러리 내부에서 androidx의 많은 라이브러리를 이용하고 있어서 따로 위 처럼 선언하지 않아도 recyclerview를 사용할 수 있다.
그리고 리사이클러 뷰를 레이아웃 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 파일에 다음처럼 각 항목에 문자열 데이터가 나오게 작성했다고 가정하자.
// 목록에 표시할 항목을 디자인한 레이아웃 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를 상속받아 작성한다.
// 뷰 홀더 준비
class MyViewHolder(val binding: ItemMainBinding): RecyclerView.ViewHolder(binding.root)
원래는 1️⃣ 뷰 홀더에 항목들의 뷰 객체를 선언하고 2️⃣ findViewById로 가져와야 한다. 하지만 뷰 바인딩 기법을 이용하면 뷰 홀더는 항목 레이아웃 XML파일에 해당하는 바인딩 객체만 가지고 있으면 된다. 이 바인딩 객체에 항목을 구성하는 뷰가 자동으로 선언되었으므로 짧은 코드로 작성할 수 있다.
어댑터 준비
어댑터는 뷰 홀더의 뷰에 데이터를 출력해 각 항목을 만들어주는 역할을 한다. 리사이클러 뷰를 위한 어댑터는 RecyclerView.Adapter를 상속받아 작성한다.
// 어댑터 준비
class MyAdapter(val binding: ItemMainBinding):
RecyclerView.Adapter<RecyclerView.ViewHolder>() { // RecyclerView.Adapter를 상속받음~
override fun getItemCount(): Int { // 항목 개수 판단하려고 자동 호출
TODO("Not yet implemented")
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):
RecyclerView.ViewHolder { // 뷰 홀더 준비하려고 자동 호출
TODO("Not yet implemented")
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
TODO("Not yet implemented") // 뷰 홀더에 데이터 출력하려고 자동 호출
}
}
이 코드에서 이름이 MyAdapter인 어댑터를 만들었으며 MyAdaper 생성자의 매개변수는 액티비티에서 전달받는 항목 구성용 데이터이다. 어댑터에 재정의해야 하는 함수는 다음과 같다.
- getItemCount(): 항목 개수를 판단하려고 자동으로 호출된다. (이 함수가 반환한 숫자만큼 onBindViewHolder 함수가 호출되어 항목을 만든다. 만약 이 함수가 0을 반환하면 화면에는 아무것도 나오지 않는다)
- onCreateViewHolder(): 항목의 뷰를 가지는 뷰 홀더를 준비하려고 자동으로 호출된다.
- onBindViewHolder(): 뷰 홀더의 뷰에 데이터를 출력하려고 자동으로 호출된다.
// 항목의 개수 구하기
override fun getItemCount(): Int = datas.size
onCreateViewHolder() 함수는 항목을 구성할 때 이용할 뷰 홀더 객체를 준비한다.
// 항목 구성에 필요한 뷰 홀더 객체 준비
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
MyViewHolder(itemMainBinding.inflate(Layoutinflater.from(parent.context),
parent, false))
뷰 바인딩 기법으로 뷰 홀더 객체를 생성해 반환하는 구문이다. onCreateViewHolder() 함수에서 반환한 뷰 홀더 객체는 자동으로 onBindViewHolder() 함수의 매개변수로 전달된다.
// 뷰에 데이터 출력
override fun onBindViewHolder(holder: RecyclerView.viewHolder, position: Int) {
// 뷰 홀더 객체가 매개변수로 전달됨
Log.d("kkang", "onBindViewHolder : $position")
val biinding = (holder as MyViewHolder).binding
// 뷰에 데이터 출력
binding.itemData.text = datas[position]
binding.itemRoot.setOnclickListener {
Log.d("kkang", "item root click : $position")
}
}
onBindViewHolder() 함수에서 매개변수로 전달된 뷰 홀더 객체의 뷰에 데이터를 출력하거나 필요한 이벤트를 등록한다. onBindviewHolder() 함수의 두 번째 매개변수가 항목의 인덱스이다.
리사이클러 뷰 출력
어댑터를 준비했으면 마지막으로 리사이클러 뷰에 어댑터와 레이아웃 매니저를 등록해 화면에 출력한다.
// 리사이클러 뷰 출력
class RecyclerViewActivity : 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,
LinearLayout Manager.VERTICAL))
}
}
항목을 동적으로 추가, 제거
리사이클러 뷰에 항목이 출력된 후 동적으로 새로운 항목을 추가하거나 화면에 보이는 항목을 제거해야 할 때가 있다. 이 작업은 항목을 구성하는 데이터에 새로운 데이터를 추가하거나 제거한 후 어댑터의 notifyDataSetChanged() 함수를 호출하면 된다.
// 항목 추가
datas.add("new data")
adapter.notifyDataSetChanged()
레이아웃 매니저
레이아웃 매니저는 어댑터로 만든 항목을 리사이클러 뷰에 배치한다. 레이아웃 매니저는 RecyclerView.LayoutManager를 상속받은 클래스로, 라이브러리에서 다음처럼 제공한다.
- LinearLayoutManager: 항목을 가로나 세로 방향으로 배치한다.
- GridLayoutManager: 항목을 그리드로 배치한다.
- StaggeredGridLayoutmanager: 항목을 높이가 불규칙한 그리드로 배치한다.
항목을 가로, 세로 방향으로 배치
항목을 가로나 세로 방향으로 배치하고 싶다면 LinearlayoutManager를 사용한다. 보통 이 레이아웃 매니저를 가장 많이 이용한다.
// 항목을 세로로 배치
binding.recyclerView.layoutmanager =
LinearLayoutManager(this)
이 코드는 리사이클러 뷰의 layoutManager 프로퍼티에 LinearLayoutManager를 등록한 것이며 방향 설정은 하지 않았다. 이처럼 방향을 설정하지 않으면 세로가 기본 적용된다. 만약 항목을 가로로 배치하고 싶으면 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 생성자의 숫자는 그리드에서 열(column)의 개수를 뜻한다. 2로 지정하면 2열, 3으로 지정하면 3열로 구성한다.
GridLayoutManager도 방향을 설정할 수 있다.
만약 가로로 설정하려면 생성자에 GridLayoutManager.HORIZONTAL을 지정한다.
// 그리드에서 항목을 가로로 배치
val layoutManager = GridLayoutManager(this, 3,
GridLayoutManager.HORIZONTAL, false)
binding.recyclerView.layoutManager = layoutManager
GridLayoutManager 생성자의 네 번째 매개변수에 Boolean 값을 설정할 수 있는데 이를 false로 지정했다. true로 지정하면 세로 방향일 때는 뷰가 아래부터(역순) 배치되며 가로 방향일 때는 오른쪽부터 배치된다.
// 그리드에서 항목을 오른쪽부터 배치
val layoutManager+GridLayoutManager(this, 3,
GridLayoutManager.HORIZONTAL, true)
binding.recyclerView.layoutManager = layoutManager
높이가 불규칙한 그리드로 배치하기
StaggeredGridLayoutManager는 GridLayoutManager처럼 뷰를 그리드 구조로 배치한다.
그런데 각 뷰의 크기가 다르면 지그재그 형태로 배치한다. (모의고사 시험지 느낌으로 2단 구성)
// 지그재그 그리드 형태로 배치
val layoutManager = StaggeredGridlayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
binding.recyclerVieew.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() 함수가 그린 그림 위에 항목이 나타난다.
// 항목이 배치되기 전에 호출되는 onDraw() 함수
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerVies.State) { // 캔버스 객체~
super.onDraw(c. parent, state)
c.drawBitmap(BitmapFactory.decodeResource(context.getResources(), R.drawable.stadium),
0f, 0f, null)
}
onDrawOver() 함수는 모든 항목이 화면에 배치된 후 호출된다. 이 함수의 매개변수로 전달되는 Canvas 객체로 그림을 그리며 항목 위에 이 그림이 나타난다. 다음은 리소스 이미지를 리사이클러 가운데에 그린 예이다.
// 모든 항목이 배치된 후 호출되는 onDrawOver() 함수
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
)
}
getItemsOffsets() 함수는 항목 하나당 한 번씩 호출되어 각 항목을 꾸미는 데 사용한다. 매개변수로 전달되는 Rect 객체는 각 항목을 화면에 출력할 때 필요한 사각형 정보이다. 이 사각형 정보를 이용해 항목에서 네 방향(left, top, right, bottom)의 여백을 설정할 수 있다. 또한 각 항목의 바탕색 등도 설정한다.
// 개별 항목을 꾸미는 getItemOffsets() 함수
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 == 0)
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() 함수를 이용한다.
// 리사이클러 뷰에 아이템 데커레이션 적용
binding.recyclerView.addItemDecoration(MyDecoration(this))
공부일기
음... 이번달에 끝내기는 힘들것같지만 그래도 공부하면서 핸드폰 보면 오 이건 무슨무슨 뷰인가~? 하는 생각도 들고 갖고놀줄만 알았던 폰을 직접 구현할 수 있을 것 같아서 설렌다. 내가 좋아하는 걸(귀여운거) 컨텐츠로 만들어보고 싶다
'깡샘 코틀린' 카테고리의 다른 글
11-6 드로어 레이아웃 - 옆에서 열리는 화면 구성 (0) | 2023.06.29 |
---|---|
11-5 뷰 페이저2 - 스와이프로 넘기는 화면 구성 (0) | 2023.06.29 |
11-3 프래그먼트 - 액티비티처럼 동작하는 뷰 (0) | 2023.06.29 |
11-2 appcompat 라이브러리 - API 호환성 해결 (0) | 2023.06.29 |
11-1 제트팩과 androidx 소개 (0) | 2023.06.27 |