[Android, Kotlin] RecyclerView에서 Swipe Menu 구현하기, Delete 메뉴 + Card View [3]

 

https://jinudmjournal.tistory.com/112

 

[Android, Kotlin] RecyclerView에서 Swipe Menu 구현하기, Delete 메뉴 + Card View [1]

Swipe Menu with RecyclerView - 데이터 구성 리싸이클러 뷰 내부에서 item을 슬라이드할 때 DELETE 버튼이 나오도록 코드를 작성한다. 메시지 목록에서 슬라이드해서 메시지를 삭제하는 등에 사용되는 기

jinudmjournal.tistory.com

 

https://jinudmjournal.tistory.com/113

 

[Android, Kotlin] RecyclerView에서 Swipe Menu 구현하기, Delete 메뉴 + Card View [2]

https://jinudmjournal.tistory.com/112 [Android, Kotlin] RecyclerView에서 Swipe Menu 구현하기, Delete 메뉴 + Card View [1] Swipe Menu with RecyclerView 리싸이클러 뷰 내부에서 item을 슬라이드할 때 DELETE 버튼이 나오도록 코드

jinudmjournal.tistory.com

 

위 두 포스팅에 이은 마지막 포스팅입니다.

 

데이터와 스와이프에 대한 동작의 준비를 마쳤으니, 마지막으로 버튼 생성과 버튼 클릭시 동작 제어 기능을 추가한다.

 

버튼 상태를 가지는 스와이프 컨트롤러

 

우선 이전 포스팅에서 작성한 ButtonState에 추가로 몇가지 데이터를 추가한다.

 

enum class ButtonsState {
    GONE,
    LEFT_VISIBLE,
    RIGHT_VISIBLE
}

 

위 클래스로 버튼의 상태를 나타내며,  buttonShwedSate 변수를 통해서 현재 버튼의 상태를 나타냈었다.

추가로 버튼의 크기, 현재 보여지고 있는 버튼을 표시할 변수를 추가한다.

    // 버튼의 상태를 나타냄
    private var buttonShowedState  : ButtonsState = ButtonsState.GONE
    private val buttonWidth : Float = 300F // 나타낼 버튼의 크기
    // 현재 보여지는 버튼
    private var buttonInstance : RectF? = null

 

상태를 바르게 나타내기 위해서 여러가지 조건을 설정한다.

터치 리스너에서 사용자가 항목을 왼쪽이나 오른쪽으로 얼마나 스와이프하였는지 확인하는 기능이 필요하다.

버튼의 상태를 나타낼 수 있을만큼 충분히 스와이프 했을 경우, 버튼을 표시하도록 상태를 변경한다.

 

    private fun setTouchListener(
        c: Canvas,
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder,
        dX: Float,
        dY: Float,
        actionState: Int,
        isCurrentlyActive: Boolean
    ){
        recyclerView.setOnTouchListener(object : OnTouchListener{
            override fun onTouch(p0: View?, p1: MotionEvent?): Boolean {
                p1?.let{ event ->
                    swipeBack = event.action == MotionEvent.ACTION_CANCEL
                            || event.action == MotionEvent.ACTION_UP

                    // 얼마나 많이 드래그 했는지 확인
                    // 좌, 우 스와이프 상태 확인
                    if (swipeBack){
                        if (dX < - buttonWidth)
                            buttonShowedState = ButtonsState.RIGHT_VISIBLE
                        else if ( dX > buttonWidth)
                            buttonShowedState = ButtonsState.LEFT_VISIBLE

                        if (buttonShowedState!= ButtonsState.GONE){
                            setTouchDownListener(c,recyclerView,viewHolder,
                                dX, dY, actionState, isCurrentlyActive)
                            setItemClickable(recyclerView,false)
                        }
                    }
                }
                return false

            }

        })
    }

 

buttonShwedState가 GONE인 경우 다른 터치 리스너를 덮어쓰고 RecyvclerView의 클릭 동작을 허용한다.

이 후에 onclick 메서드가 이미 등록되었을 경우를 대비하여, setItemClickable을 false로 변경한다.

 

    private fun setItemClickable(recyclerView: RecyclerView,boolean: Boolean){
        for (i in 0 until recyclerView.childCount){
            recyclerView.getChildAt(i).isClickable = boolean
        }
    }

setItemClickable은 리싸이클러 뷰의 클릭 이벤트를 허용 or 비허용 상태로 만든다.

 

setTouchDownListener과 setTouchUpListener를 통해서 터치 리스너에 대한 동작을 제어한다.

 

  // 터치 시작 리스너
    private fun setTouchDownListener(
        c: Canvas, recyclerView: RecyclerView,
        viewHolder: ViewHolder,dX: Float,dY: Float,
        actionState: Int,isCurrentlyActive: Boolean
    ){
        recyclerView.setOnTouchListener(object : OnTouchListener{
            override fun onTouch(p0: View?, p1: MotionEvent?): Boolean {
                p1?.let{event->
                    if (event.action == MotionEvent.ACTION_DOWN){
                        setTouchUpListener(c,recyclerView, viewHolder,
                            dX, dY, actionState, isCurrentlyActive)
                    }
                }
                return false
            }

        })
    }

    // 터치 중지 리스너 -> x좌표 고정
    private fun setTouchUpListener(
        c: Canvas, recyclerView: RecyclerView,
        viewHolder: ViewHolder,dX: Float,dY: Float,
        actionState: Int,isCurrentlyActive: Boolean
    ){
        recyclerView.setOnTouchListener(object : OnTouchListener{
            override fun onTouch(p0: View?, p1: MotionEvent?): Boolean {
                p1?.let {event ->
                    if (event.action == MotionEvent.ACTION_UP){
                        this@SwipeController.onChildDraw(c,recyclerView, viewHolder,
                            0F, dY, actionState, isCurrentlyActive)
                        // 터치 리스너 재정의
                        recyclerView.setOnTouchListener(object : OnTouchListener{
                            override fun onTouch(p0: View?, p1: MotionEvent?): Boolean {
                                return false
                            }

                        })
                        setItemClickable(recyclerView,true)
                        swipeBack = false

                        if (buttonActions != null && buttonInstance != null
                            && buttonInstance!!.contains(event.x,event.y)){
                            if (buttonShowedState == ButtonsState.LEFT_VISIBLE){
                                buttonActions?.onLeftClicked(viewHolder.absoluteAdapterPosition)
                            }
                            else if (buttonShowedState == ButtonsState.RIGHT_VISIBLE){
                                Log.d("right clikk!! ","delete")
                                buttonActions?.onRightClicked(viewHolder.absoluteAdapterPosition)
                            }
                        }
                        buttonShowedState = ButtonsState.GONE
                        currentItemViewHolder = null
                    }
                }
                return false
            }

        })
    }

 

여기까지의 과정을 끝냈다면, 사용자가 RecyclerView를 클릭하였을 때

SwipeController의 상태를 재설정하고, 뷰를 다시 그리게 된다.

 

이 과정을 통해서 스와이프 한 후의 뷰의 상태를 원래대로 돌리는 기능을 적용할 수 있다.

마지막으로 버튼을 동적으로 그리는 기능을 작성한다.

 

    // 버튼 그리기
    private fun drawButtons(c : Canvas, viewHolder: ViewHolder){
        val buttonWidthWithoutPadding = buttonWidth - 20
        val corners = 16F
        val itemView = viewHolder.itemView
        val p  = Paint()

        // 왼쪽 버튼 그리기
        val leftButton = RectF(itemView.left.toFloat(),itemView.top.toFloat(),
        itemView.left+buttonWidthWithoutPadding,itemView.bottom.toFloat())
        p.color = Color.BLUE
        c.drawRoundRect(leftButton,corners,corners,p)
        drawText("EDIT", c,leftButton,p)

        // 오른쪽 버튼 그리기
        val rightButton = RectF(itemView.right-buttonWidthWithoutPadding,
        itemView.top.toFloat(),itemView.right.toFloat(),itemView.bottom.toFloat())
        p.color = Color.RED
        c.drawRoundRect(rightButton,corners,corners,p)
        drawText("DELETE",c,rightButton,p)

        buttonInstance = null
        if (buttonShowedState == ButtonsState.LEFT_VISIBLE){
            buttonInstance = leftButton
        }else if (buttonShowedState == ButtonsState.RIGHT_VISIBLE){
            buttonInstance = rightButton
        }
    }

 

왼쪽 : EDIT, 오른쪽 : DELETE 두가지 버튼을 그리게 된다.

삭제 기능만 구현하였지만, 나중에 편집 기능도 사용할 가능성이 있으므로 해당 기능을 같이 구현했다.

현재 동작을 제어중인 버튼을 확인하기 위해서 buttonInstance에 접근한다.

아래는 버튼의 텍스트를 그려주는 기능이다.

    // 버튼의 텍스트 그리기
    private fun drawText(text:String,c:Canvas,button:RectF,p : Paint){
        val textSize : Float = 60F
        p.color = Color.WHITE
        p.isAntiAlias = true
        p.textSize = textSize
        val textWidth : Float = p.measureText(text)
        c.drawText(text, button.centerX()-(textWidth/2),button.centerY()+(textSize/2),p)
    }

 

여기까지 기능을 완성하면, 충분히 스와이프 하는 과정을 통해서 버튼이 나타나는 것을 확인할 수 있다.

한 가지 문제가 있다면 버튼을 활성화하고, 리싸이클러 뷰를 스크롤하면 버튼이 사라지는 것이다.

 

이유는 onChildDraw 메서드가 항목을 스와이프하거나 이동할 때만 동작하기 때문인데,

해당 기능을 수정하기 위해서  onDraw 기능을 별도로 구현한다.

 

    public fun onDraw(c: Canvas){
        currentItemViewHolder?.let { drawButtons(c, it) }
    }

 

onChildDraw 메서드에서 onDraw 메서드를 직접 호출하는 대신에 currentItemViewHolder 속성을 사용한다.

 

    // 현재 선택 뷰 홀더
    private var currentItemViewHolder : ViewHolder? = null

 

onChildDraw 메서드를 재정의하여 현재 선택 된 뷰 홀더를 수정하는 기능을 추가한다.

 

    override fun onChildDraw(
        c: Canvas,
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder,
        dX: Float,
        dY: Float,
        actionState: Int,
        isCurrentlyActive: Boolean
    ) {
        // 스와이프 상태일 때 적용
        if (actionState==ACTION_STATE_SWIPE){
            if (buttonShowedState != ButtonsState.GONE){
                var dX = dX
                // 스와이프 뷰를 정지시킴
                if (buttonShowedState == ButtonsState.LEFT_VISIBLE) dX = Math.max(dX,buttonWidth)
                if (buttonShowedState == ButtonsState.RIGHT_VISIBLE) dX = Math.min(dX,-buttonWidth)
                super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
            }else{
                setTouchListener(c,recyclerView,viewHolder,dX,dY
                    ,actionState, isCurrentlyActive)
            }
        }
        if (buttonShowedState == ButtonsState.GONE){
            super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
        }
        currentItemViewHolder = viewHolder
    }

 

맨 마지막 코드를 보면, currentItemViewHolder를 onChildDraw가 호출될 때 갱신하고 있다.

 

메인 액티비티에서 버튼 클릭에 대한 동작 제어

 

마지막으로 버튼 클릭 시 아이템을 삭제하는 코드를 작성해야 한다.

우선 SwipController에 작성해둔 인터페이스를 구현하는 함수를 작성해둔다.

 

    // 버튼 클릭 리스너
    private var buttonActions:SwipeControllerActions? =null
    fun setButtonActionListener(listener:SwipeControllerActions){
        this.buttonActions = listener
    }

 

버튼 클릭에 대한 리스너를 생성해두고, 메인 액티비티에서 해당 기능을 구현하여 리싸이클러뷰를 편집한다.

 

 

        val swipeController = SwipeController()
        swipeController.apply {

            setButtonActionListener(object : SwipeControllerActions{
                override fun onLeftClicked(position: Int) {
                    super.onLeftClicked(position)
                }

                override fun onRightClicked(position: Int) {
                    adapter.delData(position)
                }
            })
        }
        val itemTouchHelper = ItemTouchHelper(swipeController)
        itemTouchHelper.attachToRecyclerView(binding.recyclerView)
        binding.recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
            override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
                swipeController.onDraw(c)
            }
        })

 

swopeController를 재정의하였다.

apply를 통해서 인터페이스를 상속받아서 액션함수에 포지션을 받아온다.

받아온 포지션을 어댑터에 넘겨주어서 데이터의 삭제를 진행하는 delData 함수를 실행시킨다.

 

    fun delData(position: Int){
        dataSet.removeAt(position)
        notifyItemRemoved(position)
        notifyItemRangeChanged(position,itemCount)
    }

 

delData 함수는 포지션을 전달받아서 데이터를 삭제하고, 범위를 갱신하는 기능을 한다.

 

최종 코드

별도의 라이브러리 없이 리싸이클러 뷰의 스와이프 메뉴를 만드는 기능을 구현했다.

위와 같이 스와이프 기능을 통해서 삭제 버튼을 동적으로 나타나게 할 수 있으며,

해당 버튼 클릭을 통해서 데이터를 삭제하는 기능을 얻을 수 있었다.

 

아래 주소의 SwipeCardView 폴더에서 해당 코드를 확인할 수 있다.

 

https://github.com/jinuemong/Android-Study/tree/master/app/src/main/java/com/example/myapplication/SwipeCardView

 

GitHub - jinuemong/Android-Study: [Study] : Kotlin, Android Study File

[Study] : Kotlin, Android Study File. Contribute to jinuemong/Android-Study development by creating an account on GitHub.

github.com