이번 포스팅에서는 RecyclerView에 Swipe, Drag, Touch 동작을 연결하는 법에 대해 알아보도록 하겠습니다.
이번에 만들어볼 앱에서는 ViewHolder를 스와이프하면 삭제가 되고, 잡고 끌면 위아래로 ViewHolder를 이동할 수 있으며, ViewHolder를 터치하면 그 내용이 변하는 동작을 수행하도록 하겠습니다.
여기서는 제가 RecyclerView에 View Binding 적용하기 강의에서 만들었던 Recyclerview 앱을 수정하는 방식으로 구현해보겠습니다.
리팩토링
우선 View Binding을 사용하도록 설정을 추가합니다.
1
2
3
android {
buildFeatures . viewBinding true
}
메인액티비티에서 만들어주던 dataSet
은 초기화시 데이터를 바로 입력하도록 하고 Recyclerview 어댑터 안쪽으로 이동시켜서 받아오는 파라미터를 삭제합니다.
1
2
3
4
5
6
7
class RecyclerViewAdapter : RecyclerView . Adapter < RecyclerViewAdapter . ViewHolder >() {
private val dataSet : ArrayList < List < String >> = arrayListOf < List < String >>(). apply {
for ( i in 0. . 99 ) {
add ( listOf ( " $i th main" , " $i th sub" ))
}
}
}
메인액티비티의 Recyclerview 프로퍼티를 정하는 부분은 with 로 묶어줍니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
class MainActivity : AppCompatActivity () {
private val rvAdapter = RecyclerViewAdapter ()
override fun onCreate ( savedInstanceState : Bundle ?) {
with ( binding ) {
with ( recyclerView ) {
layoutManager = LinearLayoutManager ( context , LinearLayoutManager . VERTICAL , false )
adapter = rvAdapter
addItemDecoration ( DividerItemDecoration ( context , DividerItemDecoration . VERTICAL ))
}
}
}
}
데이터 조작함수 추가
데이터를 조작하는 함수를 Recyclerview 안에 다음과 같이 추가합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class RecyclerViewAdapter : RecyclerView . Adapter < RecyclerViewAdapter . ViewHolder >() {
// ViewHolder 포지션을 받아 그 위치의 데이터를 삭제하고 notifyItemRemoved로 어댑터에 갱신명령을 전달
fun removeData ( position : Int ) {
dataSet . removeAt ( position )
notifyItemRemoved ( position )
}
// 두 개의 ViewHolder 포지션을 받아 Collections.swap으로 첫번째 위치와 두번째 위치의 데이터를 교환
fun swapData ( fromPos : Int , toPos : Int ) {
Collections . swap ( dataSet , fromPos , toPos )
notifyItemMoved ( fromPos , toPos )
}
// 선택한 ViewHolder 포지션의 데이터 내용을 바꾸도록 함
fun setData ( position : Int ) {
dataSet [ position ] = listOf ( "main viewholder touched!" , "sub viewholder touched!" )
notifyItemChanged ( position )
}
}
스와이프와 드래그 기능 붙이기
안드로이드에서는 Recyclerview에 입력되는 스와이프와 드래그 동작을 감지하는 ItemTouchHelper.SimpleCallback 클래스가 이미 준비되어 있으니 이걸 사용하겠습니다. 이 콜백함수는 다음과 같이 정의하면 됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
val itemTouchCallback = object : ItemTouchHelper . SimpleCallback (
ItemTouchHelper . UP or ItemTouchHelper . DOWN , ItemTouchHelper . LEFT
){
override fun onMove (
recyclerView : RecyclerView ,
viewHolder : RecyclerView . ViewHolder ,
target : RecyclerView . ViewHolder
): Boolean {
val fromPos : Int = viewHolder . adapterPosition
val toPos : Int = target . adapterPosition
rvAdapter . swapData ( fromPos , toPos )
return true
}
override fun onSwiped ( viewHolder : RecyclerView . ViewHolder , direction : Int ) {
rvAdapter . removeData ( viewHolder . layoutPosition )
}
}
ItemTouchHelper ( itemTouchCallback ). attachToRecyclerView ( binding . recyclerView )
우선 ItemTouchHelper.SimpleCallback
의 인스턴스를 만들어 ItemTouchHelper 의 attachToRecyclerView 로 Recyclerview와 연결시켜주면 스와이프와 드래그 동작을 인식할 수 있게 됩니다.
초기화할때 인식할 드래그 방향을 dragDirs , 스와이프 방향을 swipeDirs 로 정해주는데 여기서는 드래그 방향은 위, 아래. 그리고 스와이프 방향은 왼쪽만 인식하도록 하였습니다.
onSwiped
에서는 스와이프가 일어날 때 데이터를 삭제하는 동작을 정의합니다. 터치한 ViewHolder 위치를 어댑터에 전달하여 removeData
를 실행합니다.
onMove
에서는 드래그가 일어날 때 두 ViewHolder의 데이터를 교환하는 동작을 정의합니다. 드래그가 시작되었을 때 ViewHolder의 위치와 이동이 완료된 후의 ViewHolder의 위치를 각각 fromPos
, toPos
로 정의하고 swapData
에 넘겨주어 데이터를 교환하게 합니다. 이때 스크롤 동작과 헷갈리지 않게 하기 위해서 ViewHolder를 드래그하기 위해서는 롱터치를 해야 합니다.
이때 onSwiped
에서는 layoutPosition 을 사용했고, onMove
에서는 adapterPosition 을 사용했습니다. 스와이프를 할 때 ViewHolder의 위치는 고정되어 있지만 드래그를 할때는 ViewHolder의 위치가 계속 변하게 됩니다. adapterPosition
은 전체 Recyclerview 안에서 위치를 가져오는 속성이고 layoutPosition
은 고정된 ViewHolder의 위치를 가져오는 속성입니다.
ViewHolder에 드래그 가능 여부를 알리는 이미지 표시
현재는 ViewHolder가 데이터만 표시되고 있어서 이 ViewHolder를 드래그 할수 있는지 바로 알수 있는 방법이 없습니다. 그래서 ViewHolder 오른쪽에 햄버거 마크를 넣어서 드래그 할 수 있는 ViewHolder라는 표시를 해 주도록 하겠습니다.
아이콘은 Vector Asset
메뉴에서 추가하고 LinearLayout
과 FrameLayout
을 섞어서 ViewHolder 모양을 수정했습니다.
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
<LinearLayout
xmlns:android= "http://schemas.android.com/apk/res/android"
android:layout_width= "match_parent"
android:layout_height= "wrap_content"
android:orientation= "horizontal"
android:paddingStart= "20dp"
android:paddingEnd= "20dp"
android:gravity= "center_vertical" >
<LinearLayout
android:layout_width= "0dp"
android:layout_height= "wrap_content"
android:layout_weight= "9"
android:orientation= "vertical" >
<TextView
android:id= "@+id/tv_main"
android:layout_width= "match_parent"
android:layout_height= "wrap_content"
android:text= "Main TextView"
android:textSize= "24sp"
android:padding= "5dp" />
<TextView
android:id= "@+id/tv_sub"
android:layout_width= "match_parent"
android:layout_height= "wrap_content"
android:text= "Sub TextView"
android:textSize= "18sp"
android:padding= "5dp" />
</LinearLayout>
<FrameLayout
android:layout_width= "wrap_content"
android:layout_height= "wrap_content"
android:layout_weight= "1" >
<ImageView
android:layout_width= "30dp"
android:layout_height= "30dp"
android:layout_gravity= "center"
android:src= "@drawable/ic_baseline_reorder_24" />
</FrameLayout>
</LinearLayout>
스와이프시 백그라운드 설정하기
다음으로 스와이프를 할때 미끄러져 나간 ViewHolder 아래에 Canvas 클래스를 이용해 작성한 빨간색 비트맵 이미지와 휴지통 아이콘을 표시되도록 하겠습니다. 이것을 위해서는 ItemTouchHelper.SimpleCallback
의 onChildDraw
를 사용합니다.
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
override fun onChildDraw (
c : Canvas ,
recyclerView : RecyclerView ,
viewHolder : RecyclerView . ViewHolder ,
dX : Float ,
dY : Float ,
actionState : Int ,
isCurrentlyActive : Boolean
) {
val icon : Bitmap
// actionState가 SWIPE 동작일 때 배경을 빨간색으로 칠하는 작업을 수행하도록 함
if ( actionState == ItemTouchHelper . ACTION_STATE_SWIPE ) {
val itemView = viewHolder . itemView
val height = ( itemView . bottom - itemView . top ). toFloat ()
val width = height / 4
val paint = Paint ()
if ( dX < 0 ) { // 왼쪽으로 스와이프하는지 확인
// ViewHolder의 백그라운드에 깔아줄 사각형의 크기와 색상을 지정
paint . color = Color . parseColor ( "#ff0000" )
val background = RectF ( itemView . right . toFloat () + dX , itemView . top . toFloat (), itemView . right . toFloat (), itemView . bottom . toFloat ())
c . drawRect ( background , paint )
// 휴지통 아이콘과 표시될 위치를 지정하고 비트맵을 그려줌
// 비트맵 이미지는 Image Asset 기능으로 추가하고 drawable 폴더에 위치하도록 함
icon = BitmapFactory . decodeResource ( resources , R . drawable . ic_menu_delete )
val iconDst = RectF ( itemView . right . toFloat () - 3 - width , itemView . top . toFloat () + width , itemView . right . toFloat () - width , itemView . bottom . toFloat () - width )
c . drawBitmap ( icon , null , iconDst , null )
}
}
super . onChildDraw (
c ,
recyclerView ,
viewHolder ,
dX ,
dY ,
actionState ,
isCurrentlyActive
)
}
터치 조작 정의하기
Recyclerview 어댑터 안에 정의한 ViewHolder 클래스에 클릭 리스너를 붙여주면 ViewHolder의 터치를 인식할 수 있습니다. 그럴려면 ViewHolder가 사용하는 list_item.xml
에서 레이아웃에 id를 부여해줘야 합니다.
1
2
3
<LinearLayout
android:id= "@+id/vhLayout" >
</LinearLayout>
그리고 클릭 리스너를 붙여서 Snackbar 로 터치한 위치를 보여주고 setData
함수가 실행되도록 합니다. 이때 setData는 Recyclerview 안의 함수이므로 ViewHolder 클래스에서 접근을 할 수가 없습니다. 그래서 ViewHolder를 inner classs
로 변경하여줍니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class RecyclerViewAdapter : RecyclerView . Adapter < RecyclerViewAdapter . ViewHolder >() {
inner class ViewHolder ( private val binding : ListItemBinding ) : RecyclerView . ViewHolder ( binding . root ) {
fun bind ( data : List < String >) {
binding . tvMain . text = data [ 0 ]
binding . tvSub . text = data [ 1 ]
binding . vhLayout . setOnClickListener {
Snackbar . make ( it , "Item $layoutPosition touched!" , Snackbar . LENGTH_SHORT ). show ()
setData ( layoutPosition )
}
}
}
}
ViewHolder 터치시 애니메이션 추가
현재는 ViewHolder를 터치했을 때 내용이 바뀌도록 하긴 했지만 정확히 어떤 ViewHolder를 터치한건지에 대한 표시가 나타나지 않습니다. 하지만 xml에 selectableItemBackground 속성을 정의하면 터치한 ViewHolder를 주목시키는 효과를 추가할 수 있습니다.
1
2
3
<LinearLayout
android:background= "?attr/selectableItemBackground" >
</LinearLayout>
이렇게 해서 Recyclerview에 스와이프, 드래그, 터치 동작에 대한 응답을 설정하는 법에 대해 알아보았습니다.
VIDEO