[Kotlin] RecyclerView에서 Index 0번 아이템을 Drag할 때 순서 뒤바뀜과 스크롤이 빠르게 넘어가는 현상, 또는 Item을 빠르게 Drag할 때 순서가 뒤바뀌는 현상

park_sujeong·2023년 1월 30일
1

Android

목록 보기
10/13
post-thumbnail

RecyclerView로 리스트를 만들고 리스트에 있는 아이템의 순서를 바꿀 때 우리는 Drag and Drop으로 한다. 아이템을 끌어서 옮기는 방식인데 한번씩 예상치 못한 현상을 조우하게 되었다. 스크롤이 가능한 길이일 때 Index 0번 아이템을 아래로 Drag하면 스크롤이 빨라지면서 내가 원하는 곳으로 Drop이 안되는 현상이다. 심지어 UI로 보이는 아이템의 순서와 일치하지 않는 경우가 있다. 또한 Index와 상관없이 Drag를 빠르게 하면 아이템의 순서와 데이터가 일치하지 않는 경우가 있는데 이럴 경우의 대안을 기록할것이다.



오류 및 오류 코드

우선, 어떤 현상인지 눈으로 보자

user3이라는 index 2의 아이템을 drag할때는 스크롤이 움직이지않고 정상적으로 작동하는데, user1이라는 index 0의 아이템을 drag할때는 스크롤이 밑으로 빠르게 넘어가고 drop후 리스트 목록을 보면 drag&drop하지않은 아이템이 섞인게 보인다. 이러면 안된다. 내가 의도한 현상이 아니다.



오류 코드

보통 drag&drop을 구현할 때 사용하는 ItemTouchHelper의 콜백 클래스다. 우리가 주목해야할 부분인 onMove()에서 현재 포지션(from)과 옮길 포지션(to)을 구해서 Collections.swap으로 List에 있는 from과 to의 순서를 바꾸고 adapter의 diffUtil을 이용해서 (adapter.differ.submitList(List)) adapter에 데이터 변경 사항을 반영한다. (전체 코드 및 설명은 아래 링크 참조)

*관련 포스팅 : [Android][Kotlin] RecyclerView의 모든것 (기본 사용법, Diffutil로 데이터 변화 시 UI 반영,Drag&Drop으로 아이템 순서 변경, Swipe 후 아이템 삭제)
*전체 코드 : github 주소

class ItemTouchSimpleCallback : ItemTouchHelper.SimpleCallback(
    ItemTouchHelper.UP or ItemTouchHelper.DOWN,
    ItemTouchHelper.RIGHT or ItemTouchHelper.LEFT
) {
    ...
    interface OnItemMoveListener {
        fun onItemMove(from: Int, to: Int)
    }

    private var listener: OnItemMoveListener? = null

    fun setOnItemMoveListener(listener: OnItemMoveListener) {
        this.listener = listener
    }

    override fun onMove(
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder,
        target: RecyclerView.ViewHolder
    ): Boolean {

        // 어댑터 획득
        val adapter = recyclerView.adapter as UserAdapter

        // 현재 포지션 획득
        val fromPosition = viewHolder.absoluteAdapterPosition

        // 옮길 포지션 획득
        val toPosition = target.absoluteAdapterPosition

        // adapter가 가지고 있는 현재 리스트 획득
        val list = arrayListOf<User>()
        list.addAll(adapter.differ.currentList)

        // 리스트 순서 바꿈
        Collections.swap(list, fromPosition, toPosition)

        // adapter.notifyItemMoved(fromPosition, toPosition)
        adapter.differ.submitList(list)

        // 추가적인 조치가 필요할 경우 인터페이스를 통해 해결
        listener?.onItemMove(fromPosition, toPosition)

        return true
    }
    ...
}





Drag & Drop 시 순서 섞이는 현상 해결방법

Drag & Drop 시 순서가 섞이는 현상을 해결하기위해서는 위의 오류 코드에서 onMove()Collections.swap(list, fromPosition, toPosition)을 아래와 같이 수정해준다.

override fun onMove(
    recyclerView: RecyclerView,
    viewHolder: RecyclerView.ViewHolder,
    target: RecyclerView.ViewHolder
): Boolean {
    ...
    // 리스트 순서 바꿈
    if (fromPosition < toPosition) {
        for (i in fromPosition until toPosition) {
            Collections.swap(list, i, i + 1)
        }
    } else {
        for (i in fromPosition downTo toPosition + 1) {
            Collections.swap(list, i, i - 1)
        }
    }

    ...
}

만약 우리가 0번 아이템을 3번으로 옮긴다면, fromPosition보다 toPosition이 크므로 if문에 걸려서 for문에 의해 0에서 2까지 반복하며 순차적으로 바꿔줄것이다. 반대로 3번 아이템을 0번으로 옮기면, fromPosition이 toPosition보다 커서 else문에 걸리게 된다. 그렇다면 for문에 의해 3부터 1까지 반복하며 바꿔준다.


이렇게 하면 아무리 빨리 drag & drop해도 RecyclerView의 리스트가 이상하게 섞이지않는다.





index 0 아이템을 drag 시 스크롤이 빨리 내려가는 현상 해결방법

RecyclerView의 adapter에 adapterDataObserver를 등록하여 fromPosition이나 toPosition이 0일 때는 RecyclerView의 scroll이 0에 있도록 하면 된다. adapterDataObserver 등록은 RecyclerView를 초기화할 때나 adapter를 초기화할 때 하면 된다.

adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
    override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
        if (fromPosition == 0 || toPosition == 0) {
            binding.recyclerview.scrollToPosition(0)
        }
    }
})




결과물





전체 코드

ItemTouchSimpleHelper.kt

class ItemTouchSimpleCallback : ItemTouchHelper.SimpleCallback(
    ItemTouchHelper.UP or ItemTouchHelper.DOWN,
    ItemTouchHelper.RIGHT or ItemTouchHelper.LEFT
) {
    ...
    override fun onMove(
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder,
        target: RecyclerView.ViewHolder
    ): Boolean {

        // 어댑터 획득
        val adapter = recyclerView.adapter as UserAdapter

        // 현재 포지션 획득
        val fromPosition = viewHolder.absoluteAdapterPosition

        // 옮길 포지션 획득
        val toPosition = target.absoluteAdapterPosition

        // adapter가 가지고 있는 현재 리스트 획득
        val list = arrayListOf<User>()
        list.addAll(adapter.differ.currentList)

/*  수정 전
        // 리스트 순서 바꿈
        Collections.swap(list, fromPosition, toPosition)
*/

/* 수정 후 */
        // 리스트 순서 바꿈
        if (fromPosition < toPosition) {
            for (i in fromPosition until toPosition) {
                Collections.swap(list, i, i + 1)
            }
        } else {
            for (i in fromPosition downTo toPosition + 1) {
                Collections.swap(list, i, i - 1)
            }
        }

        // adapter.notifyItemMoved(fromPosition, toPosition)
        adapter.differ.submitList(list)

        // 추가적인 조치가 필요할 경우 인터페이스를 통해 해결
        listener?.onItemMove(fromPosition, toPosition)

        return true
    }
    ...
}

MainActivity.kt

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private lateinit var adapter: UserAdapter
    private var userList = mutableListOf<User>()
    private val itemTouchSimpleCallback = ItemTouchSimpleCallback()
    private val itemTouchHelper = ItemTouchHelper(itemTouchSimpleCallback)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

        // RecyclerView에 사용할 리스트 제공
        for (i in 1 until 11) {
            val mUser = User(i, "user$i")
            userList.add(mUser)
        }
    }

    override fun onResume() {
        super.onResume()

        initRecyclerView()
        setupEvents()

    }

    private fun initRecyclerView() {
        // RecyclerView에 리스트 추가 및 어댑터 연결
        adapter = UserAdapter(this)
        binding.recyclerview.layoutManager = LinearLayoutManager(this)
        binding.recyclerview.adapter = adapter

        // DiffUtil 적용 후 데이터 추가
        adapter.differ.submitList(userList)

        // itemTouchSimpleCallback 인터페이스로 추가 작업
        itemTouchSimpleCallback.setOnItemMoveListener(object : ItemTouchSimpleCallback.OnItemMoveListener {
            override fun onItemMove(from: Int, to: Int) {
                // Collections.swap(userList, from, to) 처럼 from, to가 필요하다면 사용
                Log.d("MainActivity", "from Position : $from, to Position : $to")
            }
        })

        // itemTouchHelper와 recyclerview 연결
        itemTouchHelper.attachToRecyclerView(binding.recyclerview)

        // RecyclerView의 다른 곳을 터치하거나 Swipe 시 기존에 Swipe된 것은 제자리로 변경
        binding.recyclerview.setOnTouchListener { _, _ ->
            itemTouchSimpleCallback.removePreviousClamp(binding.recyclerview)
            false
        }

        adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
            override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
                if (fromPosition == 0 || toPosition == 0) {
                    binding.recyclerview.scrollToPosition(0)
                }
            }
        })

    }

    private fun setupEvents() {
        // 아이템 추가 버튼 클릭 시 이벤트(userList에 아이템 추가)
        binding.addButton.setOnClickListener {

            // 추가할 데이터 생성
            val mUser = User(userList.size+1, "added user ${userList.size+1}")

            // differ의 현재 리스트를 받아와서 newList에 넣기
            val newList = adapter.differ.currentList.toMutableList()

            // newList에 생성한 유저 추가
            newList.add(mUser)

            // adapter의 differ.submitList()로 newList 제출
            // submitList()로 제출하면 기존에 있는 oldList와 새로 들어온 newList를 비교하여 UI 반영
            adapter.differ.submitList(newList)

            // userList에도 추가 (꼭 안해줘도 되지만 userList가 필요할때가 있을 수도 있다.)
            // userList = adapter.differ.currentList 이렇게 사용하면 안됨
            userList.add(mUser)

            // 추가 메시지 출력
            Toast.makeText(this, "${mUser.name}이 추가되었습니다.", Toast.LENGTH_SHORT).show()

            // 추가된 포지션으로 스크롤 이동
            binding.recyclerview.scrollToPosition(newList.indexOf(mUser))
        }
    }
}





마치며

전체 코드를 보거나, 수정 부분만 보고 싶다면 아래 링크의 커밋을 참고하면 된다.

전체 코드 Github 주소

profile
Android Developer

0개의 댓글