Android:RecyclerView、拖放(外部 View )和自动滚动?

标签 android drag-and-drop android-recyclerview

这是我正在尝试做的事情的图片:

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

我正在尝试制作一个类似资源管理器的文件浏览器,通过拖放来移动文件,但我遇到了一个问题。

我知道有一个特殊的 RecyclerView 拖放界面(例如有 this ),但我没能找到说明如何在内部和 之间移动事物的示例在列表之外。

这是我的 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>

还有 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 ())); 
    } 
}

基本上每次“文件夹”被 ACTION_DRAG_ENTER 编辑时,都会调用 handleScroll () 方法:它会检查拖动的文件悬停在哪个文件夹上,并使用它来滚动 RecyclerView。

问题是RecyclerView的回收机制:据我了解,回收池中的 View 不是ACTION_DRAG_STARTED,所以自动滚动到 View 中的 View 不能接收文件,也不能自动滚动列表进一步。

如何在 RecyclerView 与其外部之间进行拖放操作?使用自动滚动?

有没有办法在拖动开始后将新 View 添加到拖动中?

谢谢。

最佳答案

我做了更多的研究,似乎ViewGroupView其中包含所需的信息:

  1. View.mPrivateFlags2 需要设置 View.DRAG_CAN_ACCEPT 标志;
  2. 需要调用ViewGroup.notifyChildOfDrag()

1 的问题在于 mPrivateFlags2 是一个包访问专用变量:从包外部,它既不能通过直接代码访问,也不能通过反射访问(反射给出 IllegalAccessException)。另一个问题是 ViewGroup 只会将 DragEvent 分派(dispatch)给 ViewGroup 知道的 child ,不会碰巧设置了正确标志的 child (这可以在 dispatchDragEvent 中找到() 实现,例如在第 1421 行附近,如:for (View child : mDragNotifiedChildren) ...)。

2的问题在于notifyChildOfDrag()也是package-only成员。那么,我能想到的唯一答案是从 ACTION_DRAG_STARTED 保存 DragEvent,然后在 RecyclerView 添加新子项时重新发送事件。无论如何,这基本上就是 notifyChildOfDrag () 所做的;只有这也会通知已经通知的 child ,所以它不是最佳的。

为此,我采用了这种方式:

  • 在 DragEventListener 中,在 ACTION_DRAG_STARTED 上保存事件。
  • 要保存该事件,请使用事件持有者类。
  • 在 RecyclerView.Adapter 的 onBind () 方法内:设置一个稍后执行的定时 Runnable。
  • 在 Runnable 中,检查新绑定(bind)的 RecyclerView 项目是否已经添加到其父级:
  • 如果还没有添加,稍等一下再试...
  • 添加完成后,使用ViewGroup.dispatchDragEvent(),其参数为我们之前保存的拖动事件。

我还对代码进行了其他小的修复和调整,但这些是使其正常工作的主要步骤。

这是生成的新 Java 代码(XML 保持不变,所以我不会重新发布):

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. 
    } 
}

我试图通过评论来引起人们对这些变化的关注。

可以对这段代码进行一些优化,但主要思想就在那里。让我知道这种方法是否有明显错误。

关于Android:RecyclerView、拖放(外部 View )和自动滚动?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34575100/

相关文章:

android - 如何从/dev/iio :deviceX?中读取LSM330的数据

android - Android 中没有哪些最重要的 POSIX 功能?

c# - 在 WPF 中检测拖放文件?

当 RecyclerView 到达终点时,android 加载 RecyclerView

java - Android上如何实现父子Recyclerview

android - 动态更新 RecyclerView 中的特定 View

android - 如何为android创建一个SQL数据库?

android - android中edittext的 Action

javascript - 在 DIV 上拖放文本

javascript - 为拖放的 selenium UI 集成测试执行 Javascript - Java