基本的なコンセプト

Compose の方法を試す
Jetpack Compose は、Android で推奨される UI ツールキットです。Compose でドラッグ&ドロップを使用する方法について学習します。

以降のセクションでは、ドラッグ&ドロップ プロセスの重要なコンセプトについて説明します。

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

ドラッグ&ドロップのプロセスには、開始、継続、ドロップ、終了の 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() を呼び出します。取り得る値は 6 つあり、DragEvent クラスの定数で定義されています。表 1 に示します。

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

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

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

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

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

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

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

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

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

ドラッグ シャドウ

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

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

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

View.DragShadowBuilder(View)

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

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

View.DragShadowBuilder()

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

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

onProvideShadowMetrics()

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

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

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

onDrawShadow()

onProvideShadowMetrics() 呼び出しの直後に、ドラッグ シャドウを作成する onDrawShadow() が呼び出されます。このメソッドには、onProvideShadowMetrics() で渡したパラメータに基づいてシステムにより作成される Canvas オブジェクトという単一の引数があります。このメソッドを使って、用意された Canvas 内にドラッグ シャドウを描画します。

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

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

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 の組み合わせに似ています。