코딩 일기장/Android(Kotlin)

[Android/Kotlin] recyclerview drag and drop/swipe 기능 만들기

minWachya 2021. 8. 26. 21:46
반응형

유튜브하다가 이건 또 어케하는 거야 싶어져서 만들어 봤는데

생각보다 너무 오래 걸렸다~~~~ 


시작하기 전에 다른 분들이 알려주신 이슈 해결 사항도 여기 적어보고자 합니다!!

 

1, 왼쪽으로 스와이프 해서 삭제 버튼을 보이게할 때, 다른 리스트도 함께 스와이프 되는 현상

(RecyclerView swipe to show button 구현 중 UI가 초기화 되지 않는 현상)

해결 방법 => https://velog.io/@hoyaho/RecyclerView-swipe-to-show-button-%EA%B5%AC%ED%98%84-%EC%A4%91-UI%EA%B0%80-%EC%B4%88%EA%B8%B0%ED%99%94-%EB%90%98%EC%A7%80-%EC%95%8A%EB%8A%94-%ED%98%84%EC%83%81

 

2, 아이템 삭제한 뒤 삭제 누르지 않은 index의 아이템들이 삭제되는 현상
해결 방법 => removeData()에서 notifyItemRemoved(position) 이후에 notifyItemRangeChanged(position, itemCount - position) 추가

 

댓글 감사합니다!!


구현한 기능

1, drag and drop

drag and drop

2, swipe(반만 swipe 되게 하기, swipe 고정시키기)

swipe 고정하기

3, 삭제 버튼 눌러서 삭제

삭제하기

4, sub menu 달기

서브 메뉴

5, view 터치 시 클릭 효과 주기 + 다른 view 선택 시 이전에 선택했던 view 닫기

 


전체 코드 보기 전에 굵직한 내용들은 설명을 조금 덧붙여보려고 한다.

 

recyclerview에 drag and drop, swipe 기능을 넣고 싶다면 ItemTouchHelper.Callback()을 상속받은 클래스를 recyclerview와 연결시켜 주어야 한다.

 

나는 SwipeHelperCallback라는 클래스를 만들어 ItemTouchHelper.Callback()를 상속받아 기능들을 구현했고,

MainActivity에서 아래 코드와 같이 연결시켜 주었다.

 // 리사이클러뷰에 스와이프, 드래그 기능 달기
val swipeHelperCallback = SwipeHelperCallback(recyclerViewAdapter).apply {
    // 스와이프한 뒤 고정시킬 위치 지정
    setClamp(resources.displayMetrics.widthPixels.toFloat() / 4)    // 1080 / 4 = 270
    }
 ItemTouchHelper(swipeHelperCallback).attachToRecyclerView(binding.recyclerView)

 

SwipeHelperCallback의 인수로는 recyclerViewAdapter를 넣어주었는데,

이는 recyclerViewAdapter에서 정의한 removeData, swipeData 메서드를 사용하기 위해서이다.

removeData는 데이터를 삭제하는 메서드이고, swipeData는 데이터를 교환하는 함수이다.

 

 

SwipeHelperCallback에서 오버라이드 한 함수들을 살펴보자.

 

getMovementFlags : 사용자가 view를 이동할 때, 인식할 방향을 설정

나는 드래그할 땐 위, 아래만 방향을 인식하게 했고,

스와이프할 때는 왼쪽, 오른쪽만 인식하게 했다.

만약 드래그 기능을 빼고 싶다면 UP or DOWN 부분에 0이라고 넣어주면 된다.

    // 이동 방향 결정하기
    override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
        // 드래그 방향 : 위, 아래 인식
        // 스와이프 방향 : 왼쪽, 오른쪽 인식
        // 설정 안 하고 싶으면 0
        return makeMovementFlags(UP or DOWN, LEFT or RIGHT)
    }

 

1, drag and drop

onMove : drag and drop 동작 정의

인수로 받은 recyclerviewAdapter의 swipeData 메서드를 이용해 데이터를 교환하는 것을 구현했다.

    // 드래그 일어날 때 동작 (롱터치 후 드래그)
    override fun onMove(
            recyclerView: RecyclerView,
            viewHolder: RecyclerView.ViewHolder,
            target: RecyclerView.ViewHolder
    ): Boolean {
        // 리사이클러뷰에서 현재 선택된 데이터와 드래그한 위치에 있는 데이터를 교환
        val fromPos: Int = viewHolder.adapterPosition
        val toPos: Int = target.adapterPosition
        recyclerViewAdapter.swapData(fromPos, toPos)
        return true
    }

 

 

2, swipe(반만 swipe 되게 하기, swipe 고정시키기)

나는 반만 swipe되고 나서 삭제 버튼을 눌러야 삭제가 되게 했는데,

이런 방식이 아니라 전체를 swipe를 해서 삭제하고 싶으면 onSwiped 함수에서 다음과 같이 적어주면 된다.

removeData도 recyclerViewAdapter에서 정의한 함수이다.

    // 스와이프 일어날 때 동작
    override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
        // 스와와이프 끝까지 하면 해당 데이터 삭제하기
        recyclerViewAdapter.removeData(viewHolder.layoutPosition)
    }

 

반만 swipe 되고 고정하는 코드는 아래와 같다.

// 아이템을 터치하거나 스와이프하는 등 뷰에 변화가 생길 경우 호출
    override fun onChildDraw(
            c: Canvas,
            recyclerView: RecyclerView,
            viewHolder: RecyclerView.ViewHolder,
            dX: Float,
            dY: Float,
            actionState: Int,
            isCurrentlyActive: Boolean
    ) {
        if (actionState == ACTION_STATE_SWIPE) {
            val view = getView(viewHolder)
            val isClamped = getTag(viewHolder)      // 고정할지 말지 결정, true : 고정함 false : 고정 안 함
            val newX = clampViewPositionHorizontal(dX, isClamped, isCurrentlyActive)  // newX 만큼 이동(고정 시 이동 위치/고정 해제 시 이동 위치 결정)

            // 고정시킬 시 애니메이션 추가
            if (newX == -clamp) {
                getView(viewHolder).animate().translationX(-clamp).setDuration(100L).start()
                return
            }

            currentDx = newX
            getDefaultUIUtil().onDraw(
                    c,
                    recyclerView,
                    view,
                    newX,
                    dY,
                    actionState,
                    isCurrentlyActive
            )
        }
    }

onChildDraw에서 getTag를 통해 현재 view를 고정시킬지 말지를 결정한다.

    // isClamped를 view의 tag로 관리
    // isClamped = true : 고정, false : 고정 해제
    private fun setTag(viewHolder: RecyclerView.ViewHolder, isClamped: Boolean) { viewHolder.itemView.tag = isClamped }
    private fun getTag(viewHolder: RecyclerView.ViewHolder) : Boolean =  viewHolder.itemView.tag as? Boolean ?: false

getTag에서 가져온 isClamped값으로 실제로 이동할 newX를 구한다.

    // swipe_view 를 swipe 했을 때 <삭제> 화면이 보이도록 고정
    private fun clampViewPositionHorizontal(
            dX: Float,
            isClamped: Boolean,
            isCurrentlyActive: Boolean
    ) : Float {
        // RIGHT 방향으로 swipe 막기
        val max = 0f

        // 고정할 수 있으면
        val newX = if (isClamped) {
            // 현재 swipe 중이면 swipe되는 영역 제한
            if (isCurrentlyActive)
            // 오른쪽 swipe일 때
                if (dX < 0) dX/3 - clamp
                // 왼쪽 swipe일 때
                else dX - clamp
            // swipe 중이 아니면 고정시키기
            else -clamp
        }
        // 고정할 수 없으면 newX는 스와이프한 만큼
        else dX / 2

        // newX가 0보다 작은지 확인
        return min(newX, max)
    }

이를 그림으로 표현해보면 아래와 같다.

고정되지 않을 때 왼쪽 이동 : dX / 2 (반만 이동)

고정되지 않을 때 오른쪽 이동 : 불가(min(newX, max)으로 0이상 이동을 막음)

 

고정되어있을 때 왼쪽 이동 : dX/3 - clamp (3분의 1만큼 이동하지만 clamp가 4분의 1을 이미 차지하고 있어서 (4분의 1은 이미 고정되어 있어서) 결국 반만 이동하는 것처럼 보임)

고정되어있을 때 오른쪽 이동 : dX - clamp

 

삭제 버튼을 view의 4분의 1이 되도록 weightSum과 layout_weight를 이용했고,

clamp도 view의 4분의 1로 설정해주기 위해 setClamp(resources.displayMetrics.widthPixels.toFloat() / 4)로 해줬다.

 

 

3, 삭제 버튼 눌러서 삭제

4, sub menu 달기

recyclerViewAdapter에서 정의

    // 뷰 홀더 설정
    inner class MyViewHolder(private val binding : ItemListBinding) : RecyclerView.ViewHolder(binding.root) {
        fun bind(title : String) {
            // 제목 달기
            binding.title = title

            // 서브 메뉴 달기(...모양)
            binding.textViewOptions.setOnClickListener {
                val popup = PopupMenu(binding.textViewOptions.context, binding.textViewOptions)
                popup.inflate(R.menu.recyclerview_item_menu)
                popup.setOnMenuItemClickListener { item ->
                    val str =  when (item.itemId) {
                        R.id.itemSaveLater -> "나중에 볼 동영상에 저장"
                        R.id.itemSavePalyList -> "재생목록에 저장"
                        R.id.itemSvaeOffline -> "동영상 오프라인 저장"
                        R.id.itemShare -> "공유"
                        R.id.itemRemove -> "좋아요 표시한 동영상에서 삭제"
                        else -> "오류"
                    }
                    Toast.makeText(binding.textViewOptions.context, str, Toast.LENGTH_SHORT).show()
                    true
                }
                popup.show()
            }

            // 삭제 텍스트뷰 클릭시 토스트 표시
            binding.tvRemove.setOnClickListener {
                removeData(this.layoutPosition)
                Toast.makeText(binding.root.context, "삭제했습니다.", Toast.LENGTH_SHORT).show()
            }
        }
    }

 

 

5, view 터치 시 클릭 효과 주기 + 다른 view 선택 시 이전에 선택했던 view 닫기

클릭 효과 추가 : android:background="?attr/selectableItemBackground"

이전에 선택한 view 닫기 :

SwipeHelperCallback

    // view가 swipe 되었을 때 고정될 크기 설정
    fun setClamp(clamp: Float) { this.clamp = clamp }

    // 다른 View가 swipe 되거나 터치되면 고정 해제
    fun removePreviousClamp(recyclerView: RecyclerView) {
        // 현재 선택한 view가 이전에 선택한 view와 같으면 패스
        if (currentPosition == previousPosition) return

        // 이전에 선택한 위치의 view 고정 해제
        previousPosition?.let {
            val viewHolder = recyclerView.findViewHolderForAdapterPosition(it) ?: return
            getView(viewHolder).animate().x(0f).setDuration(100L).start()
            setTag(viewHolder, false)
            previousPosition = null
        }

    }

MainActivity

        // 다른 곳 터치 시 기존 선택했던 뷰 닫기
        binding.recyclerView.setOnTouchListener { _, _ ->
            swipeHelperCallback.removePreviousClamp(binding.recyclerView)
            false
        }

 


 

activity_main.xml

 

 

 

 


item_list.xml : 리사이클러뷰의 아이템뷰(dataBinding 사용)

 

 

 

 

참고

https://developer.android.com/topic/libraries/data-binding/expressions?hl=ko 

 

레이아웃 및 결합 표현식  |  Android 개발자  |  Android Developers

표현식 언어를 사용하면 뷰에 의해 전달된 이벤트를 처리하는 표현식을 작성할 수 있습니다. 데이터 결합 라이브러리는 레이아웃의 뷰를 데이터 객체와 결합하는 데 필요한 클래스를 자동으로

developer.android.com


MyRecyclerViewAdapter.kt : 리사이클러뷰 어댑터(삭제버튼 눌러서 삭제, sub menu 달기)

 

 

 

 

참고

https://stackoverflow.com/questions/37601346/create-options-menu-for-recyclerview-item

 

Create Options Menu for RecyclerView-Item

How do I create an Options Menu like in the following Screenshot: The Options Menu should be opened afther clicking on the "More"-Icon of a RecyclerView Item! My try was this: @Override public v...

stackoverflow.com


SwipeHelperCallback.kt : drag and drop, swipe 동작 제어

 

 

 

 

참고

https://velog.io/@trycatch98/Android-RecyclerView-Swipe-Menu

 

Android - RecyclerView Swipe Menu

RecyclerView의 ItemTouchHelper를 이용하여 Swipe Menu 구현

velog.io

http://www.devexchanges.info/2017/02/android-recyclerview-dynamically-load.html?m=1 

 

Learn Programming Together: Android RecyclerView dynamically load more items when scroll to end with bottom ProgressBar

xxxxx

www.devexchanges.info

https://cliearl.github.io/posts/android/recyclerview-touch/

 

RecyclerView를 Swipe, Drag, Touch하기

이번 포스팅에서는 RecyclerView에 Swipe, Drag, Touch 동작을 연결하는 법에 대해 알아보도록 하겠습니다.

cliearl.github.io

https://developer.android.com/reference/kotlin/androidx/recyclerview/widget/ItemTouchHelper.Callback

 

ItemTouchHelper.Callback  |  Android 개발자  |  Android Developers

 

developer.android.com

 

 

https://stackoverflow.com/questions/41015101/itemtouchhelper-limit-swipe-width-of-itemtouchhelper-simplecallback-on-recycle

 

ItemTouchHelper : Limit swipe width of ItemTouchHelper.SimpleCallBack on RecyclerView

I have successfully implemented swipe behavior and performed some actions with it. The problem now I have is I want to limit the swipe width when I swipe the item. Currently this is what is happen...

stackoverflow.com


 

MainActivity.kt

 

 

 

 


추가로 handleIcon 클릭했을 때만 drag and drop 하고 싶으면 아래 문서를 참고하면 될 것 같다.

https://sudonull.com/post/3806-Drag-and-Swipe-in-RecyclerView-Part-2-Drag-and-Drop-Controllers-Grid-and-Custom-Animations

 

Sudo Null - Latest IT News

Programming news, technology, and just useful information

sudonull.com

https://niqrid2020.pe.kr/recyclerview-drag-and-drop-%EC%98%88%EC%A0%9C-%EC%BD%94%EB%93%9Ckotlin/

 

recyclerview drag and drop 예제 코드(kotlin) - The Beginner developer

recyclerview drag and drop 예제 코드(kotlin)에 대해서 포스팅합니다. recyclerview에 있는 아이템들을 드래그 앤 드롭으로 자리를 이동하는 방법에 대해서 개인적인 포스팅을 해보려고 합니다.

niqrid2020.pe.kr

 

반응형