ビューでドラッグ&ドロップを実装する

ドラッグ&ドロップ プロセスをビューに実装するには、ドラッグ開始をトリガーする可能性のあるイベントに応答し、応答してドロップ イベントを使用します。

ドラッグを開始する

ユーザーはジェスチャーでドラッグを開始します。通常は、ドラッグするアイテムをタップまたは長押しします。

これを View で処理するには、移動するデータの ClipData オブジェクトと ClipData.Item オブジェクトを作成します。ClipData の一部として、ClipData 内の ClipDescription オブジェクトに格納されているメタデータを指定します。データの移動以外のドラッグ&ドロップ オペレーションの場合は、実際のオブジェクトの代わりに null を使用することをおすすめします。

例として、次のコード スニペットは、ImageView のタグ(ラベル)を含む ClipData オブジェクトを作成することで、ImageView 上での長押しジェスチャーに対応する方法を示しています。

Kotlin

// Create a string for the ImageView label.
val IMAGEVIEW_TAG = "icon bitmap"
...
val imageView = ImageView(context).apply {
    // Set the bitmap for the ImageView from an icon bitmap defined elsewhere.
    setImageBitmap(iconBitmap)
    tag = IMAGEVIEW_TAG
    setOnLongClickListener { 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.
        val item = ClipData.Item(v.tag as? CharSequence)

        // Create a new ClipData using the tag as a label, the plain text
        // MIME type, and the already-created item. This creates a new
        // ClipDescription object within the ClipData and sets its MIME type
        // to "text/plain".
        val dragData = ClipData(
            v.tag as? CharSequence,
            arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN),
            item)

        // Instantiate the drag shadow builder. We use this imageView object
        // to create the default builder.
        val myShadow = View.DragShadowBuilder(view: this)

        // Start the drag.
        v.startDragAndDrop(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.
        )

        // Indicate that the long-click is handled.
        true
    }
}

Java

// Create a string for the ImageView label.
private static final String IMAGEVIEW_TAG = "icon bitmap";
...
// Create a new ImageView.
ImageView imageView = new ImageView(context);

// Set the bitmap for the ImageView from an icon bitmap defined elsewhere.
imageView.setImageBitmap(iconBitmap);

// Set the tag.
imageView.setTag(IMAGEVIEW_TAG);

// Set a long-click listener for the ImageView using an anonymous listener
// object that implements the OnLongClickListener interface.
imageView.setOnLongClickListener( 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((CharSequence) v.getTag());

    // Create a new ClipData using the tag as a label, the plain text MIME type,
    // and the already-created item. This creates a new ClipDescription object
    // within the ClipData and sets its MIME type to "text/plain".
    ClipData dragData = new ClipData(
            (CharSequence) v.getTag(),
            new String[] { ClipDescription.MIMETYPE_TEXT_PLAIN },
            item);

    // Instantiate the drag shadow builder. We use this imageView object
    // to create the default builder.
    View.DragShadowBuilder myShadow = new View.DragShadowBuilder(imageView);

    // Start the drag.
    v.startDragAndDrop(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.
    );

    // Indicate that the long-click is handled.
    return true;
});

ドラッグ開始に対応する

ドラッグ オペレーションの間は、現在のレイアウトにある View オブジェクトのドラッグ イベント リスナーに対して、システムからドラッグ イベントが送信されます。リスナーは、DragEvent.getAction() を呼び出してアクション タイプを取得することで対応します。ドラッグの開始時、このメソッドは ACTION_DRAG_STARTED を返します。

アクション タイプ ACTION_DRAG_STARTED のイベントに応答して、ドラッグ イベント リスナーは次のことを行う必要があります。

  1. DragEvent.getClipDescription() を呼び出し、返された ClipDescription の MIME タイプ メソッドを使用して、リスナーがドラッグ対象のデータを受け入れ可能かどうかを確認します。

    ドラッグ&ドロップ オペレーションがデータの移動ではない場合、この処理は不要です。

  2. ドラッグ イベント リスナーがドロップを受理できる場合は、true を返して、引き続きリスナーにドラッグ イベントを送信するようシステムに指示する必要があります。リスナーがドロップを受け付けない場合、false を返す必要があります。システムは ACTION_DRAG_ENDED を送信してドラッグ&ドロップ オペレーションを完了するまで、リスナーへのドラッグ イベントの送信を停止します。

ACTION_DRAG_STARTED イベントの場合、DragEvent メソッド(getClipData()getX()getY()getResult())は無効です。

ドラッグ中にイベントを処理する

ドラッグ アクションの実行中、ACTION_DRAG_STARTED ドラッグ イベントに応じて true を返すドラッグ イベント リスナーは、引き続きドラッグ イベントを受信します。ドラッグ中にリスナーが受信するドラッグ イベントのタイプは、ドラッグ シャドウの場所とリスナーの View の可視性によって異なります。リスナーは主にドラッグ イベントを使用して、View の外観を変更する必要があるかどうかを判断します。

ドラッグの間、DragEvent.getAction() からは以下の 3 つの値のいずれかが返されます。

  • ACTION_DRAG_ENTERED: タッチポイント(ユーザーの指またはマウスの下にある画面上の点)がリスナーの View の境界ボックスに入ると、リスナーはこのイベント アクション タイプを受け取ります。
  • ACTION_DRAG_LOCATION: リスナーは ACTION_DRAG_ENTERED イベントを受信すると、ACTION_DRAG_EXITED イベントを受信するまで、タッチポイントが移動するたびに新しい ACTION_DRAG_LOCATION イベントを受信します。getX() メソッドと getY() メソッドは、タッチポイントの X 座標と Y 座標を返します。
  • ACTION_DRAG_EXITED: このイベント アクション タイプは、以前に ACTION_DRAG_ENTERED を受け取ったリスナーに送信されます。このイベントは、ドラッグ シャドウのタッチポイントがリスナーの View の境界ボックス内から境界ボックスの外に移動すると送信されます。

ドラッグ イベント リスナーは、これらのアクション タイプに反応する必要はありません。リスナーがシステムに値を返しても無視されます。

各アクション タイプを受信したときの対応について、以下にガイドラインを示します。

  • ACTION_DRAG_ENTEREDACTION_DRAG_LOCATION を受信したとき、リスナーは View の外観を変更することによって、ビューが潜在的なドロップ ターゲットであることを示せます。
  • アクション タイプ ACTION_DRAG_LOCATION のイベントには、タッチポイントの位置に対応する getX()getY() の有効なデータが含まれます。リスナーはこの情報を使用して、タッチポイントでの View の外観を変更したり、ユーザーがコンテンツをドロップできる正確な位置を決定したりできます。
  • ACTION_DRAG_EXITED に応じて、リスナーは ACTION_DRAG_ENTERED または ACTION_DRAG_LOCATION に応じて適用する外観の変更をすべてリセットする必要があります。こうすることで、その View が現時点でのドロップ先ではなくなったことをユーザーに示せます。

ドロップに対応する

ユーザーが View 上でドラッグ シャドウを解放し、以前に View がドラッグ対象のコンテンツを受け入れることができると報告すると、システムはアクション タイプ ACTION_DROPView にドラッグ イベントを送信します。

ドラッグ イベント リスナーは、次のことを行う必要があります。

  1. getClipData() を呼び出して、startDragAndDrop() の呼び出し時に指定された ClipData オブジェクトを取得し、データを処理します。ドラッグ&ドロップ オペレーションがデータ移動ではない場合、この処理は不要です。

  2. ドロップが正常に処理された場合はブール値 true を返し、そうでない場合は false を返します。戻り値は、最終的な ACTION_DRAG_ENDED イベントに対して getResult() が返す値になります。システムが ACTION_DROP イベントを送信しない場合、ACTION_DRAG_ENDED イベントに対して getResult() が返す値は false です。

ACTION_DROP イベントの場合、getX()getY() は、ドロップを受け取った View の座標系を使用して、ドロップが発生した時点のタッチポイントの XY の位置を返します。

ユーザーは、ドラッグ イベント リスナーがドラッグ イベントを受信していない View、アプリの UI の空の領域、またはアプリ外の領域では、ドラッグ シャドウを解放できますが、Android はアクション タイプ ACTION_DROP のイベントを送信せず、ACTION_DRAG_ENDED イベントのみを送信します。

ドラッグ終了に対応する

ユーザーがドラッグ シャドウを解放するとすぐに、アプリ内のすべてのドラッグ イベント リスナーに、アクション タイプが ACTION_DRAG_ENDED のドラッグ イベントが送信されます。これは、ドラッグ オペレーションが完了したことを示します。

各ドラッグ イベント リスナーは、次のことを行う必要があります。

  1. オペレーション中にリスナーの外観が変更された場合は、オペレーションが完了したことをユーザーに視覚的に知らせるため、リスナーの外観をリセットする必要があります。
  2. 必要であれば、getResult() を呼び出すことで、ドラッグ&ドロップ オペレーションについてのより詳しい情報を取得できます。アクション タイプ ACTION_DROP のイベントに対してリスナーが true を返す場合、getResult() はブール値 true を返します。それ以外のケースでは、システムが ACTION_DROP イベントを送信しない場合を含め、getResult() はブール値 false を返します。
  3. ドロップ オペレーションが正常に完了したことを示すには、リスナーからシステムにブール値 true を返す必要があります。false を返さないことで、ドロップ シャドウがソースに戻ることを示すビジュアル キューによって、操作が失敗したことをユーザーに示唆できます。

ドラッグ イベントへの対応例

ドラッグ イベントはすべて、ドラッグ イベント メソッドかリスナーが受信します。次のコード スニペットは、ドラッグ イベントに応答する例です。

Kotlin

val imageView = ImageView(this)

// Set the drag event listener for the View.
imageView.setOnDragListener { v, e ->

    // Handle each of the expected events.
    when (e.action) {
        DragEvent.ACTION_DRAG_STARTED -> {
            // Determine whether this View can accept the dragged data.
            if (e.clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {
                // As an example, apply a blue color tint to the View to
                // indicate that it can accept data.
                (v as? ImageView)?.setColorFilter(Color.BLUE)

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

                // Return true to indicate that the View can accept the dragged
                // data.
                true
            } else {
                // Return false to indicate that, during the current drag and
                // drop operation, this View doesn't receive events again until
                // ACTION_DRAG_ENDED is sent.
                false
            }
        }
        DragEvent.ACTION_DRAG_ENTERED -> {
            // Apply a green tint to the View.
            (v as? ImageView)?.setColorFilter(Color.GREEN)

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

            // Return true. The value is ignored.
            true
        }

        DragEvent.ACTION_DRAG_LOCATION ->
            // Ignore the event.
            true
        DragEvent.ACTION_DRAG_EXITED -> {
            // Reset the color tint to blue.
            (v as? ImageView)?.setColorFilter(Color.BLUE)

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

            // Return true. The value is ignored.
            true
        }
        DragEvent.ACTION_DROP -> {
            // Get the item containing the dragged data.
            val item: ClipData.Item = e.clipData.getItemAt(0)

            // Get the text data from the item.
            val dragData = item.text

            // Display a message containing the dragged data.
            Toast.makeText(this, "Dragged data is $dragData", Toast.LENGTH_LONG).show()

            // Turn off color tints.
            (v as? ImageView)?.clearColorFilter()

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

            // Return true. DragEvent.getResult() returns true.
            true
        }

        DragEvent.ACTION_DRAG_ENDED -> {
            // Turn off color tinting.
            (v as? ImageView)?.clearColorFilter()

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

            // Do a getResult() and display what happens.
            when(e.result) {
                true ->
                    Toast.makeText(this, "The drop was handled.", Toast.LENGTH_LONG)
                else ->
                    Toast.makeText(this, "The drop didn't work.", Toast.LENGTH_LONG)
            }.show()

            // Return true. The value is ignored.
            true
        }
        else -> {
            // An unknown action type is received.
            Log.e("DragDrop Example", "Unknown action type received by View.OnDragListener.")
            false
        }
    }
}

Java

View imageView = new ImageView(this);

// Set the drag event listener for the View.
imageView.setOnDragListener( (v, e) -> {

    // Handle each of the expected events.
    switch(e.getAction()) {

        case DragEvent.ACTION_DRAG_STARTED:

            // Determine whether this View can accept the dragged data.
            if (e.getClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {

                // As an example, apply a blue color tint to the View to
                // indicate that it can accept data.
                ((ImageView)v).setColorFilter(Color.BLUE);

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

                // Return true to indicate that the View can accept the dragged
                // data.
                return true;

            }

            // Return false to indicate that, during the current drag-and-drop
            // operation, this View doesn't receive events again until
            // ACTION_DRAG_ENDED is sent.
            return false;

        case DragEvent.ACTION_DRAG_ENTERED:

            // Apply a green tint to the View.
            ((ImageView)v).setColorFilter(Color.GREEN);

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

            // Return true. The value is ignored.
            return true;

        case DragEvent.ACTION_DRAG_LOCATION:

            // Ignore the event.
            return true;

        case DragEvent.ACTION_DRAG_EXITED:

            // Reset the color tint to blue.
            ((ImageView)v).setColorFilter(Color.BLUE);

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

            // Return true. The value is ignored.
            return true;

        case DragEvent.ACTION_DROP:

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

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

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

            // Turn off color tints.
            ((ImageView)v).clearColorFilter();

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

            // Return true. DragEvent.getResult() returns true.
            return true;

        case DragEvent.ACTION_DRAG_ENDED:

            // Turn off color tinting.
            ((ImageView)v).clearColorFilter();

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

            // Do a getResult() and displays what happens.
            if (e.getResult()) {
                Toast.makeText(this, "The drop was handled.", Toast.LENGTH_LONG).show();
            } else {
                Toast.makeText(this, "The drop didn't work.", Toast.LENGTH_LONG).show();
            }

            // Return true. The value is ignored.
            return true;

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

    return false;

});

ドラッグ シャドウをカスタマイズする

カスタマイズされた myDragShadowBuilder を定義するには、View.DragShadowBuilder のメソッドをオーバーライドします。次のコード スニペットでは、TextView 用に小さい長方形のグレーのドラッグ シャドウを作成します。

Kotlin

private class MyDragShadowBuilder(view: View) : View.DragShadowBuilder(view) {

    private val shadow = ColorDrawable(Color.LTGRAY)

    // Define a callback that sends the drag shadow dimensions and touch point
    // back to the system.
    override fun onProvideShadowMetrics(size: Point, touch: Point) {

            // Set the width of the shadow to half the width of the original
            // View.
            val width: Int = view.width / 2

            // Set the height of the shadow to half the height of the original
            // View.
            val height: Int = view.height / 2

            // The drag shadow is a ColorDrawable. Set its dimensions to
            // be the same as the Canvas that the system provides. As a result,
            // the drag shadow fills the Canvas.
            shadow.setBounds(0, 0, width, height)

            // Set the size parameter's width and height values. These get back
            // to the system through the size parameter.
            size.set(width, height)

            // Set the touch point's position to be in the middle of the drag
            // shadow.
            touch.set(width / 2, height / 2)
    }

    // Define a callback that draws the drag shadow in a Canvas that the system
    // constructs from the dimensions passed to onProvideShadowMetrics().
    override fun onDrawShadow(canvas: Canvas) {

            // Draw the ColorDrawable on the Canvas passed in from the system.
            shadow.draw(canvas)
    }
}

Java

private static class MyDragShadowBuilder extends View.DragShadowBuilder {

    // The drag shadow image, defined as a drawable object.
    private static Drawable shadow;

    // Constructor.
    public MyDragShadowBuilder(View view) {

            // Store the View parameter.
            super(view);

            // Create a draggable image that fills the Canvas provided by the
            // system.
            shadow = new ColorDrawable(Color.LTGRAY);
    }

    // Define a callback that sends the drag shadow dimensions and touch point
    // back to the system.
    @Override
    public void onProvideShadowMetrics (Point size, Point touch) {

            // Define local variables.
            int width, height;

            // Set the width of the shadow to half the width of the original
            // View.
            width = getView().getWidth() / 2;

            // Set the height of the shadow to half the height of the original
            // View.
            height = getView().getHeight() / 2;

            // The drag shadow is a ColorDrawable. Set its dimensions to
            // be the same as the Canvas that the system provides. As a result,
            // the drag shadow fills the Canvas.
            shadow.setBounds(0, 0, width, height);

            // Set the size parameter's width and height values. These get back
            // to the system through the size parameter.
            size.set(width, height);

            // Set the touch point's position to be in the middle of the drag
            // shadow.
            touch.set(width / 2, height / 2);
    }

    // Define a callback that draws the drag shadow in a Canvas that the system
    // constructs from the dimensions passed to onProvideShadowMetrics().
    @Override
    public void onDrawShadow(Canvas canvas) {

            // Draw the ColorDrawable on the Canvas passed in from the system.
            shadow.draw(canvas);
    }
}