본문 바로가기

Android

[Android UI] Drag & Drop

Recyclerview -> Recyclerview Drag & Drop 구현하기

 

화면 구성

Top Recyclerview

Bottom Recyclerview

 

동작

Bottom Recyclerview에 있는 정보를

Drag and Drop을 통해 Top Recyclerview로 전달 

 

 

 

 

 

 

 

 

 

 

 

들어가기 전 준비물

Recyclerview 두 개

더보기

1. activity.xml

2. recyclerview_item.xml

3. Activity

 

recyclerview 두 개를 생성해준다. item layout은 귀찮으니까 하나로 간다.

간격을 위해 recyclerview LayoutManager에 ItemDecoration을 추가해주었다.

 

Adapter 

어댑터 아이템은 간단하게 textView 데이터 설정용 string 변수 하나를 가진 데이터 클래스다.

1. Top Recyclerview Adapter

class GardenAdapter(private val items: List<Item>):
    RecyclerView.Adapter<GardenAdapter.ViewHolder>(){

    override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(viewGroup.context).inflate(R.layout.item_inventory, viewGroup,false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.tv.text = items[position].id
    }

    override fun getItemCount() = items.count()

    inner class ViewHolder(view: View): RecyclerView.ViewHolder(view){
        val tv: TextView = view.findViewById(R.id.tvNum)
    }
}

2. Bottom Recyclerview Adapter

class InventoryAdapter(private val items: MutableList<Item>): RecyclerView.Adapter<InventoryAdapter.ViewHolder>(){

    override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(viewGroup.context)
            .inflate(R.layout.item_inventory, viewGroup, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.tv.text = items[position].id
    }

    override fun getItemCount() = items.count()

    inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val tv: TextView = view.findViewById(R.id.tvNum)
    }
}

 

Drag & Drop Process

1. 사용자의 UI Event (e.g. Long Click) 발생

2. 애플리케이션이 startDragAndDrop() 호출하는데, 인자로 shadowClipData를 받는다.

3. 현재 레이아웃에 있는 모든 View 객체 중 드래그 이벤트 리스너를 구현한 객체라면 관련 데이터를 받을 수 있다. 

4. onDrag() 메소드에서 계속 true를 반환하는 한, 드래그 이벤트는 종결 전까지 계속될 수 있다.

 

Started -> Continuing -> Dropped -> Ended

 

Drag & Drop 구현

Drag Event 시작하기 

이동시킬 아이템들이 존재하는 Bottom Recyclerview에서 해주어야 할 일은!!

startDragAndDrop() 메소드를 호출하여 drag and drop 이벤트가 발생하는 곳임을 알리는 것이다.

참고로 startDragAndDrop()은 API 24 이상 버전부터 지원하기 때문에 그 아래 버전을 타겟팅할 경우 startDrag()를 사용해야 한다.

 

public final boolean startDragAndDrop (ClipData data, 
                View.DragShadowBuilder shadowBuilder, 
                Object myLocalState, 
                int flags)

drag and drop 이벤트를 시작하기 위해선 startDragAndDrop 메소드를 호출해 주어야 한다.

인자로는 data, shadowBuilder, myLocalState, flags 등이 있다.

data

드래그 앤 드롭으로 전송할 데이터로 ClipData 타입이다. 

shadowBuilder

드래그 중 보여줄 이미지를 만들 수 있는 builder

myLocalState

 드래그 앤 드롭 관련 데이터로 드래그 뷰와 드롭 뷰간 정보 교환에 쓰인다. (뭔지는 자세히 보지 못했다)

 

구현

inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
	val tv: TextView = view.findViewById(R.id.tvNum)

	init {
		tv.tag = tv.text
		tv.setOnLongClickListener { v ->
			val dragData = ClipData.newPlainText("id", tv.text.toString())
			val shadow = MyDragShadowBuilder(v)
			v.startDragAndDrop(dragData, shadow, null, 0)
		}
	}
}
    
private class MyDragShadowBuilder(v: View) : View.DragShadowBuilder(v) {
	private val shadow = ColorDrawable(Color.LTGRAY)

	override fun onProvideShadowMetrics(size: Point, touch: Point) {
		val width: Int = (view.width / 1.2F).toInt()
		val height: Int = (view.height / 1.2F).toInt()

        shadow.setBounds(0, 0, width, height)
        size.set(width, height)
        touch.set(width / 2, height / 2)
	}
	override fun onDrawShadow(canvas: Canvas) {
		shadow.draw(canvas)
	}
}

 

롱클릭 리스너를 달아주고 내부에서 startDragAndDrop 메소드를 해당 뷰와 연결해준다.

startDragAndDrop에 필요한 인자들 중 ClipData와 Shadow를 생성해 넘겨주었다.

 

Drag Event 받기

drag event는 같은 화면에 있는 드래그 리스너를 구현한 모든 뷰에서 수신할 수 있다.

따라서 이벤트를 받고 싶은 뷰에서 드래그 리스너를 달아주고 관련 동작을 처리하면 된다.

 

Drag Event Listener

class DragCallback(private val listener: OnDragListener): View.OnDragListener {

    interface OnDragListener {
        fun onDrop(imgId: String)
    }

    override fun onDrag(v: View?, event: DragEvent?): Boolean {
        return when (event?.action) {
            ACTION_DRAG_STARTED -> {
                true
            }
            ACTION_DRAG_ENTERED -> {
                true
            }
            ACTION_DRAG_LOCATION -> {
                true
            }
            ACTION_DROP -> {
                val item: ClipData.Item = event.clipData.getItemAt(0)
                val itemId = item.text
                Log.d("DragCallback", "started item id: $itemId")
                listener.onDrop(itemId.toString())
                true
            }
            ACTION_DRAG_ENDED -> {
                true
            }
            else -> false
        }
    }
}

View.onDragListener를 Top Recyclerview Adapter에서 구현해도 상관없지만 코드를 분리해보고 싶어서 따로 클래스를 만들고, 

interface를 하나 더 만들어 Top Recyclerview Adapter에서 구체적인 이벤트 처리가 가능하도록 해보았다.

 

View.onDragListener를 구현할 때 onDrag를 오버라이드 해서 드래그 앤 드롭 이벤트에 대해 처리를 해 줄 수 있다.

Event를 계속 받고 싶다면 true를 리턴해주면 된다.

이벤트 종류

ACTION_DRAG_STARTED  이벤트 시작 시 호출
ACTION_DRAG_ENTERED 뷰 경계 안으로 Drag Shadow가 들어왔을 때 호출
ACTION_DRAG_LOCATION 뷰 경계 안에서 움직일 때 호출
ACTION_DRAG_EXITED  뷰 경계를 벗어났을 때 호출
ACTION_DROP 사용자가 뷰에 Drag Shadow를 놓았을 때 호출
ACTION_DRAG_ENDED  이벤트 종료 시 호출

 

사용

inner class ViewHolder(view: View): RecyclerView.ViewHolder(view){
	val tv: TextView = view.findViewById(R.id.tvNum)

	init {
		tv.setOnDragListener(DragCallback(object : DragCallback.OnDragListener {
			override fun onDrop(imgId: String) {
				Log.d("GardenAdapter", "imgId: ${imgId}, to: ${tv.text}")
			}
		}))
	}
}

 

 

[참고] Android Developer- Drag & Drop

 

드래그 앤 드롭  |  Android 개발자  |  Android Developers

Android 드래그 앤 드롭 프레임워크를 사용하면 사용자가 그래픽 드래그 앤 드롭 동작을 사용하여 데이터를 옮길 수 있습니다. 이 작업은 자체 앱의 뷰 간에 또는 멀티 윈도우 모드를 사용 설정한

developer.android.com

 

 

 

 

 

 

 

 

 

'Android' 카테고리의 다른 글

🐘 Groovy 에서 KTS로 전환하기  (0) 2023.03.03
Compose로 RecyclerView 대체해보기  (1) 2023.02.28
Hilt  (0) 2022.03.25
[Android] Very Long Vector Path issue  (0) 2021.08.17
Vector Asset  (0) 2021.07.25