본문 바로가기
깡샘 코틀린

11-4 리사이클러 뷰 - 목록 화면 구성

by 농농씨 2023. 6. 29.

리사이클러 뷰 기초 사용법

리사이클러 뷰는 목록 화면을 만들 때 사용한다.

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))

 

공부일기

더보기

음... 이번달에 끝내기는 힘들것같지만 그래도 공부하면서 핸드폰 보면 오 이건 무슨무슨 뷰인가~? 하는 생각도 들고 갖고놀줄만 알았던 폰을 직접 구현할 수 있을 것 같아서 설렌다. 내가 좋아하는 걸(귀여운거) 컨텐츠로 만들어보고 싶다