Things take time

[Android] 리사이클러 뷰(RecyclerView) 사용하기 본문

Android(기능)

[Android] 리사이클러 뷰(RecyclerView) 사용하기

겸손할 겸 2019. 1. 25. 11:15

[공식]

 

해석하면서 iOS와 매치시켜보고자 한다.

 

https://developer.android.com/guide/topics/ui/layout/recyclerview

 

리사이클러뷰를 만들어보자. 잘못이해한 것이라면 알려주시길.

 

유동적인 데이터를 다루는 큰 데이터의를 리스트로 보여주고, 그것을 스크롤링해야한다면 리사이클러 뷰를 사용하라. 가장 먼저 나오는 도큐먼트의 첫 줄이다.

 

카드뷰 위젯을 이용한 리스트 모양의 결과물을 만들 것이다.

리스트뷰보다 진화한 버전의 리사이클러 뷰, 그리고 그안에 들어갈 각 Row에 들어갈 타입은 카드뷰! 이 두개의 뷰는 라이브러리 import를 해야한다.

 

- 기본 설명

 

리사이클러뷰는 리스트뷰에서 개선된 뷰이며, 여러 컴포넌트들이 데이터를 표현하기 위해 같이 사용될 수 있다. 리사이클러뷰는 각기 다른 뷰들을 레이아웃 매니저를 이용하여 표현한다.

 

리스트에 들어가는 뷰 들은 뷰 홀더 객체에 의해 표현되며, RecyclerView.ViewHolder 객체이다. 그러므로 뷰 홀더는 각 리사이클러 뷰에 하나 이상으로 존재하게 되며, 각 한 줄을 표현한다 생각하면 된다. 이 각 뷰 홀더는 어댑터에 의해 관리되며 이 객체는 RecyclerView.Adapter 객체이다. 어댑터라는 존재가 필요한 만큼 뷰 홀더를 생성하고, 뷰 홀더안에 표시할 데이터와 연결시켜준다. 뷰 홀더가 필요한 위치에 할당 될 때, 어댑터는 onBindViewHolder() 함수를 호출한다.

 

리사이클러뷰의 로직을 말하자면, 기본적으로 리스트에 필요한 뷰 홀더를 생성한다. 그러나 유저가 스크롤을 통해 또 다른 데이터를 가져올 때 기존에 생성한 뷰 홀더들을 재활용한다.(iOS도 동일) 만약 보고있는 화면에서 아이템의 변경사항(데이터를 새로 업데이트 하거나 할 경우) 어댑터에게 알려줘야한다. (RecyclerView.Adapter.notify())

 

 

iOS의 테이블 뷰가 리사이클러 뷰와 대응되며, 테이블 뷰에 들어가는 각 Cell은 리사이클러 뷰의 뷰 홀더에 대응된다. cellForRowAt의 iOS 테이블 뷰 함수는 onBindViewHolder 안드로이드 함수에 대응된다.

 

차이점은 iOS는 테이블 뷰와 셀을 지정하여 cellForRowAt에서 각 Cell의 클래스 파일을 지정해주는 반면, 안드로이드는 리사이클러 뷰를 생성하고 각 셀을 지정해주는 것은 Adapter가 하며, 이 어댑터가 onBindViewHolder의 함수에서 연결해준다 생각하면 된다. 즉, 중간에 하나(어댑터)가 껴드는 것!

 

 

[실습]

 

기본 프로젝트를 하나 생성한다.

예제에서는 각 뷰홀더는 카드 뷰 타입이라고 했으니 카드 뷰의 라이브러리와, 리사이클러 뷰를 사용하기 위한 라이브러리를 추가해야한다.

 

1
2
3
4
dependencies {
    implementation 'com.android.support:recyclerview-v7:28.0.0'
    implementation 'com.android.support:cardview-v7:28.0.0'
}
cs

 

App 단위의 Gradle에 추가한다.

 

그리고 activity_main.xml에 리사이클러 뷰를 추가한다.

 

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.RecyclerView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:scrollbars="vertical"
    android:id="@+id/recycler_view"
    xmlns:android="http://schemas.android.com/apk/res/android">
 
</android.support.v7.widget.RecyclerView>
cs

 

그리고 MainActivity에서는 리사이클러 뷰를 연결하고, 레이아웃 매니저, 어댑터를 선언한다. 각 역할은 기본 설명부분에서 확인할 수 있다.

레이아웃 매니저는 기본적인 리니어가 있는데, 그리드 뷰 형식이나 나타낼 리스트 형식에 따라 종류가 나뉘어져 있다.

 

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
public class MainActivity extends AppCompatActivity {
 
    RecyclerView recyclerView;
    RecyclerView.Adapter adapter;
    RecyclerView.LayoutManager layoutManager;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        recyclerView = findViewById(R.id.recycler_view);
 
        // 리사이클러뷰의 notify()처럼 데이터가 변했을 때 성능을 높일 때 사용한다.
        recyclerView.setHasFixedSize(true);
 
        layoutManager = new LinearLayoutManager(this);
        recyclerView.setLayoutManager(layoutManager);
 
        String[] textSet =  {"겸군님","티스토리","입니다","g-y-e-o-m.tistory.com"};
        int[] imgSet = {R.drawable.ic_launcher_foreground, R.drawable.ic_launcher_foreground, R.drawable.ic_launcher_foreground, R.drawable.ic_launcher_foreground};
 
        // 어댑터 할당, 어댑터는 기본 어댑터를 확장한 커스텀 어댑터를 사용할 것이다.
        adapter = new MyAdapter(textSet, imgSet);
        recyclerView.setAdapter(adapter);
    }
}
cs

 

어댑터 부분은 기본 RecyclerView.Adatter를 상속받고 커스터마이징 한 클래스(MyAdater.class)를 사용한다.

 

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
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {
    // 이 데이터들을 가지고 각 뷰 홀더에 들어갈 텍스트 뷰에 연결할 것
    private String[] textSet;
    private int[] imgSet;
 
    // 생성자
    public MyAdapter(String[] textSet, int[] imgSet){
        this.textSet = textSet;
        this.imgSet = imgSet;
    }
 
    // 리사이클러뷰에 들어갈 뷰 홀더, 그리고 그 뷰 홀더에 들어갈 아이템들을 지정
    public static class MyViewHolder extends  RecyclerView.ViewHolder{
        public ImageView imageView;
        public TextView textView;
 
        public MyViewHolder(View view){
            super(view);
            this.imageView = view.findViewById(R.id.iv_pic);
            this.textView = view.findViewById(R.id.tv_text);
        }
    }
 
    // 어댑터 클래스 상속시 구현해야할 함수 3가지 : onCreateViewHolder, onBindViewHolder, getItemCount
    // 리사이클러뷰에 들어갈 뷰 홀더를 할당하는 함수, 뷰 홀더는 실제 레이아웃 파일과 매핑되어야하며, extends의 Adater<>에서 <>안에들어가는 타입을 따른다.
    @NonNull
    @Override
    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
        View holderView = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.holder_view, viewGroup, false);
        MyViewHolder myViewHolder = new MyViewHolder(holderView);
        return myViewHolder;
    }
 
    // 실제 각 뷰 홀더에 데이터를 연결해주는 함수
    @Override
    public void onBindViewHolder(@NonNull MyViewHolder myViewHolder, int i) {
       myViewHolder.textView.setText(this.textSet[i]);
       myViewHolder.imageView.setBackgroundResource(this.imgSet[i]);
    }
 
    // iOS의 numberOfRows, 리사이클러뷰안에 들어갈 뷰 홀더의 개수
    @Override
    public int getItemCount() {
        return textSet.length > imgSet.length ? textSet.length : imgSet.length;
    }
}
cs

 

설명은 예제를 하면서, 주석에 달았다.

 

onCreateViewHolder는 뷰 홀더와 레이아웃 파일을 연결해주는 역할(이전에 리사이클러뷰에 레이아웃 매니저가 set 되어야함) -> iOS에서 스토리보드로 직접 Cell만들고, Reused Identifier 할당해주는 것

onBindViewHolder는 뷰 홀더에 들어갈 데이터를 연결해주는 작업(iOS 테이블 뷰의 cellForRowAt)

getItemCount는 iOS의 numberOfRows와 대응

 

이제 레이아웃 파일 holder_view를 만든다. 라이브러리 카드뷰를 사용할 것이다.

 

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
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    app:cardCornerRadius="10dp">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal">
        <ImageView
            android:layout_width="70dp"
            android:layout_height="70dp"
            android:layout_gravity="center_vertical"
            android:layout_margin="5dp"
            android:id="@+id/iv_pic"/>
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="20dp"
            android:layout_gravity="center_vertical"
            android:id="@+id/tv_text"
            android:text="테스트용"/>
    </LinearLayout>
</android.support.v7.widget.CardView>
cs

 

그리고 실행하면!

 

 

 

기본 리사이클러뷰 완성!

 

 

[응용]

 

위 까지는 기본중의 기본이었다.

이 사이클러뷰를 사용할 곳은 어디에 있을까?

 

전화번호부에도 사용될 수 있을 것이고, 친구 목록을 나타내는 곳에서도 쓰일 수 있을 것이다. 다양한 곳에서 쓰이는데, 잘 생각해보면 이 리스트들을 나타내는 뷰 들의 각 Row(행)는 똑같은 UI를 사용하는가?

 

예를 들어 메신저 앱을 열었다고 생각하자.

친구 목록에는 여러 종류의 행이 있다.

 

기본적으로 친구 사진 + 친구 이름 + 대화 메시지가 있는 타입이 있고, 친구 추천이나 카카오톡의 플러스 친구처럼 리스트 옆에 숫자도 있는 타입이 있다. 또한, 현재 친구는 몇명이다 라고 알려주는 간단한 라벨 형식의 Row도 있다.

 

그러므로 각 뷰 홀더에 연결되는 레이아웃은 위의 예제처럼 한 개만사용하는 것이 아니라 여러 개를 사용하는 것도 많을 것이다.

 

iOS의 경우, cellForRowAt에서 기준이 되는 데이터를 각 indexPath.row를 인덱스로 잡아서 리스트, 배열에서 꺼내서 비교 후, dequeueResusableCell이란 함수를 통해 각 셀에 할당된 클래스를 접근한다. 이 작업은 스토리보드에서 이루어지기 때문에, 안드로이드 처럼 어댑터의 개념이 없는 것이다.

 

그렇다면 이 작업을 안드로이드에서 하려면 어떻게 해야하는가.

 

어댑터를 살짝 수정해보자.


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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {
    // 이 데이터들을 가지고 각 뷰 홀더에 들어갈 텍스트 뷰에 연결할 것
    private String[] textSet;
    private int[] imgSet;
 
    // 생성자
    public MyAdapter(String[] textSet, int[] imgSet){
        this.textSet = textSet;
        this.imgSet = imgSet;
    }
 
    // 리사이클러 뷰의 각 뷰에 들어갈 아이템들을 지정, 각 뷰는 뷰 홀더가 관여한다. 연결이 어댑터
    public static class MyViewHolder extends  RecyclerView.ViewHolder{
        public ImageView imageView;
        public TextView textView;
        public TextView textView2;
 
        public MyViewHolder(View view){
            super(view);
            this.imageView = view.findViewById(R.id.iv_pic);
            this.textView = view.findViewById(R.id.tv_text);
            this.textView2 = view.findViewById(R.id.tv_text2);
        }
    }
 
    // 어댑터 클래스 상속시 구현해야할 함수 3가지 : onCreateViewHolder, onBindViewHolder, getItemCount
    // 리사이클러뷰에 들어갈 뷰 홀더를 할당하는 함수, 뷰 홀더는 실제 레이아웃 파일과 매핑되어야하며, extends의 Adater<>에서 <>안에들어가는 타입을 따른다.
    @NonNull
    @Override
    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
        View holderView;
        if (i == 1){
            holderView = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.holder_view, viewGroup, false);
        }else{
            holderView = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.holder_view2, viewGroup, false);
        }
        MyViewHolder myViewHolder = new MyViewHolder(holderView);
        return myViewHolder;
    }
 
    // 실제 각 뷰 홀더에 데이터를 연결해주는 함수, i는 0부터 - length까지로  순차적으로 들어옴
    @Override
    public void onBindViewHolder(@NonNull MyViewHolder myViewHolder, int i) {
        Log.e("type" , String.valueOf(myViewHolder.getItemViewType()));
        int viewType = myViewHolder.getItemViewType();
        switch (viewType){
            case 0:
                myViewHolder.textView2.setText("이것은 또 다른 뷰 홀더");
                break;
            case 1:
                myViewHolder.textView.setText(this.textSet[i]);
                myViewHolder.imageView.setBackgroundResource(this.imgSet[i]);
                break;
        }
    }
 
    // iOS의 numberOfRows, 리사이클러뷰안에 들어갈 뷰 홀더의 개수
    @Override
    public int getItemCount() {
        return textSet.length > imgSet.length ? textSet.length : imgSet.length;
    }
 
    // onCreateViewHolder에서 각 뷰 홀더에 연결되는 레이아웃 파일이 여러 개일 경우 이 곳에서 지정 => getItemViewType -> onCreateViewHolder 순으로 실행됨
    @Override
    public int getItemViewType(int position) {
        int returnVal = 0;
        if(textSet[position].equals("겸군님")){
            returnVal = 1;
        }
        return returnVal;
    }
}
cs

 

 여기서 주목할 함수는 getItemViewType(position)이란 함수를 override 했다는 것이다.

이 함수, 메소드는 onCreateViewHolder함수 이전에 호출된다. onCreateViewHolder의 두 번째 파라미터인 int i에 전달되는 i를 설정하는 것이다.

 

여기서는 함수 그대로 View의 Type들을 미리 전부 지정해야 한다. 연결될 뷰 홀더 레이아웃이 15개라면, return에는 15개의 값 중 하나로 리턴되어야 한다. 위의 소스에서는 0 혹은 1의 두개의 뷰 타입만 있다 가정 후 리턴된다.

 

* 주의 : 위의 onCreateViewHolder의 i는 넘겨받은 데이터 타입의 length와 관련이 없다. 이 length와 관련있는 것은 onBindViewHolder의 두 번째 파라미터 i와 getItemViewType의 position이 length만큼 돌게 되기에 관련있다.

 

헛갈린다면, 아래처럼 파라미터를 변경한다.

getItemViewType(int index)

onCreateViewHolder(Viewholder viewHolder, int ViewType)

onBindViewHolder(ViewHolder viewHolder, int index)


그렇기 때문에 onBindViewHolder에서는 index에 집중하기보다는 viewHolder의 getItemViewType()를 호출함으로써 넘겨받은 배열, 리스트의 index만큼 돌 때, 각 뷰 홀더의 뷰 타입을 얻어냄으로써 분기처리하면 된다는 것이다.

 

holder_view2

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    app:cardBackgroundColor="@color/cardview_shadow_start_color"
    app:cardCornerRadius="10dp">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="20dp"
            android:layout_gravity="center_vertical"
            android:id="@+id/tv_text2"
            android:text="테스트용"/>
    </LinearLayout>
</android.support.v7.widget.CardView>
cs