Android のドラッグ&ドロップ フレームワークを使用すると、インタラクティブなドラッグ&ドロップ機能をアプリに追加できます。ドラッグ&ドロップを使用すると、ユーザーは、テキスト、画像、オブジェクトなど、URI で表現できるすべてのコンテンツを、アプリ内の 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
イベントを受け取ったリスナーも含め、ドラッグ イベントを受信するよう登録されているリスナーすべてに送信されます。
上記 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 上であるか、現在のレイアウトに含まれていない場所である場合は、このアクション タイプは送信されません。
リスナーでドロップを正常に処理した場合は、ブール値 |
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 |
✓ | ✓ |
メソッド 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 つのパラメータがあります。onDrawShadow()
onProvideShadowMetrics()
呼び出しの直後に、ドラッグ シャドウを作成するonDrawShadow()
が呼び出されます。このメソッドには、Canvas
オブジェクトという単一の引数があります。このオブジェクトは、onProvideShadowMetrics()
で渡したパラメータに基づいてシステムにより作成されます。このメソッドを使って、用意された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(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; });
次のコード スニペットでは、
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
のイベントを受けて、ドラッグ イベント リスナーでは以下の対応を行います。
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
がドラッグ対象コンテンツを受け入れ可能であることを報告していれば、システムから View
に対してアクション タイプ ACTION_DROP
のドラッグ イベントが送信されます。
ドラッグ イベント リスナーでは、以下の処理を行います。
getClipData()
を呼び出して、startDragAndDrop()
の呼び出し時に提供されたClipData
オブジェクトを取得し、データを処理します。データの移動以外のドラッグ&ドロップ オペレーションでは、この処理は不要です。
ドロップが正常に処理された場合、それを示すためにブール値
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
のドラッグ イベントが送信されます。これにより、ドラッグ&ドロップ オペレーションが終了したことが示されます。
それぞれのドラッグ イベント リスナーでは、以下の処理を行います。
- ドラッグ&ドロップ オペレーション中にリスナーが
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 -> // 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()
は、ドロップ ターゲットを指定できる静的なオーバーロード メソッドです。次のパラメータがあります。
- 現在の
Activity
(URI 権限に使用) - ドロップ ターゲットとして機能する
View
- ドロップされたデータからドロップ ターゲットが受け入れ可能な MIME タイプ
- ドロップ ターゲットの設定オプション(具体的には埋め込みの 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 まで)
MIME タイプ、権限、コンテンツの検証
ドラッグ&ドロップ データを提供するアプリが作成する DropHelper
の MIME タイプのチェックは、ドラッグ&ドロップ ClipDescription
に基づいています。ClipDescription
を検証して、MIME タイプが正しく設定されていることを確認する必要があります。
DropHelper
は、ドラッグ&ドロップ ClipData
に含まれるコンテンツ URI に対する、すべてのアクセス権限をリクエストします(DragAndDropPermissions
を参照)。この権限により、ドラッグ&ドロップ データの処理時にコンテンツ URI を解決できます。
DropHelper
は、ドロップされたデータの URI を解決する際に、コンテンツ プロバイダから返されるデータを検証しません。null を確認して、解決されたデータの正確性を検証する必要があります。