如何在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_WINDOWpermission 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.2value 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

