to top

Drag and Drop (原文链接)

翻译:汉尼拔萝卜(完整实例代码)

使用 Android 的拖放框架,应用程序可以允许用户通过拖放手势将数据从一个视图移动到另一个视图。这个框架中包括拖拽事件的类、监听器、其他一些辅助的类。

尽管这个框架是被设计用来移动数据的,但是我们也可一用它来改变 UI 显示。比如当将一个颜色图像拖放到另一个颜色图标上时,可以让这两种颜色混合起来。但是这篇文章主要介绍的是数据的拖动。

Overview

当用户作出我们定义的拖放手势时,就开始了拖放操作。应用程序会告诉系统拖放操作开始,系统回调应用程序来获取拖动的数据。当用户用手指拖动当前layout上的图像(拖动阴影)时,系统会把拖动事件发送给那些拖动事件监听器,以及在当前layout中 View 对象关联的拖动事件回调方法。一旦用户放开这个阴影,系统就会结束本次拖动操作。

你可一通过继承 View.OnDragListener 类来定义一个拖动事件("监听器"),然后通过 View 对象的 setOnDragListener() 方法来将这个监听器设给这个View对象。每个 View 类还有一个 onDragEvent() 方法。这些都会在 The drag event listener and callback method 这一章节详细描述。

Note: 为了简便,下面将处理拖动事件的方法都叫做"拖动事件监听器"尽管它可能是一个回调方法。

当开始拖动时,要将需要移动的数据以及描述这些数据的元数据都传递给系统。在拖动过程中,系统会将拖动事件发送给当前 layout 中的每一个 View 的拖动事件监听器或者回调方法。监听器或者回调方法可以使用元数据来决定是否想要接受放下来的数据。如果用户将数据拖到某一个 View 上放下,这个 View 监听器或者回调方法已经告诉系统它想要接受放下的数据,那么系统将会把拖动事件中的数据发送给监听器或者回调方法。

在程序中,通过调用 startDrag() 方法来通知系统开始拖动操作,这将会告诉系统开始分发拖动事件,这个方法同样需要发送需要拖动的数据。

你可以调用当前 layout 中任何一个 View 的 startDrag() 方法,系统通过这个 View 来获取 layout 中的全局设置。

当你调用了 startDrag() 方法后,剩下的事情就是系统向 layout 中的 View 发送拖动事件了。

The drag/drop process

拖放过程有4个基本步骤:

Started
应用程序调用 startDrag() 来响应拖动手势告诉系统拖动开始。startDrag() 的参数包括了拖动的数据、对拖动数据描述的元数据,以及一个绘制拖动阴影的回调。

系统首先会回调应用程序来绘制拖动阴影,它将会作为拖动过程中的阴影来显示。

接下来系统将会向layout中的所有 View 对象发送一个类型为 ACTION_DRAG_STARTED 的拖动事件。想要继续接收下面的拖动事件,包括放下事件,事件监听器一定要返回 true.这会向系统注册监听器,只有注册过的监听器才能收到连续的拖动事件。这个时候,可以改变 View 对象的外观来表示监听器可以接受放下事件。

如果拖动事件监听器返回了 false,那这个 View 将不会再收到系统发送的拖动事件了,除非等系统发出类型为 ACTION_DRAG_ENDED 的事件。通过返回 false,监听器告诉系统它对这次拖动操作不感兴趣,并且也不想接收拖动的数据。

Continuing
用户保持拖动的动作,当拖动阴影与 View 的边界接触时,系统会发送一个或多个拖动事件给 View 对象的监听器(前提时这个View注册过监听器)。监听器可以去改变 View 的外观来响应这个事件。举个例子,当拖动阴影与 View 的边界接触时(类型为 ACTION_DRAG_ENTERED 的拖动事件),监听器可以选择高亮这个 View 作为响应。
Dropped
用户在一个 View 的边界内松开拖动阴影,这个 View 便可以接受拖动数据。系统会发送一个类型为 ACTION_DROP 的事件给这个View 的监听器。这个拖动事件中包含了通过 startDrag() 方法传入的数据。如果监听器想要接受放下的数据,应该向系统返回 true.

发生这一步需要两个条件,首先用户必须要在 View 的边界内放下拖动阴影,其次这个 View 已经注册了接受这个事件的监听器。除此之外的其他情况,都不会有 ACTION_DROP 事件的发出。

Ended
当用户松开阴影后,并且系统已经发出了(如果需要) ACTION_DROP 事件,系统会发送一个类型为 ACTION_DRAG_ENDED 的拖动事件来表示这个拖放操作结束,不管用户在哪里放下阴影系统都会发出这个事件。这个事件会发送给所有的事件监听器,即使是已经收到 ACTION_DROP 事件的监听器。

这四步中的每一步都会在 Designing a Drag and Drop Operation 章节中详细描述。

The drag event listener and callback method

一个 View 通过继承于 View.OnDragListener 的类和它的 onDragEvent(DragEvent) 回调方法来接受拖动事件。当系统调用这些方法和监听器时,它传递给他们的是 DragEvent 实例。

你应该在大多数情况下使用监听器。当设计 UI 时,通常不会继承 View 类,但是如果想要使用回调方法就必须继承 View 类,因为得复写方法。相比而言,可以实现一个监听器并且将它设给多个不同的 View 对象。你也可以将它实现为匿名内部类,通过 setOnDragListener() 方法来将监听器设给View.

你可以为一个 View 设置监听器和回调方法,这种情况下,系统会首先调用监听器。只有在监听器返回 false 时,系统才会调用回调方法。

onDragEvent(DragEvent) 方法和 View.OnDragListener 的结合跟 onTouchEvent() 与 code>View.OnTouchListener 的结合一样。

Drag events

系统发出的拖动事件都是 DragEvent 实例,它包含了拖放过程中发生的事件类型。依事件类型的不同,还可能包含其他数据。

要获取事件类型,监听器可以调用事件的 getAction() 方法,这个方法共有6种返回值,被定义在 DragEvent 类中,都显示在table 1中.

DragEvent 实例中还包含了调用 startDrag() 方法时传给系统的值。一些数据仅仅在某个特定事件中才有效。每一个事件类型中都有效的数据在 table 2中。表中海详细阐述了可以在 Designing a Drag and Drop Operation 中使用的事件。

Table 1. 拖动事件类型

getAction() value Meaning
ACTION_DRAG_STARTED 当调用了 startDrag() 方法后,View的监听器接收到这个事件并且获得一个拖动阴影。
ACTION_DRAG_ENTERED 当拖动阴影与 View 的边界发生接触时,监听器会收到这个事件。这是拖动阴影进入 View 的边界时监听器收到的第一个事件。如果监听器想要继续接受此次拖动事件,必须向系统返回 true.
ACTION_DRAG_LOCATION 当监听器接收到 ACTION_DRAG_ENTERED 后,拖动阴影继续在 View 的边界内移动,监听器就会接收到这个类型的事件。
ACTION_DRAG_EXITED 在监听器接收到 ACTION_DRAG_ENTERED事件和一次以上的 ACTION_DRAG_LOCATION 事件后,如果拖动阴影离开 View 的边界时,监听器就会收到这个类型的事件。
ACTION_DROP 当用户在 View 的边界内放下拖动阴影时,监听器就会收到这个事件。只有那些收到 ACTION_DRAG_STARTED 事件并且返回 true 的监听器才会收到这个事件。如果用户在一个没有注册监听器的 View 上释放拖动阴影,或者不在当前layout上释放拖动阴影,这个事件将不会发出。

如果监听器成功处理了放下事件,应该返回 true,否则应该返回 false.

ACTION_DRAG_ENDED View 的监听器将会在系统结束拖动操作时收到这个类型的事件。这个事件的发出不依赖 ACTION_DROP 事件,如果系统发出过 ACTION_DROP 事件,那么收到 ACTION_DRAG_ENDED 事件并不是意味着操作成功。监听器必须调用 getResult() 方法来获取 ACTION_DROP 事件的处理结果,如果没有发出过 ACTION_DROP 事件,getResult() 将返回 false.

Table 2. Valid DragEvent data by action type

getAction() value getClipDescription() value getLocalState() value getX() value getY() value getClipData() value getResult() value
ACTION_DRAG_STARTED X X X      
ACTION_DRAG_ENTERED X X X X    
ACTION_DRAG_LOCATION X X X X    
ACTION_DRAG_EXITED X X        
ACTION_DROP X X X X X  
ACTION_DRAG_ENDED X X       X

getAction(), describeContents(), writeToParcel(), 和 toString() 方法总是可以得到有效的返回值.

如果一个方法在特定的事件中不能得到有效的返回值,此方法会返回 null 或0,视结果类型而定。

The drag shadow

在拖放过程中,系统显示一个用户拖动的图像。在拖动数据时,这个图像就代表着数据,对其它操作来说,它代表着拖动的东西。

这个图像就是拖动阴影。你可以通过申明 View.DragShadowBuilder 对象来创建拖动阴影,并在调用 startDrag() 时将这个对象传给系统。系统调用你定义在 View.DragShadowBuilder 中的回调方法来获得一个拖动阴影。

View.DragShadowBuilder 类有两个构造器:

View.DragShadowBuilder(View)
这个构造器接收你程序中的任何一个 View 对象,这个构造器会把这个 View 存储在 View.DragShadowBuilder 对象中,因此在构造阴影时你可以在回调方法中得到这个 View 对象。传入的 View 不一定时用户选择并拖动的那个 View.

如果选择的是这个构造器,你不必继承 View.DragShadowBuilder 并复写它的方法。默认会得到一个与传入的 View 一模一样的拖动阴影,它的中心点就是用户与屏幕接触的地方.

View.DragShadowBuilder()
如果你使用的是这个构造器,View.DragShadowBuilder 对象中将没有可以使用的 View对象。如果使用了这个构造器,并且没有继承 View.DragShadowBuilder 或者没有复写它的方法,那么拖动阴影将会是啥也没有,系统也不会对此报错。

View.DragShadowBuilder 类有两个方法:

onProvideShadowMetrics()
在调用了 startDrag() 方法后,系统会立即调用这个方法。使用它向系统发送拖动阴影的大小和触摸点坐标(也就是中心坐标),这个方法有两个参数:
dimensions
一个 Point 对象,用 x 表示拖动阴影的长,y 表示拖动阴影的宽.
touch_point
一个 Point 对象,触摸点就是拖动过程中拖动阴影位于手指下方的位置。X坐标用 x 表示,Y坐标用 y 表示。
onDrawShadow()
系统调用完 onProvideShadowMetrics() 方法后,会调用 onDrawShadow() 去得到拖动阴影本身。这个方法只有一个参数,就是Canvas对象,系统根据你传入给 onProvideShadowMetrics() 的参数来构造这个对象,并且将拖动阴影绘制在 code>Canvas 对象上。

为提高性能,你应该尽可能地让拖动阴影的尺寸很小。对单个的对象,可以使用一个 icon,对多个选择,应该让 icon 重叠显示,而不是将它们整个显示在屏幕上。

Designing a Drag and Drop Operation

这一节讲述怎样一步步开始拖动操作,怎样响应拖动过程中的事件。怎样响应一个放下事件,以及怎样结束拖放操作.

Starting a drag

用户收到拖动手势时,就开是拖动操作,通常是长按一个 View.在事件响应中,应该这么做:

  1. 必须要创建用来移动数据的 ClipData 对象和 ClipData.Item 对象。可以将存储在 ClipDescription 对象中的元数据作为 ClipData 对象的一部分。对于那些不需要移动数据的拖放操作,可以使用 null 来替代这个对象。

    举个例子,下面的代码展示了在长按 ImageView 时,去创建一个包含 ImageView 的 tag和 label 数据的 ClipData 对象。接下来的代码将展示如何去复写 View.DragShadowBuilder 中的方法:

    // Create a string for the ImageView label
    private static final String IMAGEVIEW_TAG = "icon bitmap"
    
    // Creates a new ImageView
    ImageView imageView = new ImageView(this);
    
    // Sets the bitmap for the ImageView from an icon bit map (defined elsewhere)
    imageView.setImageBitmap(mIconBitmap);
    
    // Sets the tag
    imageView.setTag(IMAGEVIEW_TAG);
    
        ...
    
    // Sets a long click listener for the ImageView using an anonymous listener object that
    // implements the OnLongClickListener interface
    imageView.setOnLongClickListener(new View.OnLongClickListener() {
    
        // Defines the one method for the interface, which is called when the View is long-clicked
        public boolean onLongClick(View v) {
    
        // Create a new ClipData.
        // This is done in two steps to provide clarity. The convenience method
        // ClipData.newPlainText() can create a plain text ClipData in one step.
    
        // Create a new ClipData.Item from the ImageView object's tag
        ClipData.Item item = new ClipData.Item(v.getTag());
    
        // Create a new ClipData using the tag as a label, the plain text MIME type, and
        // the already-created item. This will create a new ClipDescription object within the
        // ClipData, and set its MIME type entry to "text/plain"
        ClipData dragData = new ClipData(v.getTag(),ClipData.MIMETYPE_TEXT_PLAIN,item);
    
        // Instantiates the drag shadow builder.
        View.DragShadowBuilder myShadow = new MyDragShadowBuilder(imageView);
    
        // Starts the drag
    
                v.startDrag(dragData,  // the data to be dragged
                            myShadow,  // the drag shadow builder
                            null,      // no need to use local data
                            0          // flags (not currently used, set to 0)
                );
    
        }
    }
    
  2. 下面的代码定义了 myDragShadowBuilder,它在拖动 TextView 时创建一个灰色的矩形拖动阴影:
        private static class MyDragShadowBuilder extends View.DragShadowBuilder {
    
        // The drag shadow image, defined as a drawable thing
        private static Drawable shadow;
    
            // Defines the constructor for myDragShadowBuilder
            public MyDragShadowBuilder(View v) {
    
                // Stores the View parameter passed to myDragShadowBuilder.
                super(v);
    
                // Creates a draggable image that will fill the Canvas provided by the system.
                shadow = new ColorDrawable(Color.LTGRAY);
            }
    
            // Defines a callback that sends the drag shadow dimensions and touch point back to the
            // system.
            @Override
            public void onProvideShadowMetrics (Point size, Point touch)
                // Defines local variables
                private int width, height;
    
                // Sets the width of the shadow to half the width of the original View
                width = getView().getWidth() / 2;
    
                // Sets the height of the shadow to half the height of the original View
                height = getView().getHeight() / 2;
    
                // The drag shadow is a ColorDrawable. This sets its dimensions to be the same as the
                // Canvas that the system will provide. As a result, the drag shadow will fill the
                // Canvas.
                shadow.setBounds(0, 0, width, height);
    
                // Sets the size parameter's width and height values. These get back to the system
                // through the size parameter.
                size.set(width, height);
    
                // Sets the touch point's position to be in the middle of the drag shadow
                touch.set(width / 2, height / 2);
            }
    
            // Defines a callback that draws the drag shadow in a Canvas that the system constructs
            // from the dimensions passed in onProvideShadowMetrics().
            @Override
            public void onDrawShadow(Canvas canvas) {
    
                // Draws the ColorDrawable in the Canvas passed in from the system.
                shadow.draw(canvas);
            }
        }
    

    Note: 要记住不一定非要去继承 View.DragShadowBuilder 类,View.DragShadowBuilder(View) 构造方法默认会创建一个与传入 View 大小相同的拖动阴影,触摸点在拖动阴影中心。

Responding to a drag start

在拖动过程中,系统会给当前layout中所有View的事件监听器分发拖动事件。监听器需要调用 getAction() 来查看穿过来的事件类型。在拖动开始时,接受到的拖动事件是 ACTION_DRAG_STARTED.

为了响应类型为 ACTION_DRAG_STARTED 的事件,监听器应该完成下面工作:

  1. 调用 getClipDescription() 方法来获取一个 getClipDescription() 对象。然后调用 ClipDescription 的方法来获取 MIME 类型,来判断这个监听器是否能够接收拖动的数据。

    如果拖动操作没有移动数据,那么就可以省略这一步。

  2. 如果监听器能够接收放下操作,那应该返回 true,这将告诉系统继续向监听器发送本次拖动事件。如果不能接受要放下操作,应该返回 false,系统在这之后,只会像监听器发送 ACTION_DRAG_ENDED 事件。

注意对于 ACTION_DRAG_STARTED 事件,下面的 DragEvent 方法是不可用的:getClipData()getX()getY()getResult().

Handling events during the drag

在拖动过程中,只有那些在接收到 ACTION_DRAG_STARTED 事件时返回 true 的监听器才会收到拖动事件。监听器能否收到这中类型的事件与拖动阴影的位置和监听器所属的 View 的可见性有关。

在拖动过程中,监听器会跟觉拖动事件来改变 View 的外观.

在拖动过程中,监听器调用 code>getAction() 方法可以得到下面三种返回值:

监听器不需要对下面的事件类型有返回值,就算有返回值,也会被系统忽略。下面是在响应这些事件时的一些建议:

Responding to a drop

当用户在一个 View 上松开拖动阴影,并且这个 View 的监听器在拖动开始时已经告诉系统可以接受拖动的数据,系统将会分发 ACTION_DROP 事件。监听器应该完成下面的工作:

  1. 调用 getClipData() 方法获取一个 ClipData 对象,这个对象是在调用 startDrag() 方法时传入的。如果此次拖放操作不许要移动数据,那么这一步是不必要的。
  2. 返回 true 表示数据成功放下,返回 false 则表示数据没有成功放下。返回的结果可以通过 ACTION_DRAG_ENDED 事件的 getResult() 方法来获取到。

    如果系统没有发出过ACTION_DROP 事件,那么 code>ACTION_DRAG_ENDED 事件的 getResult() 方法将返回 false.

对于 ACTION_DROP 事件而言,getX()getY() 将返回放下点的坐标,这个坐标的坐标系是收到放下事件的 View.

系统允许用户在不接受拖动事件的 View 上释放拖动阴影,也允许用户在空白的地方释放拖动阴影,甚至在应用程序范围之外释放。在这些情况下,系统不会向发出 ACTION_DROP 事件,但是会发出 ACTION_DRAG_ENDED 事件。

Responding to a drag end

当用户释放拖动阴影后,系统会向应用同程中所有的监听器发送 ACTION_DRAG_ENDED 事件来说明此次拖动事件结束了.

每一个监听器应该完成下面的工作:

  1. 如果 View 的外观在拖动过程中做了改变,那么此时应该变回原来的样子。这可以让用户看到拖放操作的结束。
  2. 监听器此时可以选择性的调用 getResult() 方法来获取更多这次操作的信息。如果监听器在响应 ACTION_DROP 事件时返回了 true,那么 getResult() 方法也会得到 true,其他情况下 getResult() 都会返回 false,不管系统有没有发出过 ACTION_DROP 事件。
  3. 监听器应该返回 true 给系统.

Responding to drag events: an example

所有的拖动事件都可一被监听器或者拖动事件方法收到,下面的带面通过监听器来接受拖动事件:

// Creates a new drag event listener
mDragListen = new myDragEventListener();

View imageView = new ImageView(this);

// Sets the drag event listener for the View
imageView.setOnDragListener(mDragListen);

...

protected class myDragEventListener implements View.OnDragEventListener {

    // This is the method that the system calls when it dispatches a drag event to the
    // listener.
    public boolean onDrag(View v, DragEvent event) {

        // Defines a variable to store the action type for the incoming event
        final int action = event.getAction();

        // Handles each of the expected events
        switch(action) {

            case DragEvent.ACTION_DRAG_STARTED:

                // Determines if this View can accept the dragged data
                if (event.getClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {

                    // As an example of what your application might do,
                    // applies a blue color tint to the View to indicate that it can accept
                    // data.
                    v.setColorFilter(Color.BLUE);

                    // Invalidate the view to force a redraw in the new tint
                    v.invalidate();

                    // returns true to indicate that the View can accept the dragged data.
                    return(true);

                    } else {

                    // Returns false. During the current drag and drop operation, this View will
                    // not receive events again until ACTION_DRAG_ENDED is sent.
                    return(false);

                    }
                break;

            case DragEvent.ACTION_DRAG_ENTERED: {

                // Applies a green tint to the View. Return true; the return value is ignored.

                v.setColorFilter(Color.GREEN);

                // Invalidate the view to force a redraw in the new tint
                v.invalidate();

                return(true);

                break;

                case DragEvent.ACTION_DRAG_LOCATION:

                // Ignore the event
                    return(true);

                break;

                case DragEvent.ACTION_DRAG_EXITED:

                    // Re-sets the color tint to blue. Returns true; the return value is ignored.
                    v.setColorFilter(Color.BLUE);

                    // Invalidate the view to force a redraw in the new tint
                    v.invalidate();

                    return(true);

                break;

                case DragEvent.ACTION_DROP:

                    // Gets the item containing the dragged data
                    ClipData.Item item = event.getClipData().getItemAt(0);

                    // Gets the text data from the item.
                    dragData = item.getText();

                    // Displays a message containing the dragged data.
                    Toast.makeText(this, "Dragged data is " + dragData, Toast.LENGTH_LONG);

                    // Turns off any color tints
                    v.clearColorFilter();

                    // Invalidates the view to force a redraw
                    v.invalidate();

                    // Returns true. DragEvent.getResult() will return true.
                    return(true);

                break;

                case DragEvent.ACTION_DRAG_ENDED:

                    // Turns off any color tinting
                    v.clearColorFilter();

                    // Invalidates the view to force a redraw
                    v.invalidate();

                    // Does a getResult(), and displays what happened.
                    if (event.getResult()) {
                        Toast.makeText(this, "The drop was handled.", Toast.LENGTH_LONG);

                    } else {
                        Toast.makeText(this, "The drop didn't work.", Toast.LENGTH_LONG);

                    };

                    // returns true; the value is ignored.
                    return(true);

                break;

                // An unknown action type was received.
                default:
                    Log.e("DragDrop Example","Unknown action type received by OnDragListener.");

                break;
        };
    };
};