Android のドラッグ&ドロップ フレームワークを使用すると、インタラクティブなドラッグ&ドロップ機能をアプリに追加できます。ドラッグ&ドロップを使用すると、ユーザーは URI で表されるテキスト、画像、オブジェクトなどのコンテンツをコピーまたは移動できます。また、アプリ内の 1 つの View
から別の View
へ、またはマルチ ウィンドウ モードのアプリ間でコピーまたは移動できます。
![]() |
![]() |
|
|
このフレームワークには、ドラッグ イベント クラス、ドラッグ リスナー、ヘルパークラスとメソッドが含まれています。このフレームワークは主にデータ転送を可能にすることを目的に設計されていますが、他の 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 上でドラッグ シャドウを解放した場合、または現在のレイアウトに含まれないものの上にドラッグ シャドウを解放した場合、このアクション タイプは送信されません。
リスナーは、ドロップを正常に処理すると、ブール値 |
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 の位置はx
、Y の位置はy
です。onDrawShadow()
onProvideShadowMetrics()
呼び出しの直後に、ドラッグ シャドウを作成するonDrawShadow()
が呼び出されます。このメソッドの引数は 1 つだけで、onProvideShadowMetrics()
で指定されたパラメータからシステムが作成するCanvas
オブジェクトです。このメソッドは、指定されたCanvas
にドラッグ シャドウを描画します。
パフォーマンスを向上させるには、ドラッグ シャドウのサイズを小さくします。単一のアイテムにはアイコンを使用します。複数のアイテムを選択する場合は、画面全体に広がるフルイメージではなく、積み重ねたアイコンを使用します。
ドラッグ&ドロップ オペレーション
このセクションでは、ドラッグの開始、ドラッグ中のイベントへの応答、ドロップ イベントへの応答、ドラッグ&ドロップ オペレーションの終了の各方法を順を追って説明します。
ドラッグを開始する
ユーザーが View
オブジェクト上のドラッグ操作(通常は長押し)でドラッグを開始します。これに応じて、アプリは次のことを行う必要があります。
移動するデータの
ClipData
オブジェクトとClipData.Item
オブジェクトを作成します。ClipData
の一部として、ClipData
内のClipDescription
オブジェクトに格納されているメタデータを指定します。データの移動以外のドラッグ&ドロップ オペレーションの場合は、実際のオブジェクトの代わりにnull
を使用することをおすすめします。例として、次のコード スニペットは、
ImageView
のタグ(ラベル)を含むClipData
オブジェクトを作成することで、ImageView
上での長押しジェスチャーに対応する方法を示しています。Kotlin
// Create a string for the ImageView label. val IMAGEVIEW_TAG = "icon bitmap" ... val imageView = ImageView(context).apply { // Set the bitmap for the ImageView from an icon bitmap defined elsewhere. setImageBitmap(iconBitmap) tag = IMAGEVIEW_TAG setOnLongClickListener { v -> // Create a new ClipData. This is done in two steps to provide // clarity. The convenience method ClipData.newPlainText() can // create a plain text ClipData in one step. // Create a new ClipData.Item from the ImageView object's tag. val item = ClipData.Item(v.tag as? CharSequence) // Create a new ClipData using the tag as a label, the plain text // MIME type, and the already-created item. This creates a new // ClipDescription object within the ClipData and sets its MIME type // to "text/plain". val dragData = ClipData( v.tag as? CharSequence, arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN), item) // Instantiate the drag shadow builder. 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; });
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
のイベントに応答して、ドラッグ イベント リスナーは次のことを行う必要があります。
DragEvent.getClipDescription()
を呼び出し、返されたClipDescription
の MIME タイプメソッドを使用して、リスナーがドラッグ対象のデータを受け入れられるかどうかを確認します。ドラッグ&ドロップ オペレーションがデータの移動ではない場合、この処理は不要です。
ドラッグ イベント リスナーがドロップを受け入れられる場合は、
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_ENTERED
やACTION_DRAG_LOCATION
を受信したとき、リスナーはView
の外観を変更することによって、ビューが潜在的なドロップ ターゲットであることを示せます。- アクション タイプ
ACTION_DRAG_LOCATION
のイベントには、タッチポイントの位置に対応するgetX()
とgetY()
の有効なデータが含まれています。リスナーはこの情報を使用して、タッチポイントでのView
の外観を変更したり、ユーザーがドラッグ シャドウを解放する(データをドロップする)正確な位置を決定したりできます。 ACTION_DRAG_EXITED
に応じて、リスナーはACTION_DRAG_ENTERED
またはACTION_DRAG_LOCATION
に応じて適用する外観の変更をリセットする必要があります。こうすることで、そのView
が現時点でのドロップ先ではなくなったことをユーザーに示せます。
ドロップに対応する
ユーザーが View
上でドラッグ シャドウを離し、以前に View
がドラッグ対象のコンテンツを受け入れられると報告すると、システムはアクション タイプ ACTION_DROP
の View
にドラッグ イベントをディスパッチします。
ドラッグ イベント リスナーでは、次のことを行う必要があります。
getClipData()
を呼び出して、startDragAndDrop()
の呼び出しで提供されていたClipData
オブジェクトを取得し、データを処理します。ドラッグ&ドロップ オペレーションがデータの移動ではない場合、この処理は不要です。ドロップが正常に処理されたことを示す場合はブール値
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
のドラッグ イベントが送信されます。これは、ドラッグ&ドロップ操作が終了したことを示します。
各ドラッグ イベント リスナーは、次のことを行う必要があります。
- オペレーション中にリスナーで
View
オブジェクトの外観が変更された場合、リスナーはView
をデフォルトの外観にリセットする必要があります。これにより、ドラッグ&ドロップ オペレーションが終了したことをユーザーに視覚的に示せます。 - 必要であれば、
getResult()
を呼び出すことで、ドラッグ&ドロップ オペレーションについてのより詳しい情報を取得できます。アクション タイプACTION_DROP
のイベントに対してリスナーがtrue
を返す場合、getResult()
はブール値true
を返します。それ以外の場合はすべて、システムがACTION_DROP
イベントを送信しない場合を含め、getResult()
はブール値false
を返します。 - ドラッグ&ドロップ オペレーションが正常に完了したことを示すには、リスナーがブール値
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()
は、ドロップ ターゲットを指定できる静的なオーバーロード メソッドです。パラメータには次のものがあります。
- 現在の
Activity
- URI 権限に使用されます。 - ドロップ ターゲットの構成オプション(特に、埋め込み
EditText
フィールドのリスト)。 - ドロップされたデータを処理するための
OnReceiveContentListener
。
たとえば、画像を受け入れるドロップ ターゲットを作成するには、次のいずれかのメソッド呼び出しを使用します。
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
コンポーネントのいずれかを選択して、テキストデータを処理します。選択は、次の優先順位に基づいています。
ClipData
がドロップされるEditText
。- テキスト カーソル(キャレット)を含む
EditText
。 DropHelper.Options.Builder.addInnerEditTexts(EditText...)
の呼び出しに指定された最初のEditText
。
EditText
をデフォルトのテキストデータ ハンドラとして設定するには、DropHelper.Options.Builder.addInnerEditTexts(EditText...)
の呼び出しの最初の引数として EditText
を渡します。たとえば、ドロップ ターゲットで画像を処理できるものの、編集可能なテキスト フィールド T1
、T2
、T3
が含まれている場合は、次のようにして 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 をチェックし、解決されたデータの正確性を検証します。