ドラッグ&ドロップを有効にする

Android のドラッグ&ドロップ フレームワークを使用すると、インタラクティブなドラッグ&ドロップ機能をアプリに追加できます。ドラッグ&ドロップを使用すると、ユーザーは URI で表されるテキスト、画像、オブジェクトなどのコンテンツをコピーまたは移動できます。また、アプリ内の 1 つの View から別の View へ、またはマルチ ウィンドウ モードのアプリ間でコピーまたは移動できます。

アプリ内でドラッグ&ドロップされているテキスト文字列と画像 分割画面モードのアプリ間でドラッグ&ドロップされているテキスト文字列と画像
図 1. アプリ内でのドラッグ&ドロップ
図 2. アプリ間でのドラッグ&ドロップ

このフレームワークには、ドラッグ イベント クラス、ドラッグ リスナー、ヘルパークラスとメソッドが含まれています。このフレームワークは主にデータ転送を可能にすることを目的に設計されていますが、他の UI アクションにも使用できます。たとえば、ユーザーがカラーアイコンを別のアイコンの上にドラッグしたときに色を混ぜるアプリを作成できます。ただし、このドキュメントの残りの部分では、データ転送のコンテキストにおけるドラッグ&ドロップ フレームワークについて説明します。

概要

ドラッグ&ドロップ オペレーションは、ユーザーがデータのドラッグ開始のシグナルとして認識される UI ジェスチャーをユーザーが行うと開始されます。これに応じて、アプリはドラッグ&ドロップ オペレーションが開始されたことをシステムに通知します。システムはアプリにコールバックし、ドラッグされているデータの表現(ドラッグ シャドウと呼ばれます)を取得します。

ユーザーがアプリのレイアウト上にドラッグ シャドウを移動すると、レイアウト内の View オブジェクトに関連付けられたドラッグ イベント リスナーとコールバック メソッドにドラッグ イベントが送信されます。データを受け入れられるビュー(ドロップ ターゲット)上でユーザーがドラッグ シャドウを解放すると、システムはそのデータをターゲットに送信します。ドラッグ シャドウがドロップ ターゲット上にあるかどうかにかかわらず、ユーザーがドラッグ シャドウを離すと、ドラッグ&ドロップ オペレーションは終了します。

View.OnDragListener を実装して、ドラッグ イベント リスナーを作成します。View オブジェクトの setOnDragListener() メソッドを使用して、ドロップ ターゲットのリスナーを設定します。レイアウト内の各ビューには onDragEvent() コールバック メソッドも用意されています。

アプリは、startDragAndDrop() メソッドを呼び出して、ドラッグ&ドロップ オペレーションの開始をシステムに通知します。このメソッドは、ドラッグ イベントを送信するようシステムに指示します。このメソッドは、ユーザーがドラッグしているデータとデータを記述するメタデータをシステムに提供します。startDragAndDrop() は、現在のレイアウト内の任意の View で呼び出すことができます。システムは、レイアウトのグローバル設定にアクセスするためにのみ View オブジェクトを使用します。

ドラッグ&ドロップ オペレーション中は、レイアウト内の View オブジェクトのドラッグ イベント リスナーやコールバック メソッドに、システムからドラッグ イベントが送信されます。リスナーまたはコールバック メソッドは、メタデータを使用して、ドロップされたデータを受け入れるかどうかを決定します。ユーザーがドロップ ターゲット(データを受け入れる View)にデータをドロップすると、そのデータを含むドラッグ イベント オブジェクトが、ドロップ ターゲットのドラッグ イベント リスナーまたはコールバック メソッドに送信されます。

ドラッグ イベント リスナーとコールバック メソッド

View は、View.OnDragListener を実装するドラッグ イベント リスナー、またはビューの onDragEvent() コールバック メソッドにより、ドラッグ イベントを受信します。システムは、メソッドまたはリスナーを呼び出すときに DragEvent 引数を指定します。

ほとんどの場合、コールバック メソッドよりもリスナーを使用することをおすすめします。UI を設計する場合、通常は View クラスをサブクラス化しませんが、コールバック メソッドを使う場合は、メソッドをオーバーライドするためにサブクラスを作成する必要があります。これに対してリスナークラスは、1 つ実装すれば、そのリスナークラスを異なる複数の View オブジェクトで使用できます。匿名のインライン クラスまたはラムダ式として実装することもできます。View オブジェクトのリスナーを設定するには、setOnDragListener() を呼び出します。

また、メソッドをオーバーライドせずに onDragEvent() のデフォルトの実装を変更することもできます。ビューに OnReceiveContentListener を設定します。詳細については、setOnReceiveContentListener() をご覧ください。すると、onDragEvent() メソッドはデフォルトで次の処理を行います。

  • startDragAndDrop() の呼び出しに応じて true を返します。
  • ドラッグ&ドロップ データがビューにドロップされると、performReceiveContent() が呼び出されます。データは ContentInfo オブジェクトとしてメソッドに渡されます。このメソッドは OnReceiveContentListener を呼び出します。

  • ドラッグ&ドロップ データがビューにドロップされ、OnReceiveContentListener がコンテンツのいずれかを消費した場合に true を返します。

アプリのデータを処理するように OnReceiveContentListener を定義します。API レベル 24 までの下位互換性を確保するには、Jetpack バージョンの OnReceiveContentListener を使用してください。

View オブジェクトには、ドラッグ イベント リスナーとコールバック メソッドを設定できます。この場合、システムは最初にリスナーを呼び出します。リスナーが false を返さない限り、システムはコールバック メソッドを呼び出しません。

onDragEvent() メソッドと View.OnDragListener の組み合わせは、タッチイベントで使用される onTouchEvent()View.OnTouchListener の組み合わせに似ています。

ドラッグ&ドロップのプロセス

ドラッグ&ドロップのプロセスには、開始、継続、ドロップ、終了の 4 つのステップまたは状態があります。

開始

アプリは、ユーザーのドラッグ操作に応じて、startDragAndDrop() を呼び出して、ドラッグ&ドロップ オペレーションを開始するようシステムに指示します。このメソッドの引数は、以下を提供します。

  • ドラッグするデータです。
  • ドラッグ シャドウを描画するためのコールバック
  • ドラッグされたデータを記述するメタデータ: システムは、ドラッグ シャドウを取得するためにアプリケーションへコールバックすることで応答します。デバイスにドラッグ シャドウが表示されます。: 次に、現在のレイアウト内にあるすべての View オブジェクトのドラッグ イベント リスナーに、アクション タイプ ACTION_DRAG_STARTED のドラッグ イベントが送信されます。ドロップ イベントなどのドラッグ イベントを引き続き受信するには、ドラッグ イベント リスナーが true を返す必要があります。これにより、リスナーがシステムに登録されます。登録されたリスナーのみが、引き続きドラッグ イベントを受信します。この時点で、リスナーはドロップ ターゲット View オブジェクトの外観を変更して、ビューがドロップ イベントを受け入れられることを示すこともできます。: ドラッグ イベント リスナーが false を返す場合、システムがアクション タイプ ACTION_DRAG_ENDED のドラッグ イベントを送信するまで、現在のオペレーションのドラッグ イベントを受信しません。false を返すことにより、リスナーはドラッグ&ドロップ オペレーションに関係がなく、ドラッグされたデータを受け入れないことをシステムに伝えます。
ドラッグ中
ユーザーがドラッグを続行しました。ドラッグ シャドウがドロップ ターゲットの境界ボックスと交差すると、1 つ以上のドラッグ イベントがターゲットのドラッグ イベント リスナーに送信されます。リスナーは、イベントに応答してドロップ ターゲット View の外観を変更する場合があります。たとえば、ドラッグ シャドウがドロップ ターゲットの境界ボックスに入ったことを示すイベント(アクション タイプ ACTION_DRAG_ENTERED)の場合、リスナーは View をハイライト表示して反応できます。
ドロップ
ユーザーがドロップ ターゲットの境界ボックス内でドラッグ シャドウを解放します。ドロップ ターゲットのリスナーには、アクション タイプ ACTION_DROP のドラッグ イベントが送信されます。ドラッグ イベント オブジェクトには、オペレーションを開始する startDragAndDrop() の呼び出しでシステムに渡すデータが含まれています。ドロップされたデータを正常に処理した場合、リスナーはシステムにブール値 true を返す必要があります。: このステップは、ユーザーがドラッグ シャドウを View の境界ボックス内にドロップした場合にのみ発生します。このシャドウのリスナーは、ドラッグ イベント(ドロップ ターゲット)を受信するよう登録されています。それ以外の状況でユーザーがドラッグ シャドウを解放しても、ACTION_DROP ドラッグ イベントは送信されません。
終了

ユーザーがドラッグ シャドウを離し、システムが

アクション タイプ ACTION_DROP のドラッグ イベントを送信すると、必要に応じて、ドラッグ&ドロップ オペレーションが終了したことを示すアクション タイプ ACTION_DRAG_ENDED のドラッグ イベントが送信されます。これは、ユーザーがドラッグ シャドウを解放した場所に関係なく行われます。このイベントは、ドラッグ イベントを受信するよう登録されているすべてのリスナー(ACTION_DROP イベントも受信する場合でも)に送信されます。

これらの各ステップについては、ドラッグ&ドロップ オペレーションで詳しく説明します。

ドラッグ イベント

システムは、ドラッグ イベントを DragEvent オブジェクトの形式で送信します。このオブジェクトには、ドラッグ&ドロップ プロセスで何が起こっているのかを説明するアクション タイプが含まれています。アクション タイプに応じて、オブジェクトに他のデータを含めることもできます。

ドラッグ イベント リスナーは DragEvent オブジェクトを受け取ります。リスナーでアクション タイプを取得するためには、DragEvent.getAction() を呼び出します。表 1 に示すように、DragEvent クラスの定数によって定義される有効な値は 6 つあります。

表 1. DragEvent のアクション タイプ

アクション タイプ 意味
ACTION_DRAG_STARTED アプリは startDragAndDrop() を呼び出し、ドラッグ シャドウを取得します。リスナーがこのオペレーションに対するドラッグ イベントを引き続き受信するには、システムにブール値 true を返す必要があります。
ACTION_DRAG_ENTERED ドラッグ シャドウが、ドラッグ イベント リスナーの View の境界ボックスに入ります。これは、ドラッグ シャドウが境界ボックスに入ったときに、リスナーが最初に受け取るイベント アクション タイプです。
ACTION_DRAG_LOCATION ACTION_DRAG_ENTERED イベントの後、ドラッグ シャドウは、ドラッグ イベント リスナーの View の境界ボックス内に引き続き表示されます。
ACTION_DRAG_EXITED ACTION_DRAG_ENTERED と 1 つ以上の ACTION_DRAG_LOCATION イベントの後、ドラッグ シャドウは、ドラッグ イベント リスナーの View の境界ボックスの外に移動します。
ACTION_DROP ドラッグ シャドウは、ドラッグ イベント リスナーの View 上で解放されます。このアクション タイプが View オブジェクトのリスナーに送信されるのは、ACTION_DRAG_STARTED ドラッグ イベントに対してリスナーがブール値 true を返した場合に限られます。ユーザーが、リスナーが登録されていない View 上でドラッグ シャドウを解放した場合、または現在のレイアウトに含まれないものの上にドラッグ シャドウを解放した場合、このアクション タイプは送信されません。

リスナーは、ドロップを正常に処理すると、ブール値 true を返します。それ以外の場合は、false を返す必要があります。

ACTION_DRAG_ENDED ドラッグ&ドロップ オペレーションが終了します。このアクション タイプの前に ACTION_DROP イベントがあるとは限りません。システムから ACTION_DROP が送信された場合、ACTION_DRAG_ENDED アクション タイプを受信しても、ドロップが成功したことにはなりません。ACTION_DROP への応答として返される値を取得するには、表 2 に示すようにリスナーが getResult() を呼び出す必要があります。ACTION_DROP イベントが送信されなかった場合、getResult()false を返します。

DragEvent オブジェクトには、startDragAndDrop() の呼び出しでアプリがシステムに提供するデータとメタデータも含まれます。表 2 に示すように、一部のデータは特定のアクション タイプに対してのみ有効です。イベントと関連データについて詳しくは、ドラッグ&ドロップ オペレーションをご覧ください。

表 2. アクション タイプごとの有効な DragEvent データ

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

DragEvent メソッド getAction()describeContents()writeToParcel()toString() は常に有効なデータを返します。

特定のアクション タイプについて有効なデータがメソッドに含まれない場合、結果のタイプに応じて null または 0 を返します。

ドラッグ シャドウ

ドラッグ&ドロップ オペレーションの間、ユーザーのドラッグ対象となる画像がシステムにより表示されます。データを移動する場合は、この画像はドラッグ中のデータを表します。その他の操作の場合は、そのドラッグ オペレーションをなんらかのかたちで表す画像になります。

この画像はドラッグ シャドウと呼ばれます。これは、View.DragShadowBuilder オブジェクトに対して宣言したメソッドを使用して作成します。startDragAndDrop() を使用してドラッグ&ドロップ オペレーションを開始するときに、ビルダーをシステムに渡します。システムは、startDragAndDrop() に対するレスポンスの一環として、View.DragShadowBuilder で定義したコールバック メソッドを呼び出して、ドラッグ シャドウを取得します。

View.DragShadowBuilder クラスには、以下の 2 つのコンストラクタがあります。

View.DragShadowBuilder(View)

このコンストラクタでは、アプリのあらゆる View オブジェクトを使用できます。このコンストラクタにより View オブジェクトが View.DragShadowBuilder オブジェクトに格納されるため、コールバックがアクセスしてドラッグ シャドウを作成できます。ビューは、ドラッグ オペレーションの開始時にユーザーが選択する View である必要はありません。

このコンストラクタを使用すれば、View.DragShadowBuilder を拡張したり、そのメソッドをオーバーライドしたりする必要がなくなります。デフォルトでは、引数として渡した View と同じ外観のドラッグ シャドウが、ユーザーが画面に触れた位置を中心として得られます。

View.DragShadowBuilder()

このコンストラクタを使用する場合、View.DragShadowBuilder オブジェクトでは View オブジェクトを使用できません。このフィールドは null に設定されています。View.DragShadowBuilder を拡張してそのメソッドをオーバーライドする必要があります。そうしないと、ドラッグ シャドウは非表示になります。システムはエラーをスローしません。

View.DragShadowBuilder クラスには、ドラッグ シャドウを一緒に作成する 2 つのメソッドがあります。

onProvideShadowMetrics()

startDragAndDrop() を呼び出すとすぐに、システムによってこのメソッドが呼び出されます。このメソッドを使用して、ドラッグ シャドウのサイズやタッチポイントをシステムに送信します。このメソッドには、次の 2 つのパラメータがあります。

outShadowSize: Point オブジェクト。ドラッグ シャドウの幅は x に、高さは y に指定します。

outShadowTouchPoint: Point オブジェクト。タッチポイントとは、ドラッグ中にユーザーの指の下に位置するドラッグ シャドウ内の位置です。X の位置は xY の位置は y です。

onDrawShadow()

onProvideShadowMetrics() 呼び出しの直後に、ドラッグ シャドウを作成する onDrawShadow() が呼び出されます。このメソッドの引数は 1 つだけで、onProvideShadowMetrics() で指定されたパラメータからシステムが作成する Canvas オブジェクトです。このメソッドは、指定された Canvas にドラッグ シャドウを描画します。

パフォーマンスを向上させるには、ドラッグ シャドウのサイズを小さくします。単一のアイテムにはアイコンを使用します。複数のアイテムを選択する場合は、画面全体に広がるフルイメージではなく、積み重ねたアイコンを使用します。

ドラッグ&ドロップ オペレーション

このセクションでは、ドラッグの開始、ドラッグ中のイベントへの応答、ドロップ イベントへの応答、ドラッグ&ドロップ オペレーションの終了の各方法を順を追って説明します。

ドラッグを開始する

ユーザーが View オブジェクト上のドラッグ操作(通常は長押し)でドラッグを開始します。これに応じて、アプリは次のことを行う必要があります。

  1. 移動するデータの 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.
            val myShadow = MyDragShadowBuilder(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.
    View.DragShadowBuilder myShadow = new MyDragShadowBuilder(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;
    });
    
  2. View.DragShadowBuilder のメソッドをオーバーライドして、myDragShadowBuilder を定義します。次のコード スニペットでは、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);
    }
    }
    

ドラッグ開始に対応する

ドラッグ オペレーションの間は、現在のレイアウトにある 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 の座標系を使用して、ドロップした時点のタッチポイントの X 位置と Y 位置を返します。

ユーザーは、ドラッグ イベント リスナーがドラッグ イベントを受信していない View 上で、ドラッグ シャドウを解放できます。また、ユーザーは、アプリの UI の空の領域またはアプリ外の領域でドラッグ シャドウを解放できます。上記のいずれの場合も、システムからアクション タイプ ACTION_DROP のイベントは送信されませんが、ACTION_DRAG_ENDED イベントは送信されます。

ドラッグ終了に対応する

ユーザーがドラッグ シャドウを解放した直後に、アプリ内のすべてのドラッグ イベント リスナーに、アクション タイプが ACTION_DRAG_ENDED のドラッグ イベントが送信されます。これは、ドラッグ&ドロップ操作が終了したことを示します。

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

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

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

ドラッグ イベントはすべて、ドラッグ イベント メソッドかリスナーが受信します。以下のコード スニペットは、ドラッグ イベントへの対応の簡単な例です。

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;

});

マルチウィンドウ モードでのドラッグ&ドロップ

Android 7.0(API レベル 24)以降を搭載したデバイスは、マルチウィンドウ モードをサポートしています。これにより、ユーザーはドラッグ&ドロップ操作でアプリから別のアプリにデータを移動できます。詳しくは、マルチウィンドウのサポートをご覧ください。

ドラッグ&ドロップ オペレーションが開始されるソースアプリがデータを提供します。ドラッグ&ドロップ オペレーションが終了したターゲット アプリは、データを受信します。

ドラッグ&ドロップ オペレーションの開始時に、ソースアプリで DRAG_FLAG_GLOBAL フラグを設定することにより、ユーザーが別のアプリにデータをドラッグできることを示す必要があります。

データはアプリの境界を越えて移動するため、アプリはコンテンツ URI を使用してデータへのアクセスを共有します。これには、以下が必要です。

  • ソースアプリは、ターゲット アプリに付与するデータに対する読み取りまたは書き込みのアクセス権に応じて、DRAG_FLAG_GLOBAL_URI_READ フラグと DRAG_FLAG_GLOBAL_URI_WRITE フラグのいずれか、または両方を設定する必要があります。
  • ターゲット アプリは、ユーザーがアプリにドラッグ&ドロップしたデータを処理する直前に requestDragAndDropPermissions() を呼び出す必要があります。ターゲット アプリがドラッグ&ドロップ データにアクセスする必要がなくなったら、アプリは requestDragAndDropPermissions() から返されたオブジェクトに対して release() を呼び出すことができます。そうしないと、権限を含むアクティビティが破棄されたときに、権限が解放されます。 実装において、ドロップされた URI を処理する新しいアクティビティを開始する場合は、新しいアクティビティに同じ権限を付与する必要があります。クリップデータとフラグを設定する必要があります。

    Kotlin

    intent.setClipData(clipData)
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
    

    Java

    intent.setClipData(clipData);
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    

次のコード スニペットは、ドラッグ&ドロップ オペレーションが発生した直後に、ドラッグ&ドロップ データに対する読み取り専用アクセス権を解放する方法を示しています。詳細な例については、GitHub の DragAndDrop サンプルをご覧ください。

ソースのドラッグ&ドロップ アクティビティ

Kotlin

// Drag a file stored in an images/ directory in internal storage.
val internalImagesDir = File(context.filesDir, "images")
val imageFile = File(internalImagesDir, imageFilename)
val uri = FileProvider.getUriForFile(context, contentAuthority, imageFile)

val listener = OnDragStartListener@{ view: View, _: DragStartHelper ->
    val clipData = ClipData(ClipDescription("Image Description",
                                            arrayOf("image/*")),
                            ClipData.Item(uri))
    // Must include DRAG_FLAG_GLOBAL to permit dragging data between apps.
    // This example provides read-only access to the data.
    val flags = View.DRAG_FLAG_GLOBAL or View.DRAG_FLAG_GLOBAL_URI_READ
    return@OnDragStartListener view.startDragAndDrop(clipData,
                                                     View.DragShadowBuilder(view),
                                                     null,
                                                     flags)
}

// Container where the image originally appears in the source app.
val srcImageView = findViewById<ImageView>(R.id.imageView)

// Detect and start the drag event.
DragStartHelper(srcImageView, listener).apply {
    attach()
}

Java

// Drag a file stored in an images/ directory in internal storage.
File internalImagesDir = new File(context.getFilesDir(), "images");
File imageFile = new File(internalImagesDir, imageFilename);
final Uri uri = FileProvider.getUriForFile(context, contentAuthority, imageFile);

// Container where the image originally appears in the source app.
ImageView srcImageView = findViewById(R.id.imageView);

// Enable the view to detect and start the drag event.
new DragStartHelper(srcImageView, (view, helper) -> {
    ClipData clipData = new ClipData(new ClipDescription("Image Description",
                                                          new String[] {"image/*"}),
                                     new ClipData.Item(uri));
    // Must include DRAG_FLAG_GLOBAL to permit dragging data between apps.
    // This example provides read-only access to the data.
    int flags = View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ;
    return view.startDragAndDrop(clipData,
                                 new View.DragShadowBuilder(view),
                                 null,
                                 flags);
}).attach();

ターゲットのドラッグ&ドロップ アクティビティ

Kotlin

// Container where the image is to be dropped in the target app.
val targetImageView = findViewById<ImageView>(R.id.imageView)

targetImageView.setOnDragListener { view, event ->

    when (event.action) {

        ACTION_DROP -> {
            val imageItem: ClipData.Item = event.clipData.getItemAt(0)
            val uri = imageItem.uri

            // Request permission to access the image data being dragged into
            // the target activity's ImageView element.
            val dropPermissions = requestDragAndDropPermissions(event)
            (view as ImageView).setImageURI(uri)

            // Release the permission immediately afterward because it's no
            // longer needed.
            dropPermissions.release()
            return@setOnDragListener true
        }

        // Implement logic for other DragEvent cases here.

        // An unknown action type is received.
        else -> {
            Log.e("DragDrop Example", "Unknown action type received by View.OnDragListener.")
            return@setOnDragListener false
        }

    }
}

Java

// Container where the image is to be dropped in the target app.
ImageView targetImageView = findViewById(R.id.imageView);

targetImageView.setOnDragListener( (view, event) -> {

    switch (event.getAction()) {

        case ACTION_DROP:
            ClipData.Item imageItem = event.getClipData().getItemAt(0);
            Uri uri = imageItem.getUri();

            // Request permission to access the image data being dragged into
            // the target activity's ImageView element.
            DragAndDropPermissions dropPermissions =
                requestDragAndDropPermissions(event);

            ((ImageView)view).setImageURI(uri);

            // Release the permission immediately afterward because it's no
            // longer needed.
            dropPermissions.release();

            return true;

        // Implement logic for other DragEvent cases here.

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

    return false;
});

ドラッグ&ドロップを簡単にする DropHelper

DropHelper クラスは、ドラッグ&ドロップ機能の実装を簡素化します。Jetpack の DragAndDrop ライブラリのメンバーである DropHelper は、API レベル 24 までの下位互換性を提供しています。

DropHelper によって、ドロップ ターゲットの指定、ドロップ ターゲットのハイライト表示のカスタマイズ、ドロップされたデータの処理方法の定義が可能です。

ドロップ ターゲットを指定する

DropHelper.configureView() は、ドロップ ターゲットを指定できる静的なオーバーロード メソッドです。パラメータには次のものがあります。

たとえば、画像を受け入れるドロップ ターゲットを作成するには、次のいずれかのメソッド呼び出しを使用します。

Kotlin

configureView(
    myActivity,
    targetView,
    arrayOf("image/*"),
    options,
    onReceiveContentListener)

// or

configureView(
    myActivity,
    targetView,
    arrayOf("image/*"),
    onReceiveContentListener)

Java

DropHelper.configureView(
    myActivity,
    targetView,
    new String[] {"image/*"},
    options,
    onReceiveContentlistener);

// or

DropHelper.configureView(
    myActivity,
    targetView,
    new String[] {"image/*"},
    onReceiveContentlistener);

2 番目の呼び出しでは、ドロップ ターゲットの構成オプションは省略されています。この場合、ドロップ ターゲットのハイライト カラーはテーマのセカンダリ(またはアクセント)カラーに設定され、ハイライトの角の丸みは 16 dp に設定され、EditText コンポーネントのリストは空になります。詳しくは、次のセクションをご覧ください。

ドロップ ターゲットを構成する

DropHelper.Options 内部クラスを使用すると、ドロップ ターゲットを設定できます。クラスのインスタンスを DropHelper.configureView(Activity, View, String[], Options, OnReceiveContentListener) メソッドに渡します。詳しくは、前のセクションをご覧ください。

ドロップ ターゲットのハイライト表示をカスタマイズする

DropHelper は、ユーザーがターゲット上にコンテンツをドラッグしたときにハイライト表示されるように、ドロップ ターゲットを設定します。DropHelper はデフォルトのスタイルを提供し、DropHelper.Options はハイライトの色を設定し、ハイライトの長方形の角の半径を指定できます。

次の例に示すように、DropHelper.Options.Builder クラスを使用して DropHelper.Options インスタンスを作成し、構成オプションを設定します。

Kotlin

val options: DropHelper.Options = DropHelper.Options.Builder()
                                      .setHighlightColor(getColor(R.color.purple_300))
                                      .setHighlightCornerRadiusPx(resources.getDimensionPixelSize(R.dimen.drop_target_corner_radius))
                                      .build()

Java

DropHelper.Options options = new DropHelper.Options.Builder()
                                     .setHighlightColor(getColor(R.color.purple_300))
                                     .setHighlightCornerRadiusPx(getResources().getDimensionPixelSize(R.dimen.drop_target_corner_radius))
                                     .build();

ドロップ ターゲットで EditText コンポーネントを処理する

また、DropHelper は、ドロップ ターゲットに編集可能なテキスト フィールドが含まれている場合に、ドロップ ターゲット内のフォーカスを制御します。

ドロップ ターゲットは単一のビューまたはビュー階層のいずれかです。ドロップ ターゲットのビュー階層に 1 つ以上の EditText コンポーネントが含まれている場合は、コンポーネントのリストを DropHelper.Options.Builder.addInnerEditTexts(EditText...) に指定して、ドロップ ターゲットのハイライト表示とテキストデータ処理が正しく機能するようにします。

DropHelper は、ドロップ ターゲットのビュー階層内の EditText コンポーネントが、ドラッグ操作中に、それを含むビューからフォーカスを盗むのを防ぎます。

また、ドラッグ&ドロップの ClipData にテキストと URI データが含まれている場合、DropHelper はドロップ ターゲット内の EditText コンポーネントのいずれかを選択して、テキストデータを処理します。選択は、次の優先順位に基づいています。

  1. ClipData がドロップされる EditText
  2. テキスト カーソル(キャレット)を含む EditText
  3. DropHelper.Options.Builder.addInnerEditTexts(EditText...) の呼び出しに指定された最初の EditText

EditText をデフォルトのテキストデータ ハンドラとして設定するには、DropHelper.Options.Builder.addInnerEditTexts(EditText...) の呼び出しの最初の引数として EditText を渡します。たとえば、ドロップ ターゲットで画像を処理できるものの、編集可能なテキスト フィールド T1T2T3 が含まれている場合は、次のようにして T2 をデフォルトにします。

Kotlin

val options: DropHelper.Options = DropHelper.Options.Builder()
                                      .addInnerEditTexts(T2, T1, T3)
                                      .build()

Java

DropHelper.Options options = new DropHelper.Options.Builder()
                                     .addInnerEditTexts(T2, T1, T3)
                                     .build();

ドロップ ターゲットでデータを処理する

DropHelper.configureView() メソッドは、ドラッグ&ドロップ ClipData を処理するために作成した OnReceiveContentListener を受け入れます。ドラッグ&ドロップ データは、ContentInfoCompat オブジェクトでリスナーに提供されます。オブジェクト内にテキストデータが存在します。画像などのメディアは URI で表されます。

OnReceiveContentListener は、DropHelper.configureView() を使用して次のタイプのビューを構成した場合、ドラッグ&ドロップ以外のユーザー操作(コピーして貼り付けなど)によってドロップ ターゲットに提供されるデータも処理します。

  • すべてのビュー(ユーザーが Android 12 以降を実行している場合)。
  • AppCompatEditText: ユーザーが Android 7.0 より前のバージョンの Android を実行している場合。

MIME タイプ、権限、コンテンツの検証

DropHelper による MIME タイプの確認は、ドラッグ&ドロップ データを提供するアプリが作成するドラッグ&ドロップ ClipDescription に基づいています。ClipDescription を検証して、MIME タイプが正しく設定されていることを確認します。

DropHelper は、ドラッグ&ドロップ ClipData に含まれるコンテンツ URI に対するすべてのアクセス権限をリクエストします。詳細については、DragAndDropPermissions をご覧ください。この権限により、ドラッグ&ドロップ データの処理時にコンテンツ URI を解決できます。

DropHelper は、ドロップされたデータの URI を解決するときに、コンテンツ プロバイダから返されたデータを検証しません。null をチェックし、解決されたデータの正確性を検証します。