二十三、实现拖放

在上一章中,我们介绍了触摸屏、 MotionEvent 类和手势。您学习了如何使用触摸在您的应用中实现一些事情。我们没有涉及的一个领域是拖放。从表面上看,拖放似乎相当简单:触摸屏幕上的一个对象,在屏幕上拖动它(通常在其他对象上),然后放开,应用应该采取适当的动作。在许多计算机操作系统中,这是从桌面上删除文件的常用方法;你只要把文件的图标拖到垃圾桶图标上,文件就被删除了。在 Android 中,你可能已经看到如何通过将图标拖到新位置或垃圾桶来重新排列主屏幕上的图标。

这一章将深入探讨拖放。在 Android 3.0 之前,当涉及到拖放时,开发人员只能靠自己。但是因为仍然有相当多的手机运行 Android 2.3,我们将向您展示如何在这些手机上进行拖放。我们将在本章的第一节向您展示旧的方法,然后在第二部分向您展示新的方法。

探索拖放

在下一个示例应用中,我们将选取一个白点,并将其拖动到用户界面中的新位置。我们还将在用户界面中放置三个计数器,如果用户将白点拖到其中一个计数器上,该计数器将递增,该点将返回到其起始位置。如果这个点被拖到屏幕上的其他地方,我们就把它留在那里。

注意参见本章末尾的“参考”一节,获取可以将这些项目直接导入 IDE 的 URL。我们将只在文本中显示代码来解释概念。您需要下载代码来创建一个工作示例应用。

本章的第一个示例应用叫做 TouchDragDemo 。在这一部分中,我们要讨论两个关键文件:

  • /res/layout/main.xml
  • /src/com/Android book/touch/drag demo/dot . Java

main.xml 文件包含我们的拖放演示的布局。如清单 23-1 所示。我们希望您注意的一些关键概念是使用一个 FrameLayout 作为顶层布局,其中有一个 LinearLayout ,包含 TextView s 和一个名为 Dot 的自定义 View 类。因为 LinearLayout 和 Dot 共存于 FrameLayout 中,它们的位置和大小实际上不会相互影响,但是它们将共享屏幕空间,一个在另一个之上。该应用的用户界面如图 23-1 所示。

清单 23-1 。我们的拖动示例的示例布局 XML

<?xml version="1.0" encoding="utf-8"?>
<!-- This file is res/layout/main.xml -->
<FrameLayout xmlns:android="[http://schemas.android.com/apk/res/android](http://schemas.android.com/apk/res/android)"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#0000ff" >

  <LinearLayout android:id="@+id/counters"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <TextView android:id="@+id/top" android:text="0"
      android:background="#111111"
      android:layout_height="wrap_content"
      android:layout_width="60dp"
      android:layout_gravity="right"
      android:layout_marginTop="30dp"
      android:layout_marginBottom="30dp"
      android:padding="10dp" />

    <TextView android:id="@+id/middle" android:text="0"
      android:background="#111111"
      android:layout_height="wrap_content"
      android:layout_width="60dp"
      android:layout_gravity="right"
      android:layout_marginBottom="30dp"
      android:padding="10dp" />

    <TextView android:id="@+id/bottom" android:text="0"
      android:background="#111111"
      android:layout_height="wrap_content"
      android:layout_width="60dp"
      android:layout_gravity="right"
      android:padding="10dp" />
  </LinearLayout>

  <com.androidbook.touch.dragdemo.Dot android:id="@+id/dot"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

</FrameLayout>

9781430246800_Fig23-01.jpg

图 23-1 。触摸屏演示的用户界面

注意,布局 XML 文件中的包名必须与应用中使用的包名相匹配。如上所述,点的布局与线状布局是分开的。这是因为我们想要在屏幕上自由移动圆点,这也是我们选择【match _ parent】的 layout_width 和 layout_height 的原因。当我们在屏幕上画点时,我们希望它是可见的,如果我们将我们的点的视图的大小压缩到点的直径,当我们将它从我们的起始位置拖开时,我们将看不到它。

注意从技术上来说,我们可以在 FrameLayout 标签中将 android:clipChildren 设置为 true ,并将点的布局宽度和高度设置为 wrap_content ,但是这样感觉不太干净。

对于每个计数器,我们简单地用背景、填充、边距和重力来布置它们,使它们显示在屏幕的右侧。我们从零开始,但是你很快就会看到,当点被拖到它们上面时,我们会增加这些值。虽然在这个例子中我们选择使用文本视图的,但是你可以使用任何视图对象作为拖放目标。现在我们来看看清单 23-2 中点类的 Java 代码。

清单 23-2 。我们的点类的 Java 代码

public class Dot extends View {
    private static final String TAG = "TouchDrag";
    private float left = 0;
    private float top = 0;
    private float radius = 20;
    private float offsetX;
    private float offsetY;
    private Paint myPaint;
    private Context myContext;

    public Dot(Context context, AttributeSet attrs) {
        super(context, attrs);

        // Save the context (the activity)
        myContext = context;

        myPaint = new Paint();
        myPaint.setColor(Color.WHITE);
        myPaint.setAntiAlias(true);
    }

    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        float eventX = event.getX();
        float eventY = event.getY();
        switch(action) {
        case MotionEvent.ACTION_DOWN:
            // First make sure the touch is on our dot,
            // since the size of the dot's view is
            // technically the whole layout. If the
            // touch is *not* within, then return false
            // indicating we don't want any more events.
            if( !(left-20 < eventX && eventX < left+radius*2+20 &&
                top-20 < eventY && eventY < top+radius*2+20))
                return false;

            // Remember the offset of the touch as compared
            // to our left and top edges.
            offsetX = eventX - left;
            offsetY = eventY - top;
            break;
        case MotionEvent.ACTION_MOVE:
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            left = eventX - offsetX;
            top = eventY - offsetY;
            if(action == MotionEvent.ACTION_UP) {
                checkDrop(eventX, eventY);
            }
            break;
        }
        invalidate();
        return true;
    }

    private void checkDrop(float x, float y) {
        // See if the x,y of our drop location is near to
        // one of our counters. If so, increment it, and
        // reset the dot back to its starting position
        Log.v(TAG, "checking drop target for " + x + ", " + y);

        int viewCount = ((MainActivity)myContext).counterLayout
                          .getChildCount();

        for(int i = 0; i<viewCount; i++) {
            View view = ((MainActivity)myContext).counterLayout
                          .getChildAt(i);
            if(view.getClass() == TextView.class){
                Log.v(TAG, "Is the drop to the right of " +
                            (view.getLeft()-20));
                Log.v(TAG, "  and vertically between " +
                          (view.getTop()-20) +
                          " and " + (view.getBottom()+20) + "?");
                if(x > view.getLeft()-20 &&
                        view.getTop()-20 < y &&
                        y < view.getBottom()+20) {
                    Log.v(TAG, "     Yes. Yes it is.");

                    // Increase the count value in the TextView by one
                    int count =
                        Integer.parseInt(
                            ((TextView)view).getText().toString());
                    ((TextView)view).setText(String.valueOf( ++count ));

                    // Reset the dot back to starting position
                    left = top = 0;
                    break;
                }
            }
        }
    }

    public void draw(Canvas canvas) {
        canvas.drawCircle(left + radius, top + radius, radius, myPaint);
    }
}

当你运行这个应用时,你会看到一个蓝底白点。你可以触摸这个点并在屏幕上拖动它。当您抬起手指时,圆点会停留在原来的位置,直到您再次触摸它并将它拖到其他地方。 draw() 方法 将圆点放在其当前的左上方位置,并根据圆点的半径进行调整。通过在 onTouchEvent() 方法中接收 MotionEvent 对象,我们可以通过触摸的移动来修改左边和上边的值。

因为用户不会总是触摸对象的精确中心,所以触摸坐标不会与对象的位置坐标相同。这就是偏移值的目的:让我们从触摸的位置回到点的左边和上边。但是,即使在我们开始拖动操作之前,我们也希望确保用户的触摸被认为离点足够近才有效。如果用户触摸屏幕远离点,这在技术上是在点的视图布局内,我们不希望这开始一个拖动序列。这就是为什么我们要观察触摸是否在白点本身之内;如果不是,我们简单地返回假,这阻止在该触摸序列中接收任何更多的触摸事件。

当你的手指开始在屏幕上移动时,我们根据得到的运动事件来调整对象在 x 和 y 方向的位置。当你停止移动( ACTION_UP ,我们使用你触摸的最后坐标来确定我们的位置。在这个例子中,我们不必担心滚动条,滚动条会使我们的对象在屏幕上的位置计算变得复杂。但是基本原理还是一样的。通过知道要移动的对象的起始位置并跟踪从动作 _ 向下到动作 _ 向上的触摸的增量值,我们可以调整对象在屏幕上的位置。

将一个物体放到屏幕上的另一个物体上,与触摸的关系要小得多,与了解物体在屏幕上的位置关系更大。当我们在屏幕上拖动一个物体时,我们知道它相对于一个或多个参考点的位置。我们还可以询问屏幕上物体的位置和大小。然后我们可以确定我们拖动的对象是否在另一个对象的“上方”。确定被拖动对象的拖放目标的典型过程是遍历可以拖放的可用对象,并确定我们的当前位置是否与该对象重叠。每个物体的大小和位置(有时是形状)可以用来做这个决定。如果我们得到一个 ACTION_UP 事件,这意味着用户已经放开了我们拖动的对象,并且该对象位于我们可以拖放的对象之上,我们可以触发逻辑来处理拖放操作。

我们在示例应用中使用了这种方法。当检测到 ACTION_UP 动作时,我们接着查看 LinearLayout 的子视图,对于找到的每个 TextView ,我们将触摸的位置与 TextView 的边缘进行比较(加上一点额外的)。如果触摸在那个文本视图内,我们获取文本视图的当前数值,递增 1,并写回。如果发生这种情况,点的位置将被重置回其开始位置(left = 0,top = 0 ),以便进行下一次拖动。

我们的例子向您展示了在 3.0 之前的 Android 中进行拖放的基本方法。这样你就可以在你的应用中实现拖放功能。这可能是将某个对象拖到垃圾桶的动作,在垃圾桶中被拖动的对象应该被删除,也可能是将文件拖到文件夹中以便移动或复制它。为了美化您的应用,您可以预先确定哪些视图是潜在的拖放目标,并在拖动开始时使它们在视觉上发生变化。如果你希望被拖动的对象在被放下时从屏幕上消失,你总是可以通过编程从布局中移除它(参见视图组中的各种 removeView 方法)。

既然你已经看到了拖放的艰难过程,我们将向你展示 Android 3.0 中添加的拖放支持。

3.0+中拖放的基础知识

在 Android 3.0 之前,没有直接支持拖拽。在本章的第一节中,你学习了如何在屏幕上拖动一个视图;您还了解了可以使用被拖动对象的当前位置来确定下面是否有拖放目标。当接收到竖起手指事件的 MotionEvent 时,您的代码可以判断这是否意味着发生了跌落。尽管这是可行的,但肯定不如在 Android 中直接支持拖放操作那么容易。你现在得到了直接支持。

在最基本的情况下,拖放操作从一个声明拖动已经开始的视图开始;然后,所有相关方都会看到拖动的发生,直到 drop 事件被触发。如果一个视图捕捉到了拖放事件并想要接收它,那么拖放就发生了。如果没有视图接收放置,或者接收放置的视图不想要放置,那么就不会发生放置。拖动是通过使用一个 DragEvent 对象来传递的,该对象被传递给所有可用的拖动监听器。

在 DragEvent 对象中有大量信息的描述符,这取决于拖动序列的发起者。例如, DragEvent 可以包含对发起者本身的对象引用、状态信息、文本数据、URIs,或者任何您想通过拖动序列传递的东西。

可以传递导致视图到视图动态通信的信息;然而,当 DragEvent 被创建时, DragEvent 对象中的发起者数据被设置,并且此后保持不变。除了这些数据之外,拖拽事件还有一个动作值,指示拖拽序列正在进行什么,以及位置信息,指示拖拽在屏幕上的位置。

一个拖拽事件有六种可能的动作:

  • ACTION _ DRAG _ STARTEDT3】表示一个新的拖动序列已经开始。
  • ACTION _ DRAG _ enterT3】表示被拖动的对象已经被拖动到特定视图的边界内。
  • ACTION _ DRAG _ LOCATIONT3】表示被拖动的对象已经在屏幕上被拖动到一个新的位置。
  • ACTION _ DRAG _ EXITED 表示被拖动的对象已经被拖动到特定视图的边界之外。
  • ACTION_DROP 表示用户已经放开了被拖动的对象。由该事件的接收者来确定这是否真正意味着发生了丢弃。
  • ACTION _ DRAG _ ENDED 告诉所有的拖拽监听器,之前的拖拽序列已经结束。 DragEvent.getResult() 方法表示成功丢弃或失败。

您可能认为您需要在系统中参与拖动序列的每个视图上设置一个拖动监听器;但是,事实上,您可以在应用中的任何内容上定义一个拖动监听器,它将接收系统中所有视图的所有拖动事件。这可能会使事情变得有点混乱,因为拖动侦听器不需要与被拖动的对象或放置目标相关联。监听器可以管理所有的拖放协调。

事实上,如果您检查 Android SDK 附带的拖放示例项目,您会看到它在一个与实际拖放无关的 TextView 上设置了一个侦听器。接下来的示例项目使用绑定到特定视图的拖动侦听器。这些拖动监听器每个都接收一个拖动事件对象,用于拖动序列中发生的拖动事件。这意味着一个视图可能会接收到一个可以被忽略的 DragEvent 对象,因为它实际上是关于一个不同的视图。这也意味着拖动监听器必须在代码中做出决定,并且在 DragEvent 对象中必须有足够的信息让拖动监听器知道该做什么。

如果一个拖动监听器得到一个 DragEvent 对象,它只是说有一个未知的对象正在被拖动,并且它在坐标(15,57)处,那么拖动监听器对它没有什么作用。获得一个 DragEvent 对象会更有帮助,它表示一个特定的对象正在被拖动,它位于坐标(15,57),这是一个复制操作,数据是一个特定的 URI。当这个值下降时,就有足够的信息来启动拷贝操作。

我们实际上看到了两种不同的拖拽方式。在我们的第一个示例应用中,我们将一个视图拖过一个框架布局,我们可以放开它,视图将停留在原来的位置。当我们把视图放到其他东西上面时,我们只有拖放行为。支持的拖放形式与此不同。现在,当您将视图作为拖放序列的一部分进行拖动时,被拖动的视图根本不会移动。我们得到了拖动视图的阴影图像,它在屏幕上移动,但是如果我们放开它,阴影视图就消失了。这意味着,在 Android 3.0+应用中,你可能仍然有机会使用本章开头的技术,在屏幕上移动图像,而不需要拖放。

拖放示例应用

对于您的下一个示例应用,您将使用 3.0 版本的 staple,即片段。这将证明拖拽可以跨越片段边界。您将在左侧创建一个点调色板,在右侧创建一个方形目标。当长时间点击一个点时,你将改变调色板中该点的颜色,Android 将在你拖动时显示该点的阴影。当拖动的点到达正方形目标时,目标将开始发光。如果您将圆点放在方形目标上,将会出现一条信息,提示您刚刚向滴数中添加了一滴,发光将会停止,原来的圆点将恢复到原来的颜色。

文件列表

这个应用建立在我们在本书中讨论的概念之上。我们将只在文本中包含有趣的文件。至于其他的,你可以在空闲的时候在你的 IDE 里看看。以下是我们包含在文本中的内容:

  • palette.xml 是左侧圆点的片段布局(见清单 23-3 )。
  • dropzone.xml 是右边正方形目标的片段布局,加上 drop-count 消息(见清单 23-4 )。
  • DropZone.java 膨胀 dropzone.xml 片段布局文件,然后实现拖放目标的拖动监听器(见清单 23-5 )。
  • 是你要拖动的对象的自定义视图类。它处理拖动序列的开始,观察拖动事件,并画出点(见清单 23-6 )。

布置示例拖放应用

在我们进入代码之前,图 23-2 展示了应用的样子。

9781430246800_Fig23-02.jpg

图 23-2 。拖放片段示例应用用户界面

主布局文件有一个简单的水平线性布局和两个片段规范。第一个片段将用于点调色板,第二个片段将用于 dropzone。

调色板片段布局文件(清单 23-3 )变得更有趣了。虽然这个布局表示一个片段,但是您不需要在这个布局中包含一个片段标记。这个布局将被放大,成为您的面板片段的视图层次结构。这些点被指定为自定义点,其中有两个垂直排列。注意,在 dots 的定义中有几个定制的 XML 属性( dot:color 和 dot:radius) 。如您所见,这些属性指定了点的颜色和半径。您可能还注意到,布局的宽度和高度是 wrap_content ,而不是本章前面的示例应用中的 match_parent 。新的拖放支持使事情变得更加容易。

清单 23-3 。点 s 的 palette.xml 布局文件

<?xml version="1.0" encoding="utf-8"?>
<!-- This file is res/layout/palette.xml -->
<LinearLayout
  xmlns:android="[http://schemas.android.com/apk/res/android](http://schemas.android.com/apk/res/android)"
  xmlns:dot=
    "[http://schemas.android.com/apk/res/com.androidbook.drag.drop.demo](http://schemas.android.com/apk/res/com.androidbook.drag.drop.demo)"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="vertical">

  <com.androidbook.drag.drop.demo.Dot android:id="@+id/dot1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:padding="30dp"
    android:tag="Blue dot"
    dot:color="#ff1111ff"
    dot:radius="20dp"  />

  <com.androidbook.drag.drop.demo.Dot android:id="@+id/dot2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:padding="10dp"
    android:tag="White dot"
    dot:color="#ffffffff"
    dot:radius="40dp"  />

</LinearLayout>

清单 23-4 中的 dropzone 片段布局文件也很容易理解。有一个绿色方块和一条横向排列的短信。这将是您将要拖动的点的拖放区。文本消息将用于显示液滴的运行计数。

清单 23-4 。 dropzone.xml 布局文件

<?xml version="1.0" encoding="utf-8"?>
<!-- This file is res/layout/dropzone.xml -->
<LinearLayout
  xmlns:android="[http://schemas.android.com/apk/res/android](http://schemas.android.com/apk/res/android)"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="horizontal" >

  <View android:id="@+id/droptarget"
    android:layout_width="75dp"
    android:layout_height="75dp"
    android:layout_gravity="center_vertical"
    android:background="#00ff00" />

  <TextView android:id="@+id/dropmessage"
    android:text="0 drops"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center_vertical"
    android:paddingLeft="50dp"
    android:textSize="17sp" />

</LinearLayout>

在空投区回应翁德拉格

现在您已经有了主应用布局,让我们通过检查清单 23-5 来看看需要如何组织放置目标。

清单 23-5 。DropZone.java 文件

public class DropZone extends Fragment {

    private View dropTarget;
    private TextView dropMessage;

    @Override
    public View onCreateView(LayoutInflater inflater,
            ViewGroup container, Bundle icicle)
    {
        View v = inflater.inflate(R.layout.dropzone, container, false);

        dropMessage = (TextView)v.findViewById(R.id.dropmessage);

        dropTarget = (View)v.findViewById(R.id.droptarget);
        dropTarget.setOnDragListener(new View.OnDragListener() {
            private static final String DROPTAG = "DropTarget";
            private int dropCount = 0;
            private ObjectAnimator anim;

            public boolean onDrag(View v, DragEvent event) {
                int action = event.getAction();
                boolean result = true;
                switch(action) {
                case DragEvent.ACTION_DRAG_STARTED:
                    Log.v(DROPTAG, "drag started in dropTarget");
                    break;
                case DragEvent.ACTION_DRAG_ENTERED:
                    Log.v(DROPTAG, "drag entered dropTarget");
                    anim = ObjectAnimator.ofFloat(
                                (Object)v, "alpha", 1f, 0.5f);
                    anim.setInterpolator(new CycleInterpolator(40));
                    anim.setDuration(30*1000); // 30 seconds
                    anim.start();
                    break;
                case DragEvent.ACTION_DRAG_EXITED:
                    Log.v(DROPTAG, "drag exited dropTarget");
                    if(anim != null) {
                        anim.end();
                        anim = null;
                    }
                    break;
                case DragEvent.ACTION_DRAG_LOCATION:
                    Log.v(DROPTAG, "drag proceeding in dropTarget: " +
                            event.getX() + ", " + event.getY());
                    break;
                case DragEvent.ACTION_DROP:
                    Log.v(DROPTAG, "drag drop in dropTarget");
                    if(anim != null) {
                        anim.end();
                        anim = null;
                    }

                    ClipData data = event.getClipData();
                    Log.v(DROPTAG, "Item data is " +
                          data.getItemAt(0).getText());

                    dropCount++;
                    String message = dropCount + " drop";
                    if(dropCount > 1)
                        message += "s";
                    dropMessage.setText(message);
                    break;
                case DragEvent.ACTION_DRAG_ENDED:
                    Log.v(DROPTAG, "drag ended in dropTarget");
                    if(anim != null) {
                        anim.end();
                        anim = null;
                    }
                    break;
                default:
                    Log.v(DROPTAG, "other action in dropzone: " +
                                   action);
                    result = false;
                }
                return result;
            }
        });
        return v;
    }
}

现在你开始进入有趣的代码。对于 dropzone,您需要创建要在其上拖动点的目标。正如您前面看到的,布局在屏幕上指定了一个绿色方块,旁边有一条文本消息。因为 dropzone 也是一个片段,所以您覆盖了 DropZone 的 onCreateView() 方法 。首先要做的是膨胀 dropzone 布局,然后提取方形目标( dropTarget )和文本消息( dropMessage )的视图引用。然后,您需要在目标上设置一个拖动监听器,这样它就会知道拖动何时开始。

拖放目标拖动监听器中有一个回调方法: onDrag() 。这个回调将接收一个视图引用以及一个 DragEvent 对象。视图引用与拖拽事件相关的视图相关。如前所述,拖动侦听器不一定连接到将与拖动事件交互的视图,因此这个回调必须标识发生拖动事件的视图。

在任何 onDrag() 回调中,您可能要做的第一件事就是从 DragEvent 对象中读取动作。这会告诉你发生了什么。在大多数情况下,您在这个回调中唯一想做的事情就是记录一个拖动事件正在发生的事实。例如,你不需要为 ACTION_DRAG_LOCATION 做任何事情。但是,当对象被拖动到您的边界内时,您确实希望有一些特殊的逻辑(ACTION _ DRAG _ enter),当对象被拖动到您的边界外时( ACTION_DRAG_EXITED )或者当对象被放下时( ACTION_DROP ),这些逻辑将被关闭。

你正在使用在第 18 章中介绍的 ObjectAnimator 类 ,只是在这里你在代码中使用它来指定一个修改目标 alpha 的循环插值器。这将具有使绿色目标方块的透明度脉动的效果,这将是目标愿意接受物体掉落到其上的视觉指示。因为您打开了动画,所以您必须确保在对象离开或被放下,或者拖放结束时也关闭动画。理论上,你不需要在 ACTION_DRAG_ENDED 上停止动画,但无论如何这样做是明智的。

对于这个特定的拖动监听器,只有当被拖动的对象与您关联的视图交互时,您才会得到 ACTION _ DRAG _ enter 和 ACTION_DRAG_EXITED 。正如您将看到的, ACTION_DRAG_LOCATION 事件只有在被拖动的对象在您的目标视图中时才会发生。

另一个有趣的条件是 ACTION_DROP 本身(注意 DRAG_ 不是这个动作名称的一部分)。如果你的视图上出现了一个点,这意味着用户已经放开了绿色方块上的点。因为您希望这个对象被放到绿色方块上,所以您可以直接从第一个项目中读取数据,然后将其记录到 LogCat 中。在生产应用中,您可能会更加关注包含在拖动事件本身中的 ClipData 对象。通过检查它的属性,您可以决定是否接受这个拖放操作。

这是在这个 onDrag() 回调方法中指出结果布尔值的好时机。根据事情的进展,你想让 Android 知道你处理了拖动事件(通过返回真)或者你没有处理(通过返回假)。如果您在拖动事件对象中没有看到您想要看到的内容,您当然可以从这个回调中返回 false,这将告诉 Android 这个拖放操作没有被处理。

一旦在 LogCat 中记录了来自拖动事件的信息,就会增加接收到的 drops 的计数;这是在用户界面中更新的,关于 DropZone 也就这些了。

如果你看一下这个类,它真的很简单。这里实际上没有任何处理 MotionEvents 的代码,也不需要自己判断是否有拖拽在进行。随着拖动序列的展开,您只需获得适当的回调调用。

设置拖动源视图

现在让我们考虑一下对应于一个拖动源的视图是如何组织的,从查看清单 23-6 开始。

清单 23-6 。自定义视图的 Java:点

public class Dot extends View
    implements View.OnDragListener
{
    private static final int DEFAULT_RADIUS = 20;
    private static final int DEFAULT_COLOR = Color.WHITE;
    private static final int SELECTED_COLOR = Color.MAGENTA;
    protected static final String DOTTAG = "DragDot";
    private Paint mNormalPaint;
    private Paint mDraggingPaint;
    private int mColor = DEFAULT_COLOR;
    private int mRadius = DEFAULT_RADIUS;
    private boolean inDrag;

    public Dot(Context context, AttributeSet attrs) {
        super(context, attrs);

        // Apply attribute settings from the layout file.
        // Note: these could change on a reconfiguration
        // such as a screen rotation.
        TypedArray myAttrs = context.obtainStyledAttributes(attrs,
                R.styleable.Dot);

        final int numAttrs = myAttrs.getIndexCount();
        for (int i = 0; i < numAttrs; i++) {
            int attr = myAttrs.getIndex(i);
            switch (attr) {
            case R.styleable.Dot_radius:
                mRadius = myAttrs.getDimensionPixelSize(attr,
                          DEFAULT_RADIUS);
                break;
            case R.styleable.Dot_color:
                mColor = myAttrs.getColor(attr, DEFAULT_COLOR);
                break;
            }
        }
        myAttrs.recycle();

        // Setup paint colors
        mNormalPaint = new Paint();
        mNormalPaint.setColor(mColor);
        mNormalPaint.setAntiAlias(true);

        mDraggingPaint = new Paint();
        mDraggingPaint.setColor(SELECTED_COLOR);
        mDraggingPaint.setAntiAlias(true);

        // Start a drag on a long click on the dot
        setOnLongClickListener(lcListener);
        setOnDragListener(this);
    }

    private static View.OnLongClickListener lcListener =
        new View.OnLongClickListener() {
        private boolean mDragInProgress;

        public boolean onLongClick(View v) {
            ClipData data =
            ClipData.newPlainText("DragData", (String)v.getTag());

            mDragInProgress =
            v.startDrag(data, new View.DragShadowBuilder(v),
                    (Object)v, 0);

            Log.v((String) v.getTag(),
              "starting drag? " + mDragInProgress);

            return true;
        }
    };

    @Override
    protected void onMeasure(int widthSpec, int heightSpec) {
        int size = 2*mRadius + getPaddingLeft() + getPaddingRight();
        setMeasuredDimension(size, size);
    }

    // The dragging functionality
    public boolean onDrag(View v, DragEvent event) {
        String dotTAG = (String) getTag();
        // Only worry about drag events if this is us being dragged
        if(event.getLocalState() != this) {
            Log.v(dotTAG, "This drag event is not for us");
            return false;
        }
        boolean result = true;

        // get event values to work with
        int action = event.getAction();
        float x = event.getX();
        float y = event.getY();

        switch(action) {
        case DragEvent.ACTION_DRAG_STARTED:
            Log.v(dotTAG, "drag started. X: " + x + ", Y: " + y);
            inDrag = true; // used in draw() below to change color
            break;
        case DragEvent.ACTION_DRAG_LOCATION:
            Log.v(dotTAG, "drag proceeding… At: " + x + ", " + y);
            break;
        case DragEvent.ACTION_DRAG_ENTERED:
            Log.v(dotTAG, "drag entered. At: " + x + ", " + y);
            break;
        case DragEvent.ACTION_DRAG_EXITED:
            Log.v(dotTAG, "drag exited. At: " + x + ", " + y);
            break;
        case DragEvent.ACTION_DROP:
            Log.v(dotTAG, "drag dropped. At: " + x + ", " + y);
            // Return false because we don't accept the drop in Dot.
            result = false;
            break;
        case DragEvent.ACTION_DRAG_ENDED:
            Log.v(dotTAG, "drag ended. Success? " + event.getResult());
            inDrag = false; // change color of original dot back
            break;
        default:
            Log.v(dotTAG, "some other drag action: " + action);
            result = false;
            break;
        }
        return result;
    }

    // Here is where you draw our dot, and where you change the color if
    // you're in the process of being dragged. Note: the color change
    // affects the original dot only, not the shadow.
    public void draw(Canvas canvas) {
        float cx = this.getWidth()/2 + getLeftPaddingOffset();
        float cy = this.getHeight()/2 + getTopPaddingOffset();
        Paint paint = mNormalPaint;
        if(inDrag)
            paint = mDraggingPaint;
        canvas.drawCircle(cx, cy, mRadius, paint);
        invalidate();
    }
}

点代码看起来有点类似于 DropZone 的代码。这在一定程度上是因为在这个类中也接收到了拖动事件。一个点的构造器计算出属性以设置正确的半径和颜色,然后它设置两个监听器:一个用于长点击,另一个用于拖动事件。

这两种颜料将被用来画你的圆圈。当圆点就在那里的时候,你使用普通的颜料。但是,当拖动点时,您希望通过将原件的颜色更改为洋红色来表明这一点。

长点击监听器是您启动拖动序列的地方。让用户开始拖动点的唯一方式是用户点击并按住点。当长点击监听器触发时,您使用一个字符串和点的标签创建一个新的 ClipData 对象。您碰巧知道标记是 XML 布局文件中指定的点的名称。还有其他几种方法可以将数据指定到一个 ClipData 对象中,所以请随意阅读参考文档中关于在一个 ClipData 对象中存储数据的其他方法。

接下来的语句是关键的一个: startDrag() 。这是 Android 将接管并开始拖动过程的地方。注意,第一个参数是之前的 ClipData 对象;然后是拖动阴影对象,然后是本地状态对象,最后是数字零。

拖动阴影对象是拖动过程中显示的图像。在您的情况下,这不会替换屏幕上的原始点图像,但在拖动时,除了屏幕上的原始点之外,还会显示一个点的阴影。默认的 dragshaodbuilder 行为是创建一个看起来非常像原始的阴影,所以对于你的目的,你仅仅调用它并在你的视图中传递。在这里,你可以随心所欲地创建任何你想要的阴影视图,但是如果你要覆盖这个类,你需要实现一些方法来使它工作。

这里的 onMeasure() 方法 为 Android 提供您在这里使用的自定义视图的维度信息。你必须告诉 Android 你的视图有多大,这样它才知道如何把它和其他东西放在一起。这是自定义视图的标准做法。

最后,还有一个 onDrag() 回调。如上所述,每个拖动监听器都可以接收拖动事件。比如他们都得到动作 _ 拖动 _ 开始和动作 _ 拖动 _ 结束。因此,当事件发生时,你必须小心处理这些信息。因为在这个示例应用中有两个点,所以无论何时使用这些点做什么,都必须小心不要影响到正确的点。

当两个点都接收到 ACTION_DRAG_STARTED 动作时,只有一个点应该将自己的颜色设置为洋红色。要弄清楚哪一个是正确的,请将传入的本地状态对象与您自己进行比较。如果您回顾设置本地状态对象的位置,您将当前视图传递给了。现在,当您接收到本地状态对象时,您将它与您自己进行比较,看看您是否是启动拖动序列的视图。

如果您不同意这种观点,您可以向 LogCat 写一条日志消息,说明这不适合您,然后返回 false 说明您没有处理这条消息。

如果您是应该接收这个拖动事件的视图,那么您从拖动事件中收集一些值,然后您只需将该事件记录到 LogCat 中。第一个例外是 ACTION_DRAG_STARTED 。如果你得到了这个动作,并且是给你的,那么你就知道你的点已经开始了一个拖动序列。因此,您设置了 inDrag boolean,以便稍后的 draw() 方法会做正确的事情并显示不同颜色的点。这种不同的颜色只持续到收到 ACTION_DRAG_ENDED 为止,这时你就恢复了点的原始颜色。

如果一个点得到了 ACTION_DROP 动作,这意味着用户试图将一个点放到一个点上——甚至可能是原来的点。这不应该做任何事情,所以你只要从这个回调函数中返回 false 就可以了。

最后,您的自定义视图的 draw() 方法计算出您的圆(点)的中心点的位置,然后用适当的颜料画出来。 invalidate() 方法 告诉 Android 你已经修改了视图,Android 应该重新绘制用户界面。通过调用 invalidate() ,您可以确保用户界面会很快被新内容更新。

现在,您已经拥有了编译和部署这个示例拖放应用所需的所有文件和背景。

测试示例拖放应用

下面是我们运行这个示例应用时 LogCat 的一些示例输出。注意日志消息如何使用蓝点 来表示来自蓝点的消息,白点 表示来自白点的消息, DropTarget 表示允许放置的视图。

White dot:  starting drag? true
Blue dot:   This drag event is not for us
White dot:  drag started. X: 53.0, Y: 206.0
DropTarget: drag started in dropTarget
DropTarget: drag entered dropTarget
DropTarget: drag proceeding in dropTarget: 29.0, 36.0
DropTarget: drag proceeding in dropTarget: 48.0, 39.0
DropTarget: drag proceeding in dropTarget: 45.0, 39.0
DropTarget: drag proceeding in dropTarget: 41.0, 39.0
DropTarget: drag proceeding in dropTarget: 40.0, 39.0
DropTarget: drag drop in dropTarget
DropTarget: Item data is White dot
ViewRoot:   Reporting drop result: true
White dot:  drag ended. Success? true
Blue dot:   This drag event is not for us
DropTarget: drag ended in dropTarget

在这个特殊的例子中,拖动是从白点开始的。一旦长点击触发了拖动序列的开始,我们就会得到开始拖动的消息。

请注意,接下来的三行都表示在三个不同的视图中收到了一个 ACTION_DRAG_STARTED 动作。蓝点确定回调不适合它。这也不是为了的 DropTarget 。

接下来,注意拖动进行消息如何显示通过 DropTarget 发生的拖动,从 ACTION _ DRAG _ enter 动作开始。这意味着圆点被拖到了绿色方块的顶部。拖动事件对象中报告的 x 和 y 坐标是拖动点相对于视图左上角的坐标。因此,在示例应用中,拖放目标中的第一条拖动记录位于(x,y) = (29,36),拖放发生在(40,39)。看看 drop target 是如何从事件的 ClipData 中提取白点的标签名并将其写入 LogCat 的。

同样,看看所有的拖动监听器是如何接收到 ACTION_DRAG_ENDED 动作的。只有白点确定可以使用 getResult() 显示结果。

请随意试验这个示例应用。将一个点拖到另一个点,甚至拖到它本身。继续添加另一个点到 palette.xml 。请注意,当拖动的圆点离开绿色方块时,会有一条消息提示拖动已退出。还要注意的是,如果你把一个点放到绿色方块以外的地方,那么这个点就被认为是失败的。

参考

以下是一些对您可能希望进一步探索的主题有帮助的参考:

  • :与本书相关的可下载项目列表。对于这一章,寻找一个名为 pro Android 5 _ Ch23 _ dragndrop . zip 的 zip 文件。这个 zip 文件包含本章中的所有项目,列在单独的根目录中。还有一个自述。TXT 文件,它准确地描述了如何将项目从这些 zip 文件之一导入到您的 IDE 中。
  • 【http://developer.android.com/guide/topics/ui/drag-drop.html】:Android 开发者拖拽指南。

摘要

让我们总结一下本章涉及的主题:

  • Android 3.0 中的拖放支持,并在 3.0 之前使用其他方法实现它
  • 遍历可能的放置目标,查看是否发生了放置(即手指在拖动后离开屏幕)
  • 计算跟踪拖动对象的位置以及它是否在放置目标上的难度
  • Android 3.0+中的拖放支持,这要好得多,因为它消除了许多猜测
  • 拖动侦听器,它可以是任何对象,不需要是可拖动对象或拖放目标视图
  • 拖动可以发生在片段之间的事实
  • DragEvent 对象,它可以包含大量关于什么被拖动以及为什么被拖动的信息
  • Android 如何利用数学来确定视图顶部是否发生了拖放