ドラッグ&ドロップ

Android のドラッグ&ドロップ フレームワークを使用すると、インタラクティブなドラッグ&ドロップ機能をアプリに追加できます。ドラッグ&ドロップを使用すると、ユーザーは、テキスト、画像、オブジェクトなど、URI で表現できるすべてのコンテンツを、アプリ内の 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 イベントを受け取ったリスナーも含め、ドラッグ イベントを受信するよう登録されているリスナーすべてに送信されます。

上記 4 ステップそれぞれの詳細については、ドラッグ&ドロップ オペレーションをご覧ください。

ドラッグ イベント

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

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

表 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 への応答で返された値を取得するには、リスナーは getResult()表 2 を参照)を呼び出す必要があります。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   ✓       ✓

メソッド DragEventgetAction()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 座標を x、Y 座標を y に指定します。
onDrawShadow()

onProvideShadowMetrics() 呼び出しの直後に、ドラッグ シャドウを作成する onDrawShadow() が呼び出されます。このメソッドには、Canvas オブジェクトという単一の引数があります。このオブジェクトは、onProvideShadowMetrics() で渡したパラメータに基づいてシステムにより作成されます。このメソッドを使って、用意された 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(this).apply {
        // Sets the bitmap for the ImageView from an icon bit map (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(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 was 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(this);
    
    // Set the bitmap for the ImageView from an icon bit map (defined elsewhere).
    imageView.setImageBitmap(iconBitmap);
    
    // Set 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( 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 was handled.
        return true;
    });
    
  2. 次のコード スニペットでは、View.DragShadowBuilder のメソッドをオーバーライドすることで myDragShadowBuilder を定義しています。このコードは、TextView 用にグレーで小さな長方形のドラッグ シャドウを作成します。

    Kotlin

    private class MyDragShadowBuilder(v: View) : View.DragShadowBuilder(v) {
    
        private val shadow = ColorDrawable(Color.LTGRAY)
    
        // Defines 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. This sets 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)
        }
    
        // Defines 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 v) {
    
            // Stores the View parameter.
            super(v);
    
            // Creates a draggable image that fills 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
            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. This sets 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);
        }
    
        // Defines 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_ENTEREDACTION_DRAG_LOCATION への対応で適用した外観の変更をすべてリセットします。こうすることで、その View が現時点でのドロップ先ではなくなったことをユーザーに示せます。

ドロップに対応する

ユーザーが View 上でドラッグ シャドウを解放したときに、事前に View がドラッグ対象コンテンツを受け入れ可能であることを報告していれば、システムから View に対してアクション タイプ ACTION_DROP のドラッグ イベントが送信されます。

ドラッグ イベント リスナーでは、以下の処理を行います。

  1. getClipData() を呼び出して、startDragAndDrop() の呼び出し時に提供された ClipData オブジェクトを取得し、データを処理します。

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

  2. ドロップが正常に処理された場合、それを示すためにブール値 true を返します。そうでない場合には、ブール値 false を返します。ここで返した値が、最終的な ACTION_DRAG_ENDED イベントの getResult() から返される値になります。

    システムが ACTION_DROP イベントを送信しない場合、ACTION_DRAG_ENDED イベントに対して getResult() が返す値は false になります。

ACTION_DROP イベントでは getX()getY() によって、ドロップ時点でのタッチポイントの X 座標と Y 座標が、ドロップを受信した View の座標系を使って返されます。

ユーザーは、ドラッグ イベントを受信しないドラッグ イベント リスナーの 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 ->

    // Handles each of the expected events.
    when (e.action) {
        DragEvent.ACTION_DRAG_STARTED -> {
            // Determines if this View can accept the dragged data.
            if (e.clipDescription.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 as? ImageView)?.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.
                true
            } else {
                // Returns false to indicate that, during the current drag and drop operation,
                // this View will not receive events again until ACTION_DRAG_ENDED is sent.
                false
            }
        }
        DragEvent.ACTION_DRAG_ENTERED -> {
            // Applies a green tint to the View.
            (v as? ImageView)?.setColorFilter(Color.GREEN)

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

            // Returns true; the value is ignored.
            true
        }

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

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

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

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

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

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

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

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

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

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

            // Does a getResult(), and displays what happened.
            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()

            // Returns true; the value is ignored.
            true
        }
        else -> {
            // An unknown action type was 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) -> {

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

        case DragEvent.ACTION_DRAG_STARTED:

            // Determines if this View can accept the dragged data.
            if (e.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.
                ((ImageView)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;

            }

            // Returns false to indicate that, during the current drag and drop operation,
            // this View will not receive events again until ACTION_DRAG_ENDED is sent.
            return false;

        case DragEvent.ACTION_DRAG_ENTERED:

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

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

            // Returns true; the value is ignored.
            return true;

        case DragEvent.ACTION_DRAG_LOCATION:

            // Ignore the event.
            return true;

        case DragEvent.ACTION_DRAG_EXITED:

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

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

            // Returns true; the value is ignored.
            return true;

        case DragEvent.ACTION_DROP:

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

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

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

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

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

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

        case DragEvent.ACTION_DRAG_ENDED:

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

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

            // Does a getResult(), and displays what happened.
            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();
            }

            // Returns true; the value is ignored.
            return true;

        // An unknown action type was 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() を呼び出します。呼び出さなかった場合は、権限を含むアクティビティが破棄されたときに、権限が解放されます。

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

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

Kotlin

// Drag a file stored in internal storage. The file is in an "images/" directory.
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 allow for 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 under 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 allow for 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 for 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 afterwards because it's
            // no longer needed.
            dropPermissions.release()
            return@setOnDragListener true
        }

        // Implement logic for other DragEvent cases here.

        // An unknown action type was 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 afterwards 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 まで)

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

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

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

DropHelper は、ドロップされたデータの URI を解決する際に、コンテンツ プロバイダから返されるデータを検証しません。null を確認して、解決されたデータの正確性を検証する必要があります。

参考情報