使用 ItemTouchHelper 时如何在拖动时取消对 RecyclerView 中项目的拖动?

Posted

技术标签:

【中文标题】使用 ItemTouchHelper 时如何在拖动时取消对 RecyclerView 中项目的拖动?【英文标题】:How to cancel dragging of items in RecyclerView when using ItemTouchHelper, as you drag? 【发布时间】:2019-12-08 17:06:58 【问题描述】:

背景

我正在尝试拥有一个具有不同视图类型的 RecyclerView,但具有拖放功能,以及单击和长按操作。

它类似于您在电话应用中的应用,您可以在其中更改收藏项目的顺序。在电话应用中,当您长按某个项目时,会立即出现上下文菜单,如果继续拖动,上下文菜单就会消失。

但是,在这种情况下,我需要做相反的事情。长按时,如果用户在很短的时间内没有拖动,或者用户在没有拖动的情况下停止了长按,我们会在屏幕上显示一个对话框,我需要停止拖动过程。

问题

虽然我已经成功处理了长触摸机制,并且我在这些特殊情况下显示了一个对话框,但我未能使拖动停止。

这意味着,如果即使在对话框出现后用户继续触摸屏幕,仍然可以继续拖动:

整个代码可用here(没有长触摸行为的代码可用here),但这里是主要代码:

class MainActivity : AppCompatActivity() 
    sealed class Item(val id: Long, val itemType: Int) 
        class HeaderItem(id: Long) : Item(id, ITEM_TYPE_HEADER)
        class NormalItem(id: Long, val data: Long) : Item(id, 1)
    

    enum class ItemActionState 
        IDLE, LONG_TOUCH_OR_SOMETHING_ELSE, DRAG, SWIPE, HANDLED_LONG_TOUCH
    

    override fun onCreate(savedInstanceState: Bundle?) 
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        setSupportActionBar(toolbar)
        val items = ArrayList<Item>(100)
        var itemDataCounter = 0L
        items.add(Item.HeaderItem(0L))
        for (i in 0 until 100) 
            items.add(Item.NormalItem(itemDataCounter, itemDataCounter))
            ++itemDataCounter
        
        val gridLayoutManager = recyclerView.layoutManager as GridLayoutManager
        gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() 
            override fun getSpanSize(position: Int): Int 
                return when (recyclerView.adapter!!.getItemViewType(position)) 
                    ITEM_TYPE_HEADER -> gridLayoutManager.spanCount
                    ITEM_TYPE_NORMAL -> 1
                    else -> throw Exception("unknown item type")
                
            
        
        recyclerView.adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() 
            init 
                setHasStableIds(true)
            

            override fun getItemViewType(position: Int): Int = items[position].itemType

            override fun getItemId(position: Int): Long = items[position].id

            override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder 
                val view = when (viewType) 
                    ITEM_TYPE_HEADER -> LayoutInflater.from(parent.context).inflate(R.layout.header_item, parent, false)
                    ITEM_TYPE_NORMAL -> LayoutInflater.from(parent.context).inflate(R.layout.grid_item, parent, false)
                    else -> throw Exception("unknown item type")
                
                return object : RecyclerView.ViewHolder(view) 
            

            override fun getItemCount() = items.size

            override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) 
                when (getItemViewType(position)) 
                    ITEM_TYPE_NORMAL -> 
                        val data = (items[position] as Item.NormalItem).data
                        holder.itemView.setBackgroundColor(when (data % 4L) 
                            0L -> 0xffff0000.toInt()
                            1L -> 0xffffff00.toInt()
                            2L -> 0xff00ff00.toInt()
                            else -> 0xff00ffff.toInt()
                        )
                        holder.itemView.textView.text = "item $data"
                    
                    ITEM_TYPE_HEADER -> 
                    
                    else -> throw Exception("unknown item type")
                
            
        
        val itemTouchHelper = ItemTouchHelper(object : ItemTouchHelper.Callback() 
            val touchSlop = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1f, resources.displayMetrics)
            //            val touchSlop = ViewConfiguration.get(this@MainActivity).scaledTouchSlop
            val longTouchTimeout = ViewConfiguration.getLongPressTimeout() * 2
            var touchState: ItemActionState = ItemActionState.IDLE
            var lastViewHolderPosHandled: Int? = null
            val handler = Handler()
            val longTouchRunnable = Runnable 
                if (lastViewHolderPosHandled != null && touchState == ItemActionState.LONG_TOUCH_OR_SOMETHING_ELSE) 
                    //                    Log.d("AppLog", "timer timed out to trigger long touch")
                    onItemLongTouch(lastViewHolderPosHandled!!)
                
            

            private fun onItemLongTouch(pos: Int) 
                //                Log.d("AppLog", "longTouchTimeout:$longTouchTimeout")
                val item = items[pos] as Item.NormalItem
                //                Toast.makeText(this@MainActivity, "long touch on :$pos ", Toast.LENGTH_SHORT).show()
                AlertDialog.Builder(this@MainActivity).setTitle("long touch").setMessage("long touch on pos: $pos - item $item.data").show()
                touchState = ItemActionState.HANDLED_LONG_TOUCH
                lastViewHolderPosHandled = null
                handler.removeCallbacks(longTouchRunnable)
            

            override fun onChildDrawOver(c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder?, dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean) 
                super.onChildDrawOver(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
                //                Log.d("AppLog", "onChildDrawOver $dX $dY pos:$viewHolder?.adapterPosition actionState:$actionState isCurrentlyActive:$isCurrentlyActive")
                if (touchState == ItemActionState.LONG_TOUCH_OR_SOMETHING_ELSE && (dX >= touchSlop || dY >= touchSlop)) 
                    lastViewHolderPosHandled = null
                    handler.removeCallbacks(longTouchRunnable)
                    touchState = if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) ItemActionState.DRAG else ItemActionState.SWIPE
                    Log.d("AppLog", "decided it's not a long touch, but $touchState instead")
                
            

            override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) 
                super.onSelectedChanged(viewHolder, actionState)
                //                Log.d("AppLog", "onSelectedChanged adapterPosition: $viewHolder?.adapterPosition actionState:$actionState")
                when (actionState) 
                    ItemTouchHelper.ACTION_STATE_IDLE -> 
                        //user finished drag or long touch
                        if (touchState == ItemActionState.LONG_TOUCH_OR_SOMETHING_ELSE)
                            onItemLongTouch(lastViewHolderPosHandled!!)
                        touchState = ItemActionState.IDLE
                        handler.removeCallbacks(longTouchRunnable)
                        lastViewHolderPosHandled = null
                    
                    ItemTouchHelper.ACTION_STATE_DRAG, ItemTouchHelper.ACTION_STATE_SWIPE -> 
                        if (touchState == ItemActionState.IDLE) 
                            lastViewHolderPosHandled = viewHolder!!.adapterPosition
                            //                            Log.d("AppLog", "setting timer to trigger long touch")
                            handler.removeCallbacks(longTouchRunnable)
                            //started as long touch, but could also be dragging or swiping ...
                            touchState = ItemActionState.LONG_TOUCH_OR_SOMETHING_ELSE
                            handler.postDelayed(longTouchRunnable, longTouchTimeout.toLong())
                        
                    
                
            

            override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean 
                //                Log.d("AppLog", "onMove")
                if (touchState == ItemActionState.LONG_TOUCH_OR_SOMETHING_ELSE) 
                    lastViewHolderPosHandled = null
                    handler.removeCallbacks(longTouchRunnable)
                    touchState = ItemActionState.DRAG
                
                if (viewHolder.itemViewType != target.itemViewType)
                    return false
                val fromPosition = viewHolder.adapterPosition
                val toPosition = target.adapterPosition
                //                val item = items.removeAt(fromPosition)
                //                recyclerView.adapter!!.notifyItemRemoved(fromPosition)
                //                items.add(toPosition, item)
                //                recyclerView.adapter!!.notifyItemInserted(toPosition)
                Collections.swap(items, fromPosition, toPosition)
                recyclerView.adapter!!.notifyItemMoved(fromPosition, toPosition)
                //                recyclerView.adapter!!.notifyDataSetChanged()
                return true
            

            override fun isLongPressDragEnabled(): Boolean = true

            override fun isItemViewSwipeEnabled(): Boolean = false

            override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int 
                if (viewHolder.itemViewType == ITEM_TYPE_HEADER)
                    return makeMovementFlags(0, 0)
                //                Log.d("AppLog", "getMovementFlags")
                val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
                val swipeFlags = if (isItemViewSwipeEnabled) ItemTouchHelper.START or ItemTouchHelper.END else 0
                return makeMovementFlags(dragFlags, swipeFlags)
            

            override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) 
                if (touchState == ItemActionState.LONG_TOUCH_OR_SOMETHING_ELSE) 
                    lastViewHolderPosHandled = null
                    handler.removeCallbacks(longTouchRunnable)
                    touchState = ItemActionState.DRAG
                
                val position = viewHolder.adapterPosition
                items.removeAt(position)
                recyclerView.adapter!!.notifyItemRemoved(position)
            
        )
        itemTouchHelper.attachToRecyclerView(recyclerView)
    

    override fun onCreateOptionsMenu(menu: Menu): Boolean 
        menuInflater.inflate(R.menu.menu_main, menu)
        return true
    

    override fun onOptionsItemSelected(item: MenuItem): Boolean 
        var url: String? = null
        when (item.itemId) 
            R.id.menuItem_all_my_apps -> url = "https://play.google.com/store/apps/developer?id=androidDeveloperLB"
            R.id.menuItem_all_my_repositories -> url = "https://github.com/AndroidDeveloperLB"
            R.id.menuItem_current_repository_website -> url = "https://github.com/AndroidDeveloperLB/RecyclerViewDragAndDropTest"
        
        if (url == null)
            return true
        val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
        @Suppress("DEPRECATION")
        intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET)
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
        startActivity(intent)
        return true
    

    companion object 
        const val ITEM_TYPE_HEADER = 0
        const val ITEM_TYPE_NORMAL = 1
    

我尝试过的

我试图查看 RecyclerView 和 ItemTouchHelper 的所有文档。还试图在这里和互联网上寻找类似的问题。

我看不出有什么方法可以告诉拖动机制:“我已经完成拖动了,取消拖动”。

问题

如何取消由 ItemTouchHelper 发起和维护的拖动?

【问题讨论】:

正如您在我的回答https://***.com/a/54216384/4079010 中看到的那样,您可以使用getMovementFlags 方法来覆盖状态。在与方法一起传递的viewHolder 参数中,您可以使用viewHolder.getAdapterPostition() 获取位置。 @RahulKhurana 问题是它开始拖动后该怎么办。不是以前。这个函数在开始时被调用。不是在拖动过程中。 可能通过调用itemTouchHelper.attachToRecyclerView(null);会取消触摸 并在 1 秒后重新连接。 我现在试过了。将其设置为 null 后,它可以让您在 RecyclerView 上滚动。在你把它放回去之后,它就会被拖回来。所有这一切都在对话框仍在显示时...... 【参考方案1】:

false 覆盖isLongPressDragEnabled 方法:

override fun isLongPressDragEnabled() :Boolean 
    return false;

来源

https://android.googlesource.com/platform/frameworks/support/+/c045910/v7/recyclerview/src/android/support/v7/widget/helper/ItemTouchHelper.java

/**
     * Starts dragging the provided ViewHolder. By default, ItemTouchHelper starts a drag when a
     * View is long pressed. You can disable that behavior via
     * @link ItemTouchHelper.Callback#isLongPressDragEnabled().

【讨论】:

以上是关于使用 ItemTouchHelper 时如何在拖动时取消对 RecyclerView 中项目的拖动?的主要内容,如果未能解决你的问题,请参考以下文章

RecyclerView ItemTouchHelper.Callback:拖动交换条件

Android开发 RecyclerView实现拖动与滑动ItemTouchHelper

Android开发 RecyclerView实现拖动与滑动ItemTouchHelper

RecyclerView借助ItemTouchHelper实现拖动和滑动删除功能

Android使用ItemTouchHelper实现RecyclerView的item拖动位置交换

Android使用ItemTouchHelper实现RecyclerView的item拖动位置交换和侧滑删除