You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在ViewPager中实现跨页面RecyclerView(GridLayoutManager)的拖拽?

Hey Patryk, let's work through this cross-ViewPager RecyclerView drag-and-drop problem you're facing—losing those native RecyclerView animations when using View.startDrag() is definitely frustrating, but we can fix this by combining ItemTouchHelper (which preserves those smooth animations) with a global drag controller and overlay views to handle cross-page interactions.

Here's a step-by-step solution to replicate the desired drag effect with ViewPager:

1. Create a Global Drag Controller

First, we need a way to track the state of the ongoing drag (which item is being moved, its source RecyclerView, etc.) across all ViewPager pages:

class DragController {
    var draggingViewHolder: RecyclerView.ViewHolder? = null
    var sourceRecyclerView: RecyclerView? = null
    var sourceAdapter: RecyclerView.Adapter<*>? = null
    var sourcePosition: Int = -1
    var draggedItem: Any? = null // Replace with your actual data model type

    fun startDrag(holder: RecyclerView.ViewHolder, rv: RecyclerView, adapter: RecyclerView.Adapter<*>, position: Int, item: Any) {
        draggingViewHolder = holder
        sourceRecyclerView = rv
        sourceAdapter = adapter
        sourcePosition = position
        draggedItem = item
    }

    fun clear() {
        draggingViewHolder = null
        sourceRecyclerView = null
        sourceAdapter = null
        sourcePosition = -1
        draggedItem = null
    }
}

2. Customize ItemTouchHelper.Callback

ItemTouchHelper is built to handle RecyclerView drag-and-drop with native animations—we'll extend its callback to detect when the drag crosses ViewPager page boundaries, trigger page swipes, and use an overlay view to keep the dragged item visible across pages:

class CrossPageDragCallback(
    private val dragController: DragController,
    private val viewPager: ViewPager2,
    private val getRecyclerViewForPage: (Int) -> RecyclerView
) : ItemTouchHelper.Callback() {

    private var windowManager: WindowManager? = null
    private var draggedOverlayView: View? = null
    private var overlayLayoutParams: WindowManager.LayoutParams? = null

    override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
        // Allow drag in all four directions
        val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
        return makeMovementFlags(dragFlags, 0)
    }

    override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
        // Handle in-page item reordering (keep your existing logic here)
        val sourcePos = viewHolder.adapterPosition
        val targetPos = target.adapterPosition
        (recyclerView.adapter as YourItemAdapter).swapItems(sourcePos, targetPos)
        return true
    }

    override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
        super.onSelectedChanged(viewHolder, actionState)
        if (actionState == ItemTouchHelper.ACTION_STATE_DRAG && viewHolder != null) {
            // Initialize drag state and create overlay view
            val adapter = viewHolder.adapter as YourItemAdapter
            dragController.startDrag(
                viewHolder,
                viewHolder.itemView.parent as RecyclerView,
                adapter,
                viewHolder.adapterPosition,
                adapter.getItem(viewHolder.adapterPosition)
            )
            createDraggedOverlay(viewHolder.itemView)
        }
    }

    override fun onDragReleased(viewHolder: RecyclerView.ViewHolder, x: Float, y: Float) {
        super.onDragReleased(viewHolder, x, y)
        // Clean up overlay and drag state when release happens
        removeDraggedOverlay()
        dragController.clear()
        viewHolder.itemView.alpha = 1f
    }

    override fun onChildDraw(
        c: Canvas,
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder,
        dX: Float,
        dY: Float,
        actionState: Int,
        isCurrentlyActive: Boolean
    ) {
        super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
        if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
            // Hide original item view (use overlay instead)
            viewHolder.itemView.alpha = 0f

            // Update overlay position to match drag movement
            draggedOverlayView?.let {
                overlayLayoutParams?.x = (viewHolder.itemView.left + dX).toInt()
                overlayLayoutParams?.y = (viewHolder.itemView.top + dY).toInt()
                windowManager?.updateViewLayout(it, overlayLayoutParams)
            }

            // Trigger ViewPager page switch when drag reaches edge threshold
            val screenWidth = recyclerView.context.resources.displayMetrics.widthPixels
            val currentPage = viewPager.currentItem
            val pageCount = viewPager.adapter?.itemCount ?: 0

            if (dX < -screenWidth * 0.2 && currentPage > 0) {
                viewPager.setCurrentItem(currentPage - 1, true)
            } else if (dX > screenWidth * 0.2 && currentPage < pageCount - 1) {
                viewPager.setCurrentItem(currentPage + 1, true)
            }

            // When page switches, move dragged item to target RecyclerView
            viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
                override fun onPageSelected(position: Int) {
                    super.onPageSelected(position)
                    dragController.draggedItem?.let { item ->
                        val targetRv = getRecyclerViewForPage(position)
                        val targetAdapter = targetRv.adapter as YourItemAdapter
                        // Add item to target adapter
                        targetAdapter.addItem(item)
                        // Remove item from source adapter
                        dragController.sourceAdapter?.notifyItemRemoved(dragController.sourcePosition)
                    }
                    viewPager.unregisterOnPageChangeCallback(this)
                }
            })
        }
    }

    private fun createDraggedOverlay(originalView: View) {
        windowManager = originalView.context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
        // Clone original view or inflate a new item view and populate it with dragged data
        draggedOverlayView = inflateAndPopulateOverlayView(originalView.context)
        overlayLayoutParams = WindowManager.LayoutParams().apply {
            width = originalView.width
            height = originalView.height
            format = PixelFormat.TRANSLUCENT
            flags = WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
            type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
            } else {
                WindowManager.LayoutParams.TYPE_PHONE
            }
            x = originalView.left
            y = originalView.top
        }
        draggedOverlayView?.let { windowManager?.addView(it, overlayLayoutParams) }
    }

    private fun inflateAndPopulateOverlayView(context: Context): View {
        val overlayView = LayoutInflater.from(context).inflate(R.layout.your_item_layout, null)
        // Populate overlay view with data from dragController.draggedItem
        val item = dragController.draggedItem as YourDataModel
        overlayView.findViewById<TextView>(R.id.item_title).text = item.title
        // Add other view population logic here
        return overlayView
    }

    private fun removeDraggedOverlay() {
        draggedOverlayView?.let { windowManager?.removeView(it) }
        draggedOverlayView = null
        windowManager = null
    }
}

3. Attach the Callback to Each RecyclerView in ViewPager

In your ViewPager adapter, attach the custom ItemTouchHelper to each page's RecyclerView:

// Initialize drag controller
val dragController = DragController()

// Setup ViewPager2
viewPager.adapter = object : RecyclerView.Adapter<PageViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PageViewHolder {
        val rv = RecyclerView(parent.context).apply {
            layoutManager = GridLayoutManager(parent.context, 2) // Your grid column count
            tag = "rv_${currentList.size}" // Tag to identify the RecyclerView later
        }
        return PageViewHolder(rv)
    }

    override fun onBindViewHolder(holder: PageViewHolder, position: Int) {
        val adapter = YourItemAdapter(pageData[position])
        holder.recyclerView.adapter = adapter

        // Attach cross-page drag callback
        val callback = CrossPageDragCallback(dragController, viewPager) { pagePos ->
            viewPager.findViewWithTag<RecyclerView>("rv_$pagePos")
        }
        val itemTouchHelper = ItemTouchHelper(callback)
        itemTouchHelper.attachToRecyclerView(holder.recyclerView)
    }

    override fun getItemCount() = pageData.size
}

class PageViewHolder(val recyclerView: RecyclerView) : RecyclerView.ViewHolder(recyclerView)

Key Notes:

  • Overlay View Permissions: On Android 8.0+, you'll need to request the SYSTEM_ALERT_WINDOW permission to display the overlay view.
  • Data Consistency: Make sure your adapters handle item addition/removal correctly and call notifyItemInserted()/notifyItemRemoved() to keep RecyclerView states in sync.
  • Threshold Tuning: Adjust the screenWidth * 0.2 value to control how close the drag needs to be to the edge before triggering a page switch.

This approach keeps all of RecyclerView's native drag animations intact while enabling smooth cross-ViewPager item dragging, just like the effect you saw with HorizontalScrollView.

内容的提问来源于stack exchange,提问作者patrykcd

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.21 07:47:58