my repository

Android :: RecyclerView 리사이클러뷰 사용법 본문

IT/Android (Kotlin)

Android :: RecyclerView 리사이클러뷰 사용법

hjin 2020. 6. 12. 18:48

💡 RecyclerView

 

RecyclerView는 사용자가 관리하는 많은 수의 데이터 집합(Data Set)을 개별 아이템 단위로 구성하여 화면에 출력하는 뷰그룹(ViewGroup)이며, 한 화면에 표시되기 힘든 많은 수의 데이터를 스크롤 가능한 리스트로 표시해주는 위젯이다.

RecyclerView는 이름 그대로 View를 재활용하여 사용한다.

 

LayoutManager를 사용하여 다양한 뷰 배치를 표현할 수 있어서 유연하다는 장점이 있다.

  • LinearLayoutManager : 세로/가로방향 배치
  • GridLayoutManager : 바둑판 형식 배치

RecyclerView의 사용 방식은 다음과 같다.

 

📝 예제 : LinearLayoutManager

 

 

0. 라이브러리 추가 (build.gradle - app)

// 리사이클러뷰를 다루기 위한 라이브러리
implementation 'androidx.recyclerview:recyclerview:1.1.0'
// material 디자인 라이브러리
implementation "com.google.android.material:material:1.2.0-alpha05"
// 이미지 url 로딩 라이브러리
implementation "com.github.bumptech.glide:glide:4.10.0"
kapt "com.github.bumptech.glide:compiler:4.10.0"
// 동그란 이미지 커스텀 뷰 라이브러리
implementation 'de.hdodenhof:circleimageview:3.1.0'

 

1. ItemView (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="wrap_content">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/constraintLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        ...>

        <de.hdodenhof.circleimageview.CircleImageView
            android:id="@+id/img_profile"
            android:layout_width="48dp"
            android:layout_height="48dp"
            ... />

        <TextView
            android:id="@+id/tv_username"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            ... />

        <ImageView
            android:id="@+id/imageView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            ... />
    </androidx.constraintlayout.widget.ConstraintLayout>

    <ImageView
        android:id="@+id/img_contents"
        android:layout_width="0dp"
        android:layout_height="0dp"
        ... />

</androidx.constraintlayout.widget.ConstraintLayout>

 

2. RecyclerView 배치

- 리사이클러뷰를 사용할 곳에 View를 배치하고, listitem에 앞서 만든 itemView를 넣어준다.

<androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_home"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        android:orientation="vertical"
        android:paddingTop="6dp"
        android:paddingLeft="6dp"
        android:paddingRight="6dp"
        android:clipToPadding="false"
        tools:listitem="@layout/item_insta" />

 

3. Data class

- 데이터 형태를 정의하는 class를 생성한다.

- 예제에서 만든 item의 TextView 1개와 ImageView 2개에 들어갈 data를 저장할 class이다.

data class InstaData (
    val userName : String,
    val img_profile : String,
    val img_contents : String
)

 

4. ViewHolder

- ViewHolder를 통해 받은 데이터를 뷰로 연결시켜준다.

- ViewHolder란 각 뷰들을 보관하는 홀더 객체이다. 각 뷰 객체를 ViewHolder에 보관함으로써 findViewById와 같이 반복적으로 호출되는 메서드를 효과적으로 줄여 속도를 향상시킨다.

class InstaViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
    val tv_username = itemView.findViewById<TextView>(R.id.tv_username)
    val img_profile = itemView.findViewById<ImageView>(R.id.img_profile)
    val img_contents = itemView.findViewById<ImageView>(R.id.img_contents)

// ViewHolder와 instaData 클래스의 각 변수를 연동하는 역할
    fun bind(instaData: InstaData) {
        tv_username.text = instaData.userName
        Glide.with(itemView).load(instaData.img_profile).into(img_profile)
        Glide.with(itemView).load(instaData.img_contents).into(img_contents)
    }
}

 

5. Adapter

- RecyclerView에 표시될 아이템 뷰를 생성한다.
- Adapter는 필요에 따라 ViewHolder를 만들고, 데이터와 바인딩함으로써 ViewHolder를 특정 위치에 할당한다.
- RecyclerView의 Adapter에서 꼭 구현해야 하는 것은 다음과 같다.

메서드 설명
onCreateViewHolder(ViewGroup parent, int viewType) viewType 형태의 아이템 뷰를 위한 뷰홀더 객체 생성
onBindViewHolder(ViewHolder holder, int position) position에 해당하는 데이터를 뷰홀더의 아이템뷰에 표시
getItemCount() 전체 아이템 갯수 리턴
class InstaAdapter(private val context : Context) : RecyclerView.Adapter<InstaViewHolder>() {
    var datas = mutableListOf<InstaData>()
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InstaViewHolder {
        val view = LayoutInflater.from(context).inflate(R.layout.item_insta, parent, false)
        return InstaViewHolder(view)
    }

    override fun getItemCount(): Int {
        return datas.size
    }

    override fun onBindViewHolder(holder: InstaViewHolder, position: Int) {
        holder.bind(datas[position])
    }
}

onCreateViewHolder 메서드에서 LayoutInflater를 이용하여 item_insta.xml을 inflate 시킨다.

(참고) inflate란? xml에 쓰여있는 view의 정의를 실제 view객체로 만드는 역할

 

 

6. 마지막으로 데이터를 넣고, Adapter를 이용해서 RecyclerView에 띄어준다.

- loadDatas()에서 임의로 데이터를 만들어 어댑터에 추가시킨다.

- 데이터를 추가했으면 notifyDataSetChanged()를 통해 데이터가 갱신됨을 어댑터에 알려주어야 한다.

class HomeFragment : Fragment() {

    lateinit var instaAdapter: InstaAdapter
    val datas = mutableListOf<InstaData>()

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_home, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        instaAdapter = InstaAdapter(view.context)
        rv_home.adapter = instaAdapter // 리사이클러뷰의 어댑터를 instaAdapter로 지정해줌
        rv_home.addItemDecoration(RecyclerDecoration(20))
        loadDatas() // 데이터를 임의로 생성하고 어댑터에 전달
    }

    private fun loadDatas() {
        datas.apply {
            add(
                InstaData(
                    userName = "userName",
                    img_profile = "이미지 파일 or 링크",
                    img_contents = "이미지 파일 or 링크"
                )
            )
            add(
                InstaData(
                    userName = "userName",
                    img_profile = "이미지 파일 or 링크",
                    img_contents = "이미지 파일 or 링크"
                )
            )
            add(
                InstaData(
                    userName = "userName",
                    img_profile = "이미지 파일 or 링크",
                    img_contents = "이미지 파일 or 링크"
                )
            )
            instaAdapter.datas = datas
            instaAdapter.notifyDataSetChanged() // 데이터가 갱신됨을 어댑터에 알려주는 역할
        }
    }

}

 


 

📝 예제 : GridLayoutManager

 

- GridLayoutManager(context, 한 줄에 들어가는 아이템 개수, RecyclerView.VERTICAL, false)

val myLayoutManager = GridLayoutManager(this, 3, RecyclerView.VERTICAL, false)
rv_toon.layoutManager = myLayoutManager

 

- DividerItemDecoration으로 아이템 사이 구분선을 추가할 수 있다.

- VERTICAL : 수직 구분선 / HORIZONTAL : 수평 구분선

val v_decoration = DividerItemDecoration(applicationContext, LinearLayoutManager.VERTICAL)
rv_toon.addItemDecoration(v_decoration)

val h_decoration = DividerItemDecoration(applicationContext, LinearLayoutManager.HORIZONTAL)
rv_toon.addItemDecoration(h_decoration)

 

💡 RecyclerView의 itemDecoration, clipToPadding

 

 

itemDecoration

 

ItemDecoration 클래스는 RecyclerView 내부에 있는 추상 클래스이다.
아이템 사이의 간격 조절, 구분선 추가 등에 사용할 수 있다.

 

1. RecyclerDecoration 클래스를 만들어 준다.
- getItemOffsets를 통해 recyclerView의 아이템에 여백을 설정해줄 수 있다.

class RecyclerDecoration(private val divHeight : Int) : RecyclerView.ItemDecoration() {

    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        super.getItemOffsets(outRect, view, parent, state)
        outRect.bottom = divHeight
    }

}

2. 리사이클러뷰에 연결해준다.

rv_home.addItemDecoration(RecyclerDecoration(20))

 

clipToPadding


리사이클러뷰에 패딩을 줄 경우 스크롤 시에도 패딩 공간이 유지될 것이다.
이 때 clipToPadding="false" 라는 속성값을 이용하면 패딩공간을 스크롤 영역으로 활용할 수 있다.

<androidx.recyclerview.widget.RecyclerView
    ...
    android:paddingTop="6dp"
    android:clipToPadding="false" />