Android: RecyclerView, Drag-and-Drop (External Vie

2020-07-18 11:48发布

问题:

Here is an image of what I am trying to do:

Image: Drag-and-drop app with a list and an item outside the list

I am trying to make an explorer-like file browser, with drag-and-drop to move the files, but I run into a problem.

I know there is a special RecyclerView drag-and-drop interface (there is this, for example), but I haven't been able to find examples that tell how to move things between the inside and outside of the list.

Here is my XML:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="net.wbord.recyclerviewdragdropautoscrolltest.MainActivity">

    <TextView
        android:id="@+id/exampleItem"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="20sp"
        android:text="Hello World!"/>

    <Space
        android:layout_width="match_parent"
        android:layout_height="50dp"

        />


    <android.support.v7.widget.RecyclerView
        android:id="@+id/mainList"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"

        android:paddingLeft="100dp"
        android:paddingRight="100dp"
        android:clipToPadding="false"
        />


</LinearLayout>

And the Java:

package net.wbord.recyclerviewdragdropautoscrolltest;

import android.app.Activity;
import android.content.ClipData;
import android.content.ClipDescription;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.DragEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;

public class MainActivity extends Activity {

    @Override
    protected void onCreate (Bundle savedInstanceState) {
        super.onCreate (savedInstanceState);
        setContentView (R.layout.activity_main);

        // Find Views: 
        final TextView exampleItem = (TextView) findViewById (R.id.exampleItem); 
        final RecyclerView rv = (RecyclerView) findViewById (R.id.mainList); 

        // Define Drag Listener: 
        final View.OnDragListener onDrag = new View.OnDragListener () {
            @Override
            public boolean onDrag (View v, DragEvent event) {
                switch (event.getAction ()) { 
                    case DragEvent.ACTION_DRAG_ENTERED: 
                        v.setScaleX (1.5f); v.setScaleY (1.5f); 
                        handleScroll (rv, v); 
                        break; 
                    case DragEvent.ACTION_DRAG_EXITED: 
                    case DragEvent.ACTION_DRAG_ENDED: 
                        v.setScaleX (1); v.setScaleY (1); 
                        break; 
                    case DragEvent.ACTION_DROP: 
                        ClipData data = event.getClipData (); 
                        String folder = (String) v.getTag (); 
                        String msg = "File '" + data.getItemAt (0).getText () + "' " + 
                                             "moved into folder '" + folder + "'"; 
                        Toast.makeText (MainActivity.this, msg, Toast.LENGTH_LONG).show (); 
                        break; 
                } 
                return true; 
            }
        }; 

        // The "file" for the user to drag-drop into a folder: 
        exampleItem.setOnLongClickListener (new View.OnLongClickListener () {
            @Override
            public boolean onLongClick (View v) {
                // Start drag: 
                ClipData.Item item = new ClipData.Item (exampleItem.getText ()); 
                ClipData data = new ClipData (exampleItem.getText (), 
                                                     new String [] {ClipDescription.MIMETYPE_TEXT_PLAIN}, 
                                                     item); 
                View.DragShadowBuilder builder = new View.DragShadowBuilder (exampleItem); 
                v.startDrag (data, builder, null, 0); 
                return true; 
            }
        }); 

        // The list of "folders" that can accept the file: 
        rv.setLayoutManager (new LinearLayoutManager (this, LinearLayoutManager.VERTICAL, false)); 
        rv.setAdapter (new RecyclerView.Adapter () {
            class ViewHolder extends RecyclerView.ViewHolder { 
                private final TextView vItem; 
                public ViewHolder (TextView textView) { 
                    super (textView); 
                    vItem = textView; 
                } 
                public void bind (String itemName) { 
                    vItem.setText (itemName); 
                    vItem.setTag (itemName); 
                    vItem.setOnDragListener (onDrag); 
                } 
            } 
            @Override
            public RecyclerView.ViewHolder onCreateViewHolder (ViewGroup parent, int viewType) { 
                TextView vText = new TextView (MainActivity.this); 
                vText.setTextSize (50); 
                return new ViewHolder (vText); 
            } 
            @Override
            public void onBindViewHolder (RecyclerView.ViewHolder holder, int position) {
                if (holder instanceof ViewHolder) 
                    ((ViewHolder) holder).bind (getItem (position)); 
            }
            @Override
            public int getItemCount () {
                return 100; 
            }
            public String getItem (int position) { 
                return "Folder " + (1 + position); 
            } 
        });
    }

    protected void handleScroll (RecyclerView vList, View viewHoveredOver) { 
        LinearLayoutManager mgr = (LinearLayoutManager) vList.getLayoutManager (); 
        int iFirst = mgr.findFirstCompletelyVisibleItemPosition (); 
        int iLast = mgr.findLastCompletelyVisibleItemPosition (); 
        // Auto-Scroll: 
        if (mgr.findViewByPosition (iFirst) == viewHoveredOver) 
            vList.smoothScrollToPosition (Math.max (iFirst - 1, 0)); 
        else if (mgr.findViewByPosition (iLast) == viewHoveredOver) 
            vList.smoothScrollToPosition (Math.min (iLast + 1, 
                    mgr.getChildCount ())); 
    } 
}

Basically every time a "folder" is ACTION_DRAG_ENTER-ed, the handleScroll () method is called: it checks which folder the dragged file is hovering over, and uses that to scroll the RecyclerView.

The problem is the RecyclerView's recycling mechanism: as I understand it, the views in the recycling pool are not ACTION_DRAG_STARTED, so the views that are auto-scrolled into view are not capable of receiving the file, nor capable of auto-scrolling the list further.

How does drag-and-drop work between a RecyclerView and its outside? With auto-scroll?

Is there a way to add the new views to the drag even after the drag has been started?

Thanks.

回答1:

I did a little more research, and it seems that ViewGroup and View have the needed pieces of information in them:

  1. View.mPrivateFlags2 needs to have the View.DRAG_CAN_ACCEPT flag set; OR
  2. ViewGroup.notifyChildOfDrag () needs to be called

The problem with 1 is that mPrivateFlags2 is a package-access-only variable: from outside the package, it can be accessed neither by direct code nor by reflect (reflect gives an IllegalAccessException). The other problem is that ViewGroup will only dispatch DragEvent to children the ViewGroup knows about, not the children who happen to have the right flag set (this can be found in the dispatchDragEvent () implementation, near line 1421 for example, as: for (View child : mDragNotifiedChildren) ...).

The problem with 2 is that notifyChildOfDrag () is also a package-only member. The only answer I could think of, then, is to save the DragEvent from ACTION_DRAG_STARTED, and then re-send the event whenever new children to the RecyclerView are added. This is basically what notifyChildOfDrag () does, anyway; only this also notifies the already-notified children, so it is not as optimal.

To do that, I went this way:

  • Inside the DragEventListener, on ACTION_DRAG_STARTED save the event.
  • To save that event, use an event holder class.
  • Inside the onBind () method for the RecyclerView.Adapter: set a timed Runnable that would execute later.
  • In the Runnable, check if the newly-bound RecyclerView item has been added to its parent already, or not yet:
  • If it has not been added yet, wait a little and try again ...
  • When it has been added, use ViewGroup.dispatchDragEvent (), with its argument being the drag event we saved earlier.

There are also other minor fixes and tweaks I did to the code, but these are the main steps to make it work.

Here is the resulting new Java code (the XML stays the same, so I will not re-post it):

package net.wbord.recyclerviewdragdropautoscrolltest;

import android.app.Activity;
import android.content.ClipData;
import android.content.ClipDescription;
import android.os.Bundle;
import android.os.Handler;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.DragEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.TextView;
import android.widget.Toast;

public class MainActivity extends Activity {

    // DEFINE HOLDER CLASS: 
    public static class DragEventHolder { 
        DragEvent mStartDrag = null; 
    } 

    @Override
    protected void onCreate (Bundle savedInstanceState) {
        super.onCreate (savedInstanceState);
        setContentView (R.layout.activity_main);

        // Find Views: 
        final TextView exampleItem = (TextView) findViewById (R.id.exampleItem); 
        final RecyclerView rv = (RecyclerView) findViewById (R.id.mainList); 

        // Define Drag Listener: 
        final DragEventHolder dragEventHolder = new DragEventHolder (); // PART OF ANSWER: VARIABLE TO HOLD DRAG EVENT 
        final View.OnDragListener onDrag = new View.OnDragListener () {
            @Override
            public boolean onDrag (View v, DragEvent event) {
                switch (event.getAction ()) { 
                    case DragEvent.ACTION_DRAG_STARTED: 
                        dragEventHolder.mStartDrag = event; // PART OF ANSWER 
                        v.setScaleX (1); v.setScaleY (1); // MINOR TWEAK (makes appearance better) 
                        break; 
                    case DragEvent.ACTION_DRAG_ENTERED: 
                        v.setScaleX (1.5f); v.setScaleY (1.5f); 
                        break; 
                    case DragEvent.ACTION_DRAG_ENDED: 
                        dragEventHolder.mStartDrag = null; // PART OF ANSWER 
                    case DragEvent.ACTION_DRAG_EXITED: 
                        v.setScaleX (1); v.setScaleY (1); 
                        break; 
                    case DragEvent.ACTION_DROP: 
                        ClipData data = event.getClipData (); 
                        String folder = (String) v.getTag (); 
                        String msg = "File '" + data.getItemAt (0).getText () + "' " + 
                                             "moved into folder '" + folder + "'"; 
                        Toast.makeText (MainActivity.this, msg, Toast.LENGTH_LONG).show (); 
                        break; 
                    case DragEvent.ACTION_DRAG_LOCATION: 
                        handleScroll (rv, v); // MINOR FIX: CALL handleScroll () FROM ACTION_DRAG_LOCATION RATHER THAN ACTION_DRAG_ENTERED (helps with easier auto-scrolling) 
                } 
                return true; 
            }
        }; 

        // The "file" for the user to drag-drop into a folder: 
        exampleItem.setOnLongClickListener (new View.OnLongClickListener () {
            @Override
            public boolean onLongClick (View v) {
                // Start drag: 
                ClipData.Item item = new ClipData.Item (exampleItem.getText ()); 
                ClipData data = new ClipData (exampleItem.getText (), 
                                                     new String [] {ClipDescription.MIMETYPE_TEXT_PLAIN}, 
                                                     item); 
                View.DragShadowBuilder builder = new View.DragShadowBuilder (exampleItem); 
                v.startDrag (data, builder, null, 0); 
                return true; 
            }
        }); 

        // The list of "folders" that can accept the file: 
        final android.os.Handler updateDragHandler = new Handler (); 
        rv.setLayoutManager (new LinearLayoutManager (this, LinearLayoutManager.VERTICAL, false)); 
        rv.setAdapter (new RecyclerView.Adapter () {
            class ViewHolder extends RecyclerView.ViewHolder { 
                private final TextView vItem; 
                public ViewHolder (TextView textView) { 
                    super (textView); 
                    vItem = textView; 
                } 
                public void bind (String itemName) { 
                    vItem.setText (itemName); 
                    vItem.setTag (itemName); 
                    vItem.setOnDragListener (onDrag); 
                    // Re-send DragEvent: 
                    updateDragHandler.postDelayed (new Runnable () {
                        @Override
                        public void run () {
                            ViewParent parent = vItem.getParent (); 
                            if (parent == null || !(parent instanceof ViewGroup)) { 
                                updateDragHandler.postDelayed (this, 50); 
                                return; 
                            } 
                            if (dragEventHolder.mStartDrag != null)
                                ((ViewGroup) parent).dispatchDragEvent (dragEventHolder.mStartDrag);
                        } 
                    }, 100); 
                } 
            } 
            @Override
            public RecyclerView.ViewHolder onCreateViewHolder (ViewGroup parent, int viewType) { 
                TextView vText = new TextView (MainActivity.this); 
                vText.setTextSize (50); 
                return new ViewHolder (vText); 
            } 
            @Override
            public void onBindViewHolder (RecyclerView.ViewHolder holder, int position) {
                if (holder instanceof ViewHolder) 
                    ((ViewHolder) holder).bind (getItem (position)); 
            }
            @Override
            public int getItemCount () {
                return 100; 
            }
            public String getItem (int position) { 
                return "Folder " + (1 + position); 
            } 
        });
    }

    protected void handleScroll (RecyclerView vList, View viewHoveredOver) { 
        LinearLayoutManager mgr = (LinearLayoutManager) vList.getLayoutManager (); 
        int iFirst = mgr.findFirstCompletelyVisibleItemPosition (); 
        int iLast = mgr.findLastCompletelyVisibleItemPosition (); 
        // Auto-Scroll: 
        if (mgr.findViewByPosition (iFirst) == viewHoveredOver) 
            vList.smoothScrollToPosition (Math.max (iFirst - 1, 0)); 
        else if (mgr.findViewByPosition (iLast) == viewHoveredOver) 
            vList.smoothScrollToPosition (Math.min (iLast + 1, 
                    vList.getAdapter ().getItemCount ())); // MINOR FIX:  Was getting the wrong count before. 
    } 
}

I tried to bring attention to the changes using comments.

There are some optimizations that can be done to this code, but the main idea is there. Let me know if there is something significantly wrong about this approach.