ドラッグ&ドロップ

Android のドラッグ&ドロップ フレームワークを利用して、ユーザーが直感的なドラッグ&ドロップ操作でデータの移動を行えるようにできます。こうした移動は、自分のアプリ内の View 間や、マルチウィンドウ モードが有効な場合は自分のアプリと他のアプリ間で行えます。このフレームワークには、ドラッグ イベント クラス、ドラッグ リスナー、ヘルパー メソッド、ヘルパークラスが含まれています。

主にデータの移動を目的としたフレームワークですが、他の UI 操作にも利用できます。たとえば、ある色のアイコンを別の色のアイコンにドラッグすると色が混ざり合うアプリなども作成できます。ただし、このトピックでは、データの移動に主眼を置いてこのフレームワークの説明を行います。

以下の関連リソースもご確認ください。

概要

ドラッグ&ドロップ オペレーションは、アプリでデータのドラッグが開始されるシグナルと見なされる操作をユーザーが行った時点で始まります。この操作を受けて、アプリはシステムに、ドラッグが開始されたことを伝えます。システムからは、アプリに対して、ドラッグ中のデータの画面表示を取得するためのコールバックがなされます。ユーザーが指でこの画面表示(ドラッグ シャドウ)を現在のレイアウト上で動かすと、そのレイアウトにある View オブジェクトに関連付けられたドラッグ イベント リスナー オブジェクトやドラッグ イベント コールバック メソッドに、システムからドラッグ イベントが送信されます。 ユーザーがドラッグ シャドウから指を放すと、システムはドラッグ オペレーションを終了します。

ドラッグ イベント リスナー オブジェクト(リスナー)は、View.OnDragListener を実装するクラスから作成します。View に対するドラッグ イベント リスナー オブジェクトの設定は、その View オブジェクトの setOnDragListener() メソッドで行います。 各 View オブジェクトに対しては、onDragEvent() コールバック メソッドも使用できます。両メソッドの詳細については、ドラッグ イベント リスナーとコールバック メソッドのセクションをご覧ください。

: わかりやすくするため、以下のセクションではドラッグ イベントを受信するルーティンを「ドラッグ イベント リスナー」と呼びますが、実際にはコールバック メソッドの場合もあります。

ドラッグの開始時には、移動するデータと、そのデータを説明するメタデータの両方を、システムの呼び出しに含めます。ドラッグ中は、レイアウト内の各 View のドラッグ イベント リスナーやコールバック メソッドに、システムからドラッグ イベントが送信されます。リスナーやコールバック メソッドでは、メタデータを使用して、データがドロップされたときにそれを受け入れるかどうかの判断を行います。 ユーザーが View オブジェクトにデータをドロップすると、その View オブジェクトのリスナーやコールバック メソッドからシステムに対してあらかじめそのドロップを受け入れることを伝えていた場合は、システムからそのリスナーまたはコールバック メソッドにドラッグ イベントでデータが送信されます。

アプリでは、startDrag() メソッドを呼び出すことにより、システムにドラッグの開始を伝えます。これは、システムに対するドラッグ イベント送信開始の指示になります。このメソッドでドラッグ対象データも送信します。

現在のレイアウトにアタッチされているどの View に対しても、startDrag() を呼び出すことができます。システムでは、この View オブジェクトは、レイアウト内のグローバル設定にアクセスするためだけに使用されます。

アプリで一度 startDrag() を呼び出したら、その後のプロセスでは、システムから現在のレイアウトの View オブジェクトに送信されるイベントが使用されます。

注: アプリがマルチウィンドウ モードで実行されている場合は、ユーザーは異なるアプリ間でデータをドラッグ&ドロップできます。詳細については、ドラッグ&ドロップのサポートをご覧ください。

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

ドラッグ&ドロップのプロセスは、基本的に次の 4 つのステップ(または状態)で構成されます。

開始
ユーザーによるドラッグ開始の操作を受けて、アプリは startDrag() を呼び出し、システムにドラッグ開始を伝えます。startDrag() の引数で、ドラッグ対象のデータ、そのデータのメタデータ、ドラッグ シャドウを描画するためのコールバックを渡します。

するとシステムにより、まずアプリへのコールバックがなされ、ドラッグ シャドウが取得されます。その後、デバイスにドラッグ シャドウが表示されます。

次に、システムから現在のレイアウト上にあるすべての View オブジェクトのドラッグ イベント リスナーに対して、アクション タイプ ACTION_DRAG_STARTED のドラッグ イベントが送信されます。ドラッグ イベント リスナーでは、今後発生する可能性のあるドロップ イベントなどのドラッグ イベントを受信し続けるには、true を返す必要があります。 これにより、そのリスナーがシステムに登録されます。登録されたリスナーのみが、引き続きドラッグ イベントを受信します。この時点で、リスナーは View オブジェクトの外観を変更し、ドロップ イベントの受け入れが可能であることを示すこともできます。

false を返したドラッグ イベント リスナーでは、システムからアクション タイプ ACTION_DRAG_ENDED のドラッグ イベントが送信されるまで、現在のオペレーションのドラッグ イベントは受信されません。false を送信することにより、そのリスナーは現在のドラッグ オペレーションに関係がなく、ドラッグ対象データを受け入れないことがシステムに伝えられます。

続行中
ユーザーがドラッグを続行している状態。ドラッグ シャドウが View オブジェクトの境界ボックスと交差すると、システムから 1 つ以上のドラッグ イベントが、その View オブジェクトのドラッグ イベント リスナーに送信されます(そのリスナーがイベントを受信するよう登録されている場合)。このイベントを受けて、リスナーは View オブジェクトの外観を変更することもできます。たとえば、ドラッグ シャドウが View の境界ボックスに入ったことを示すイベント(アクション タイプ ACTION_DRAG_ENTERED)であれば、リスナーで View をハイライト表示して反応するのもよいでしょう。
ドロップ
ユーザーが、データ受け入れ可能な View の境界ボックス内でドラッグ シャドウを解放するステップ。システムからその View オブジェクトのリスナーに対して、アクション タイプ ACTION_DROP のドラッグ イベントが送信されます。このドラッグ イベントには、そのドラッグ オペレーションを開始した startDrag() 呼び出しでシステムに渡されたデータが含まれています。ドロップ受け入れのコードが正常に処理された場合は、リスナーはシステムにブール値 true を返すことになっています。

なお、このステップが発生するのは、ユーザーがドラッグ シャドウを View の境界ボックス内にドロップし、その View のリスナーがドラッグ イベントを受信するよう登録されている場合に限られます。それ以外の状況でユーザーがドラッグ シャドウを解放しても、ドラッグ イベント ACTION_DROP は送信されません。

終了
ユーザーがドラッグ シャドウを解放し、(必要に応じて)システムからアクション タイプ ACTION_DROP のドラッグ イベントが送信された後、ドラッグ オペレーションが終了したことを示すアクション タイプ ACTION_DRAG_ENDED のドラッグ イベントがシステムから送信されます。この動作は、ユーザーがドラッグ シャドウをどこで解放したかにかかわらず行われます。このイベントは、ACTION_DROP イベントを受け取ったリスナーも含め、ドラッグ イベントを受信するよう登録されているリスナーすべてに送信されます。

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

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

View によるドラッグ イベントの受信は、View.OnDragListener を実装したドラッグ イベント リスナーか、その View の onDragEvent(DragEvent) コールバック メソッドのいずれかによってなされます。 このリスナーまたはメソッドには、システムからの呼び出し時に DragEvent オブジェクトが渡されます。

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

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

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

ドラッグ イベント

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

リスナーでアクション タイプを取得するためには、getAction() を呼び出します。取り得る値は 6 つあり、DragEvent クラスの定数で定義されています。表 1 にその値を示します。

DragEvent オブジェクトには、startDrag() 呼び出しでアプリからシステムに渡したデータも含まれています。 一部のデータは、特定のアクション タイプでのみ有効です。表 2 に、各アクション タイプで有効なデータをまとめます。イベントに対して有効なデータについて詳しくは、ドラッグ&ドロップ オペレーションの設計のセクションをご覧ください。

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

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

リスナーでドロップを正常に処理した場合は、ブール値 true を返すことになっています。それ以外の場合は false を返します。

ACTION_DRAG_ENDED View オブジェクトのドラッグ イベント リスナーは、このイベント アクション タイプの受信を、システムがドラッグ オペレーションを終了するときに行います。このアクション タイプの前に ACTION_DROP イベントが発生するとは限りません。システムから ACTION_DROP が送信されていた場合、ACTION_DRAG_ENDED アクション タイプを受信したからといって、ドロップ オペレーションが正常に処理されたことにはなりません。ACTION_DROP への応答で返された値を取得するには、リスナーは getResult() を呼び出す必要があります。ACTION_DROP イベントが送信されていなかった場合は、getResult() から false が返されます。

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

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

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

特定のアクション タイプに対してメソッドに有効なデータがない場合は、そのデータの種類に応じて null または 0 が返されます。

ドラッグ シャドウ

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

この画像を、ドラッグ シャドウと呼びます。ドラッグ シャドウは、View.DragShadowBuilder オブジェクトに対して宣言したメソッドで作成し、ドラッグの開始時に startDrag() でシステムに渡します。 システムでは、startDrag() に対する応答の一環として、View.DragShadowBuilder で定義したコールバック メソッドが呼び出され、ドラッグ シャドウが取得されます。

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

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

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

View.DragShadowBuilder()
このコンストラクタでは、View.DragShadowBuilder オブジェクト内で View オブジェクトを指定しません(フィールドは null に設定されます)。このコンストラクタを使用する場合、View.DragShadowBuilder の拡張や、そのメソッドのオーバーライドを行わない限り、ドラッグ シャドウは表示されません。 システムからエラーは発生しません。

View.DragShadowBuilder クラスには、以下の 2 つのメソッドがあります。

onProvideShadowMetrics()
startDrag() を呼び出すとすぐに、システムによってこのメソッドが呼び出されます。このメソッドを使用して、ドラッグ シャドウのサイズやタッチポイントをシステムに送信します。このメソッドには、以下の 2 つの引数があります。
dimensions
Point オブジェクト。ドラッグ シャドウの幅を x、高さを y に指定します。
touch_point
Point オブジェクト。タッチポイントとは、ドラッグ中にユーザーの指の下にくるドラッグ シャドウ内の位置です。その X 座標を x、Y 座標を y に指定します。
onDrawShadow()
onProvideShadowMetrics() 呼び出しの直後、システムによって onDrawShadow() が呼び出され、ドラッグ シャドウそのものが取得されます。このメソッドには、Canvas オブジェクトという単一の引数があります。このオブジェクトは、onProvideShadowMetrics() で渡したパラメータに基づいてシステムにより作成されます。このメソッドを使って、用意された Canvas オブジェクト内にドラッグ シャドウを描画します。

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

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

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

ドラッグの開始

ユーザーは、View オブジェクト上のドラッグ操作(通常は長押し)でドラッグを開始します。 これを受けて、アプリでは以下のように対応します。

  1. 必要に応じて、移動対象のデータの ClipDataClipData.Item を作成します。ClipData オブジェクトの一部として ClipData 内の ClipDescription オブジェクトに格納されているメタデータを渡します。データの移動以外のドラッグ&ドロップ オペレーションでは、実際のオブジェクトの代わりに null を使うとよいでしょう。

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

    Kotlin

    const val IMAGEVIEW_TAG = "icon bitmap"
    ...
    val imageView = ImageView(this).apply {
        setImageBitmap(iconBitmap)
        tag = IMAGEVIEW_TAG
        imageView.setOnLongClickListener { v: View ->
            // 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 will create a new ClipDescription object within the
            // ClipData, and set its MIME type entry to "text/plain"
            val dragData = ClipData(
                    v.tag as? CharSequence,
                    arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN),
                    item)
    
            // Instantiates the drag shadow builder.
            val myShadow = MyDragShadowBuilder(this)
    
            // Starts the drag
            v.startDrag(
                    dragData,   // the data to be dragged
                    myShadow,   // the drag shadow builder
                    null,       // no need to use local data
                    0           // flags (not currently used, set to 0)
            )
        }
    }
    

    Java

    // Create a string for the ImageView label
    private static final String IMAGEVIEW_TAG = "icon bitmap"
    
    // Creates a new ImageView
    ImageView imageView = new ImageView(this);
    
    // Sets the bitmap for the ImageView from an icon bit map (defined elsewhere)
    imageView.setImageBitmap(iconBitmap);
    
    // Sets the tag
    imageView.setTag(IMAGEVIEW_TAG);
    
        ...
    
    // Sets a long click listener for the ImageView using an anonymous listener object that
    // implements the OnLongClickListener interface
    imageView.setOnLongClickListener(new View.OnLongClickListener() {
    
        // Defines the one method for the interface, which is called when the View is long-clicked
        public boolean onLongClick(View v) {
    
        // Create a new ClipData.
        // This is done in two steps to provide clarity. The convenience method
        // ClipData.newPlainText() can create a plain text ClipData in one step.
    
        // Create a new ClipData.Item from the ImageView object's tag
        ClipData.Item item = new ClipData.Item(v.getTag());
    
        // Create a new ClipData using the tag as a label, the plain text MIME type, and
        // the already-created item. This will create a new ClipDescription object within the
        // ClipData, and set its MIME type entry to "text/plain"
        ClipData dragData = new ClipData(
            v.getTag(),
            new String[] { ClipDescription.MIMETYPE_TEXT_PLAIN },
            item);
    
        // Instantiates the drag shadow builder.
        View.DragShadowBuilder myShadow = new MyDragShadowBuilder(imageView);
    
        // Starts the drag
    
                v.startDrag(dragData,  // the data to be dragged
                            myShadow,  // the drag shadow builder
                            null,      // no need to use local data
                            0          // flags (not currently used, set to 0)
                );
    
        }
    }
    
  2. 以下のコード スニペットでは、myDragShadowBuilder を定義し、TextView をドラッグする際のドラッグ シャドウとして、小さなグレーの長方形を作成しています。

    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) {
            // Sets the width of the shadow to half the width of the original View
            val width: Int = view.width / 2
    
            // Sets 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 will provide. As a result, the drag shadow will fill the
            // Canvas.
            shadow.setBounds(0, 0, width, height)
    
            // Sets the size parameter's width and height values. These get back to the system
            // through the size parameter.
            size.set(width, height)
    
            // Sets the touch point's position to be in the middle of the drag shadow
            touch.set(width / 2, height / 2)
        }
    
        // Defines a callback that draws the drag shadow in a Canvas that the system constructs
        // from the dimensions passed in onProvideShadowMetrics().
        override fun onDrawShadow(canvas: Canvas) {
            // Draws the ColorDrawable in 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 thing
        private static Drawable shadow;
    
            // Defines the constructor for myDragShadowBuilder
            public MyDragShadowBuilder(View v) {
    
                // Stores the View parameter passed to myDragShadowBuilder.
                super(v);
    
                // Creates a draggable image that will fill the Canvas provided by the system.
                shadow = new ColorDrawable(Color.LTGRAY);
            }
    
            // Defines a callback that sends the drag shadow dimensions and touch point back to the
            // system.
            @Override
            public void onProvideShadowMetrics (Point size, Point touch) {
                // Defines local variables
                private int width, height;
    
                // Sets the width of the shadow to half the width of the original View
                width = getView().getWidth() / 2;
    
                // Sets the height of the shadow to half the height of the original View
                height = getView().getHeight() / 2;
    
                // The drag shadow is a ColorDrawable. This sets its dimensions to be the same as the
                // Canvas that the system will provide. As a result, the drag shadow will fill the
                // Canvas.
                shadow.setBounds(0, 0, width, height);
    
                // Sets the size parameter's width and height values. These get back to the system
                // through the size parameter.
                size.set(width, height);
    
                // Sets the touch point's position to be in the middle of the drag shadow
                touch.set(width / 2, height / 2);
            }
    
            // Defines a callback that draws the drag shadow in a Canvas that the system constructs
            // from the dimensions passed in onProvideShadowMetrics().
            @Override
            public void onDrawShadow(Canvas canvas) {
    
                // Draws the ColorDrawable in the Canvas passed in from the system.
                shadow.draw(canvas);
            }
        }
    

    注: View.DragShadowBuilder を拡張する必要はありません。コンストラクタ View.DragShadowBuilder(View) により、デフォルトのドラッグ シャドウが作成されます。このドラッグ シャドウは、引数で渡された View と同じサイズで、タッチポイントが中心にあります。

ドラッグ開始への対応

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

アクション タイプ ACTION_DRAG_STARTED のイベントを受けて、リスナーでは以下の対応を行います。

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

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

  2. そのリスナーでドロップを受け入れ可能であれば、true を返します。これにより、システムからそのリスナーに引き続きドラッグ イベントが送信されます。 ドロップの受け入れができなければ、false を返します。その場合は、システムから ACTION_DRAG_ENDED が送信されるまで、ドラッグ イベントは送信されなくなります。

なお、ACTION_DRAG_STARTED イベントに対しては、DragEvent メソッド getClipData()getX()getY()getResult() は有効ではありません。

ドラッグ中のイベントの処理

ドラッグの間、ACTION_DRAG_STARTED ドラッグ イベントに true を返したリスナーは、ドラッグ イベントを受信し続けます。ドラッグ中にリスナーが受信するドラッグ イベントのタイプは、ドラッグ シャドウの位置とリスナーの View の可視性によって異なります。

リスナーでは、ドラッグ中のドラッグ イベントを、主に View の外観を変更するかどうかの判断に使用します。

ドラッグの間、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() を呼び出して、startDrag() の呼び出し時に提供された 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 オブジェクトの外観を変更した場合は、それをデフォルトの外観にリセットします。これにより、ドラッグ オペレーションが終了したことをユーザーに視覚的に示せます。
  2. 必要であれば、getResult() を呼び出すことで、ドラッグ オペレーションについてのより詳しい情報を取得できます。アクション タイプ ACTION_DROP のイベントに対して、いずれかのリスナーから true が返されていた場合は、getResult() からブール値 true が返されます。それ以外の場合はすべて(システムから ACTION_DROP イベントが送信されない場合も含め)、getResult() からブール値 false が返されます。
  3. リスナーはシステムにブール値 true を返します。

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

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

Kotlin

// Creates a new drag event listener
private val dragListen = View.OnDragListener { v, event ->

    // Handles each of the expected events
    when (event.action) {
        DragEvent.ACTION_DRAG_STARTED -> {
            // Determines if this View can accept the dragged data
            if (event.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. 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. Return true; the return value is ignored.
            (v as? ImageView)?.setColorFilter(Color.GREEN)

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

        DragEvent.ACTION_DRAG_LOCATION ->
            // Ignore the event
            true
        DragEvent.ACTION_DRAG_EXITED -> {
            // Re-sets the color tint to blue. Returns true; the return value is ignored.
            (v as? ImageView)?.setColorFilter(Color.BLUE)

            // Invalidate the view to force a redraw in the new tint
            v.invalidate()
            true
        }
        DragEvent.ACTION_DROP -> {
            // Gets the item containing the dragged data
            val item: ClipData.Item = event.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(event.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 OnDragListener.")
            false
        }
    }
}
...
val imageView = ImageView(this)

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

Java

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

View imageView = new ImageView(this);

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

...

protected class myDragEventListener implements View.OnDragListener {

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

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

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

            case DragEvent.ACTION_DRAG_STARTED:

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

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

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

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

                }

                // Returns false. 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. Return true; the return value is ignored.

                v.setColorFilter(Color.GREEN);

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

                return true;

            case DragEvent.ACTION_DRAG_LOCATION:

                // Ignore the event
                return true;

            case DragEvent.ACTION_DRAG_EXITED:

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

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

                return true;

            case DragEvent.ACTION_DROP:

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

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

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

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

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

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

            case DragEvent.ACTION_DRAG_ENDED:

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

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

                // Does a getResult(), and displays what happened.
                if (event.getResult()) {
                    Toast.makeText(this, "The drop was handled.", Toast.LENGTH_LONG).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 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 の DragAndDropAcrossApps サンプルから入手できます。

SourceDragAndDropActivity

Kotlin

// Drag a file stored under an "images/" directory within internal storage.
val internalImagesDir = File(context.filesDir, "images")
val imageFile = File(internalImagesDir, file-name)
val uri: Uri = FileProvider.getUriForFile(
        context, file-provider-content-authority, imageFile)

// Container for where the image originally appears in the source app.
val srcImageView = findViewById(R.id.my-image-id)

val listener = DragStartHelper.OnDragStartListener = { view, _ ->
    val clipData = ClipData(clip-description, 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, drag-shadow-builder, null, flags)
}

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

Java

// Drag a file stored under an "images/" directory within internal storage.
File internalImagesDir = new File(context.filesDir, "images");
File imageFile = new File(internalImagesDir, file-name);
final Uri uri = FileProvider.getUriForFile(
        context, file-provider-content-authority, imageFile);

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

DragStartHelper.OnDragStartListener listener =
        new DragStartHelper.OnDragStartListener() {
            @Override
            public boolean onDragStart(View v, DragStartHelper helper) {
                ClipData clipData = new ClipData(
                        clip-description, 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 v.startDragAndDrop(clipData, drag-shadow-builder, null, flags);
            }
        };

// Detect and start the drag event.
DragStartHelper helper = new DragStartHelper(srcImageView, listener);
helper.attach();

TargetDragAndDropActivity

Kotlin

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

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.
    }
}

Java

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

targetImageView.setOnDragListener(
        new View.OnDragListener() {
            @Override
            public boolean onDrag(View view, DragEvent dragEvent) {
                switch (dragEvent.getAction()) {
                    case ACTION_DROP:
                        ClipData.Item imageItem =
                                dragEvent.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(dragEvent);
                        ((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.
                }
            }
        });