[Android] 안드로이드 개발 레벨업 교과서 정리 #4 리사이클러뷰(2)


출처 : 안드로이드 개발 레벨업 교과서 120~128p

몰랐던 부분 정리하는 포스트!


1. GridLayoutManager

RecyclerView에는 GridLayoutManager 라는 격자 형태로 레이아웃을 표시하는 LayoutManager 가 있다. 간단한 그리드형태가 아닌, GridLayoutManager.SpanSizeLookup을 이용하여 스크린샷과 같이 응용할 수 있다.


앞으로 설명할 예제 소스(Java)


GridLayoutManager.SpanSizeLookup에서는 getSpanSize() 메서드가 호출되므로 독점하고 싶은 열의 개수를 반환한다. 헤더 요소에서는 3열을 모두 차지하고 콘텐츠 요소에서는 1열을 차지하게 한다. 이때 헤더 요소인지 아닌지는 Adapter.getItemViewType() 메서드를 이용한다. 이 메서드가 헤더 요소인지 일반 아이템 요소인지 판단할 수 있는 값(Int)을 반환한다. 헤더 요소일 경우 getSpanSize()의 반환값으로서 이번에 독점하고 싶은 열의 수인 3을 반환하도록 구현하면 된다.

단, 이번에는 나중의 열의 수가 바뀔 것을 고려해 전체 열의 수인 3을 얻을 수 있는(레이아웃 매니저 생성자로 넘겼던 값) GridLayoutManager.getSpanCount() 메서드를 이용한다. 헤더가 아닐 경우 일반 아이템 요소이므로 1칸만 사용하도록 1을 반환한다.

SpanSizeLoopup 인스턴스를 위에서 생성했으면 GridLayoutManagerSpanSizeLookup을 설정하고, RecyclerView에 레이아웃 매니저를 설정한다.

위에 링크된 책의 예제는 Java로 작성되었지만, 직접 kotlin으로 바꿔서 코드를 작성해보았다.

xml은 동일하기 때문에, 첨부하지 않는다.


ViewHolder.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class HeaderViewHolder private constructor(itemView: View) : RecyclerView.ViewHolder(itemView) {
companion object {
fun create(parent: ViewGroup) =
HeaderViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.viewholder_header, parent, false))
}

fun bind(text: String) {
itemView.title_text_view.text = "시리즈 : $text" // 이 친구는 스트링 리소스를 어떻게 분리할지 모르겠다.
itemView.detail_text_view.text = "$text 시리즈입니다"
}
}

class ItemViewHolder private constructor(itemView: View) : RecyclerView.ViewHolder(itemView) {
companion object {
fun create(parent: ViewGroup) =
ItemViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.viewholder_item, parent, false))
}

fun bind(text: String) {
itemView.simple_text_view.text = text
}
}


RichAdapter.kt
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
class RichAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val dataSet = ArrayList<String>()

companion object {
const val ITEM_VIEW_TYPE = 0
const val HEADER_VIEW_TYPE = 1
}

fun setItems(data: List<String>) {
dataSet.clear()
dataSet.addAll(data)
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
HEADER_VIEW_TYPE -> HeaderViewHolder.create(parent)
ITEM_VIEW_TYPE -> ItemViewHolder.create(parent)
else -> throw RuntimeException("예측되지 않는 viewType 입니다")
}
}

override fun getItemCount() = dataSet.size

// 네모로 시작할 경우 헤더로 판정
override fun getItemViewType(position: Int) = if (dataSet[position].startsWith("■")) {
HEADER_VIEW_TYPE
} else {
ITEM_VIEW_TYPE
}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val text = dataSet[position]

if (holder is ItemViewHolder) holder.bind(text)
else (holder as? HeaderViewHolder)?.bind(text)
}
}


액티비티에 추가할 코드

아래 함수를 onCreate() 에서 사용하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private fun setupRecyclerView() {
simple_recycler_view.setHasFixedSize(true)

val adapter = RichAdapter()
adapter.setItems(DummyDataGenerator.generateStringListData())

simple_recycler_view.adapter = adapter

// 열을 3으로 설정한 GridLayoutManager 의 인스턴스를 생성하고 설정
val gridLayoutManager = GridLayoutManager(this, 3)

// SpanSizeLookup 으로 위치별로 차지할 폭을 결정한다
gridLayoutManager.spanSizeLookup = object : SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
// 헤더는 3칸을 차지해서 표시
return if (adapter.getItemViewType(position) == RichAdapter.HEADER_VIEW_TYPE) {
gridLayoutManager.spanCount
} else 1 // 나머지는 1칸만 사용
}
}

simple_recycler_view.layoutManager = gridLayoutManager
}



2. 아이템 추가 및 삭제

우선 RecyclerViewAdapter 클래스에서 아래 메서드를 추가한다. 이러한 메서드를 호출함으로써 데이터를 추가하고 삭제할 수 있다. 실제로 Adapter에서 이용하는 데이터를 변경하고 나서 RecyclerView.Adapter 클래스의 notifyItemInserted(position) 메서드와 notifyItemRemoved(position) 메서드 등을 호출해 RecyclerView에 변경을 알려줄 수 있고, 애니메이션이 실행된다.

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
// 데이터 삽입
fun addAtPosition(position: Int, text: String) {
if (position > dataSet.size) {
// 현재 아이템의 수보다 많은 위치를 지정하므로, 마지막 위치에 추가
position = dataset.size
}
// 데이터 추가
dataSet.add(position, text)
// 삽입했다고 Adapter에 알린다
notifyItemInserted(position)
}

// 데이터 삭제
fun removeAtPosition(position: Int) {
if (position < dataSet.size) {
// 데이터 삭제
dataSet.remove(position)
// 삭제했다고 Adapter에 알린다
notifyItemRemoved(position)
}
}

// 데이터 이동
fun move(from: Int, to: Int) {
val text = dataSet[from]
dataSet.remove(from)
dataSet.add(to, text)
notifyItemMoved(from, to)
}

이 외에도 notify 해주는 많은 메소드가 있다.



3. 풍부한 조작 구현하기

RecyclerView 초기화 처리에서 ItemTouchHelper 클래스를 이용하면 밀어서 삭제하거나 데이터 이동을 할 수 있다. 이 클래스를 구현함으로써 드래그 앤 드롭이나, 스와이프 삭제가 가능해진다.

구현은 매우 간단해서 ItemTouchHelper의 생성자로 ItemTouchHelper.SimpleCallback을 구현한 인스턴스를 전달하기만 하면 된다참고. 첫 번째 파라미터로는 onMove()에서 드래그할 방향을 전달하고, 두 번째 파라미터로는 onSwiped()메서드로 아이템을 스와이프했을 때의 처리를 기술한다. 기본적으로는 조금 전에 정의한 Adapter의 메서드를 호출하는 것만으로도 쉽게 구현할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ItemTouchHelper 클래스를 구현한다
// 이에따라 드래그 앤 드롭이나 스와이프로 삭제 등을 할 수 있게된다.
new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.RIGHT)) {
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
// 드래그 앤 드롭 시
adapter.move(viewHolder.getAdapterPosition(), target.getAdapterPosition());
return true
}

@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
// 아이템 스와이프 시
adapter.removeAtPosition(viewHolder.getAdapterPosition());
}
}).attachToRecyclerView(recyclerView);
Share