카테고리 없음

Kotlin Recycler View 구현하기

start1a 2019. 12. 29. 23:46

RecyclerView

RecyclerView는 데이터 목록을 제한된 영역에서 다양한 포맷으로 화면에 출력하는 위젯이다.

사용하기 위해서 RecyclerView, 데이터를 관리 및 목록 표시를 담당하는 Adapter, Adapter를 통해 RecyclerView에서 각 아이템의 View를 제공하는 ViewHolder, 총 3가지가 필요하다.

 

  1. RecyclerView 기본 구현
  2. RecyclerView 다양한 방식의 Layout
  3. RecyclerView 선택 커스터마이징 (다른 포스트)

 

1. RecyclerView 기본 구현

ListView처럼 LinearLayout 형식으로 간단히 구현해 볼 것이다.

 

기본 구현 순서

  1. RecyclerView를 추가
  2. 리스트의 아이템으로 들어갈 Layout 디자인
  3. 리스트의 Model Class 생성
  4. ViewHolder 생성
  5. Adapter 생성
  6. ListView를 구현할 Activity에서 Data List - Adapter - ListView를 연결

 

1) RecyclerView를 추가

activity_main.xml에 RecyclerView 1개만 추가했다. RecyclerView는 기본 라이브러리가 아니므로 추가할 것이냐는 창이 뜬다. ok를 눌러주면 추가된다.

ok를 눌러 RecyclerView를 사용할 수 있음
RecyclerView 1개만 넣음

 

 

2) 리스트의 아이템으로 들어갈 Layout 디자인

Layout Resource File을 만들어 리스트의 각 아이템으로 들어갈 Layout을 디자인한다. ConstraintLayout에 간단하게 사진과 이름, 나이가 들어갈 TextView 2개를 선언했다.

 

Layout 디자인

 

 

3) 리스트의 Model Class 생성

리스트 데이터를 저장하고자 Custom Class로 Model을 만들었다.

1
class Person (val name : String, val age : Int)
 

 

4) ViewHolder Class 생성

RecyclerView.ViewHolder와 LayoutContainer 인터페이스를 상속받아 View 멤버를 override하고 View?의 ?를 제거하여 null을 허용하지 않도록 한다. override한 View는 RecyclerView.ViewHolder의 생성자로 넘겨준다. containerView에는 아까 디자인한 각 item 레이아웃 객체를 받을 것이다.

LayoutContainer는 kotlin android extensin 패키지에서 제공한다. 이를 상속받으면 내부적으로 View를 캐시하는 코드가 생성되고 더 빠른 화면 출력이 가능하다.

https://proandroiddev.com/kotlin-android-extensions-using-view-binding-the-right-way-707cd0c9e648

1
class DataViewHolder(override val containerView: View)
: RecyclerView.ViewHolder(containerView), LayoutContainer
 

 

 

5) Adapter 생성

생성자에서는 데이터 목록으로 쓰일 List를 받는다. RecyclerView.Adapter를 상속받고 ViewHolder로 쓰일 곳에 아까 생성한 Class를 Generic으로 넘겨준다. 이제 상속받은 클래스로부터 추상 메서드들을 override하면 3가지 메서드가 생성된다. 

  1. onCreateViewHolder() : RecyclerView가 필요할 때마다 호출하여 ViewHolder를 생성
  2. getItemCount() : 생성자의 List의 아이템의 개수 반환
  3. onBindViewHolder() : 생성된 ViewHolder가 화면에 표시될 데이터를 바인딩함

onCreateViewHolder()

  • 아까 만든 아이템 레이아웃을 사용하려면 LayoutInflater Class를 통해 xml 파일을 View 객체 형태로 생성해야 함
  • 생성한 View 객체를 ViewHolder에 넘겨 반환한다.

getItemCount()

  • 생성자의 list의 아이템 개수인 list.count()를 반환한다.

onBindViewHolder()

  • ViewHolder의 멤버인 containerView로 View의 Id를 참조하여 설정한다.
  • holder.containerView.(ViewId)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class DataAdapter (val list : List<Person>) : RecyclerView.Adapter<DataViewHolder>() {
 
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DataViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.item_list_1, parent, false)
        return DataViewHolder(view)
    }
 
    override fun getItemCount(): Int {
        return list.count()
    }
 
 
    override fun onBindViewHolder(holder: DataViewHolder, position: Int) {
        holder.containerView.imageView.setImageResource(R.drawable.ic_launcher_background)
        holder.containerView.textName.text = list[position].name
        holder.containerView.textAge.text = list[position].age.toString()
    }
}
 

 

 

6) ListView를 구현할 Activity에서 Data List - Adapter - ListView를 연결

listOf()를 통해 List를 구성하고 Adapter와 연결한다. ListView를 Adapter와 연결하고 LinearLayout으로 나열하기 위해 layoutManager를 LinearLayoutManager를 생성한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MainActivity : AppCompatActivity() {
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
 
        val personList = listOf(
            Person("a", 1),
            Person("b", 2),
            Person("c", 3),
            Person("d", 4),
            Person("e", 5)
        )
 
        val adapter = DataAdapter(personList)
personListview.adapter = adapter
        personListview.layoutManager = LinearLayout(this)
    }
}
 

 

실행 화면

 

이처럼 LinearLayout 형식으로 RecyclerView가 생성되었다.

 

 

 

 

 

 

2. 다양한 방식의 Layout

기존에는 LinearLayoutManager를 통해 세로로 일련의 리스트만 표시했다. RecyclerView는 다양한 LayoutManager를 통해 여러 포맷의 Layout 표현이 가능하다.

 

  1. LinearLayoutManager : 가로 혹은 세로로 일련의 리스트를 나열
  2. GridLayoutManager : 바둑판 배열처럼 Grid 형태로 리스트를 나열
  3. StaggeredLayoutManager : 잡지의 목록처럼 다양한 크기의 Grid 형태의 리스트 나열

 

item_list_2.xml

layout : width=200dp, height=match_parent

image : width=150dp, height=150dp

 

item_list_3.xml

layout : width=match_parent, height=wrap_content

image : width=match_parent, height=wrap_content, adjustViewbounds=true

 

 

 

Fruit Class

레이아웃을 디자인하고 People Class를 Fruit Class로 설계하였다.

imageView의 Id를 받기 위해 생성자에 변수를 넣었다.

1
class Fruit (val name : String, val imgId : Int)
 

 

DataAdapter Class

생성자에 list와 다양한 Layout의 Id를 받기 위해 layoutId를 넣었다. onBindViewHolder()에서는 list의 생성자로 들어온 image Id 값을 ImageView의 Resource로 설정했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class DataAdapter (val list : List<Fruit>, val layoutId : Int)
: RecyclerView.Adapter<DataViewHolder>() {
 
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DataViewHolder {
        val view = LayoutInflater.from(parent.context)
.inflate(layoutId, parent, false)
        return DataViewHolder(view)
    }
 
    override fun getItemCount(): Int {
        return list.count()
    }
 
 
    override fun onBindViewHolder(holder: DataViewHolder, position: Int) {
        holder.containerView.imageView.setImageResource(list[position].imgId)
        holder.containerView.textName.text = list[position].name
    }
}
 
 

 

MainActivity

fruitList로 과일 정보를 저장할 리스트를 생성했다.(수박이 중복되는 이유는 스크롤된 이미지까지 나타내기 위해) 아까 생성한 Layout 타입에 따라 Adapter도 여러 개 생성했다. 버튼을 눌러 Linear, Grid, StaggeredGrid을 다른 Adapter로 변환하면서 표시하도록 했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class MainActivity : AppCompatActivity() {
 
    val context : Context = this
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
 
        val fruitList = listOf(
            Fruit("사과", R.drawable.img_apple),
            Fruit("바나나", R.drawable.img_banana),
            Fruit("체리", R.drawable.img_cherry),
            Fruit("복숭아", R.drawable.img_peach),
            Fruit("파인애플", R.drawable.img_pineapple),
            Fruit("수박", R.drawable.img_watermelon),
            Fruit("수박", R.drawable.img_watermelon),
            Fruit("수박", R.drawable.img_watermelon),
            Fruit("수박", R.drawable.img_watermelon),
            Fruit("수박", R.drawable.img_watermelon)
        )
 
        val adapter1 = DataAdapter(fruitList, R.layout.item_list_1)
        val adapter2 = DataAdapter(fruitList, R.layout.item_list_2)
        val adapter3 = DataAdapter(fruitList, R.layout.item_list_3)
        fruitListview.layoutManager = LinearLayoutManager(this)
        fruitListview.adapter = adapter1
 
        LinearButton.setOnClickListener(object : View.OnClickListener {
            override fun onClick(v: View?) {
                fruitListview.layoutManager = LinearLayoutManager(context)
                fruitListview.adapter = adapter1
            }
        })
 
        GridButton.setOnClickListener(object : View.OnClickListener {
            override fun onClick(v: View?) {
                fruitListview.layoutManager = GridLayoutManager(context, 2)
                fruitListview.adapter = adapter2
            }
        })
 
        StaggeredGridButton.setOnClickListener(object : View.OnClickListener {
            override fun onClick(v: View?) {
                fruitListview.layoutManager =
StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
                fruitListview.adapter = adapter3
            }
        })
    }
 

 

결과

LinearLayout
GridLayout

 

StaggeredGridLayout

StaggeredGridLayout에서 스크롤 사용 시 아래 사진이 짤리는 현상

ScrollView로 감싸고 width, height를 match_parent로 설정했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<?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="match_parent"
    tools:context=".MainActivity">
 
    <Button
        android:id="@+id/StaggeredGridButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="32dp"
        android:layout_marginTop="16dp"
        android:text="Staggered"
        app:layout_constraintStart_toEndOf="@+id/GridButton"
        app:layout_constraintTop_toTopOf="parent" />
 
    <Button
        android:id="@+id/GridButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="32dp"
        android:layout_marginTop="16dp"
        android:text="Grid"
        app:layout_constraintStart_toEndOf="@+id/LinearButton"
        app:layout_constraintTop_toTopOf="parent" />
 
    <Button
        android:id="@+id/LinearButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="16dp"
        android:text="Linear"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
 
    <ScrollView
        android:layout_marginTop="80dp"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
 
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/fruitListview"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/LinearButton" />
 
    </ScrollView>
 
</androidx.constraintlayout.widget.ConstraintLayout>