[読書会]UI要素のドラッグ

 ドラッグ&ドロップは、一般的なモバイルアプリの操作です。
(すなわち、ウィジェットを長押しタッチ&ホールド)し、(ユーザの指の下に別のウィジェットが表示されるので、(そのウィジェットを))最終的な位置までドラッグ&ドロップする、という操作です。 )
 ここでは(例として)ユーザが料理の選択肢を長押しし、その料理をお金を払っている客の写真にドラッグする、というドラッグ&ドロップインタラクションの解説がされています。

1.Press and drag

 FlutterはLongPressDraggableと呼ばれるウィジェットを提供しており、ドラッグアンドドロップのインタラクションを開始するために必要な動作を正確に提供します。
LongPressDraggableウィジェットは、長押しが発生したことを認識し、ユーザの指の近くに新しいウィジェットを表示します。
LongPressDraggableは、ユーザがドラッグするウィジェットを完全にコントロールできます。

(1)MenuListItem

 各メニューリスト項目はカスタムMenuListItemウィジェットで表示されます。

MenuListItem(例):
MenuListItem(
  name: item.name,
  price: item.formattedTotalItemPrice,
  photoProvider: item.imageProvider,
)

(2)LongPressDraggable

 MenuListItem ウィジェットを LongPressDraggable ウィジェットでラップします。

LongPressDraggable(例):
LongPressDraggable<Item>(
  data: item,
  //dragAnchorStrategy プロパティ:pointerDragAnchorStrategy
  // このプロパティ値は、LongPressDraggable が 
  //DraggableListItem の位置をユーザの指を基準にするように指示します。
  dragAnchorStrategy: pointerDragAnchorStrategy,
  //ユーザが MenuListItem ウィジェットを長押しすると、
  // LongPressDraggable ウィジェットは DraggingListItem
  //(選択された食品の写真 )を(ユーザの指の下の中央に)表示します。
  feedback: DraggingListItem(
    dragKey: _draggableKey,
    photoProvider: item.imageProvider,
  ),
  //長押し対象の MenuListItem ウィジェット
  child: MenuListItem(
    name: item.name,
    price: item.formattedTotalItemPrice,
    photoProvider: item.imageProvider,
  ),
);

 この場合、ユーザが MenuListItem ウィジェットを長押しすると、LongPressDraggable ウィジェットは DraggingListItem(選択された食品の写真 )を(ユーザの指の下の中央)表示します。

 dragAnchorStrategy プロパティは pointerDragAnchorStrategy に設定されています。
このプロパティ値は、LongPressDraggable DraggableListItem の位置をユーザの指を基準にするように指示します。

 ドラッグ&ドロップは、アイテムがドロップされたときに情報が送信されなければ、ほとんど意味がないので、LongPressDraggableデータ・パラメータ(ここでは、データの型はItemであり、ユーザーが押したフードメニュー項目に関する情報)保有しています。

 LongPressDraggable関連付けられたデータは、DragTargetと呼ばれる特別なウィジェット(ユーザがドラッグジェスチャでドロップする場所に送られます。

 ※後述にある様なドロップ後の動作は、↑上記処理の↓後ろに、実装することになります。

2.Drop the draggable

 ユーザはLongPressDraggable好きな場所にドロップできますが、DragTarget 以外の場所にドロップしても効果はありません
ユーザがDragTargetウィジェットの上にDraggingListItemウィジェットをドロップすると、DragTargetウィジェットはDraggingListItemウィジェットのデータを受け入れ(ここでは、ユーザはメニュー項目CustomerCart ウィジェットにドロップして、メニュー項目をユーザのカートに追加でき)ます(拒否することも可能です)。

ドロップ先にしたいウィジェット設計(例:お客様カート(CustomerCart)):
CustomerCart(
  hasItems: customer.items.isNotEmpty,
  highlighted: candidateItems.isNotEmpty,
  customer: customer,
);

 CustomerCart ウィジェットを DragTarget ウィジェットで囲み、ドロップ先を設定します。

DragTarget でラップしてドロップ先を設定する(例):
//CustomerCart ウィジェットを DragTarget ウィジェットで囲み、
//これを ドロップ先として設定しています。
DragTarget<Item>(
  builder: (context, candidateItems, rejectedItems) {
    //ドロップ先にしたいウィジェット
    return CustomerCart(
      hasItems: customer.items.isNotEmpty,
      highlighted: candidateItems.isNotEmpty,
      customer: customer,
    );
  },
  onAcceptWithDetails: (details) {
    _itemDroppedOnCustomerCart(
      item: details.data,
      customer: customer,
    );
  },
)

 DragTargetは、既存のウィジェットが(表示され、またLongPressDraggableと連携して(ユーザによりDragTargetの上に))ドラッグされたことを認識します。(DragTargetは、ユーザがDragTargetウィジェットの上にドラッグ可能なものをドロップした場合も認識します。)

 ユーザが DragTarget ウィジェット上でドラッグすると、candidateItems ユーザがドラッグするデータ項目が含まれます。この draggable を使用すると、ユーザがウィジェットの上をドラッグした時に、ウィジェットがどの様に見えるかを変更できます。 (ここでは、DragTargetウィジェットの上にアイテムがドラッグされると、Customerウィジェットを赤くしています。赤い外観は、CustomerCartウィジェット内のhighlightedプロパティで設定します。)

 ユーザが DragTarget ウィジェットに draggable をドロップすると、onAcceptWithDetails コールバックが呼び出されます。この時点で、ドロップされたデータ受け入れるかどうかを決めることになります。
(ここでは、商品は常に受入れられ処理されますが、別の判断の為の入荷商品検査も選択余地もあります。)

 DragTargetドロップされるアイテムのタイプは、LongPressDraggableからドラッグされるアイテムのタイプ一致しなければならないことに注意して下さい。型に互換性がない場合、onAcceptWithDetailsメソッドは呼び出されません。

 DragTargetウィジェットが必要なデータを受け付けるように設定されていれば、ドラッグ&ドロップUIのある部分から別の部分にデータを転送できるようになります。

 ※次のステップでは、顧客のカートをドロップしたメニューアイテムで更新します。

3.Add a menu item to a cart

 ここでは、各顧客はCustomerオブジェクトで表され、Customerオブジェクトは商品のカート価格の合計保持します。

各顧客をCustomerオブジェクトで表し、Customerオブジェクトに商品のカートと価格の合計を保持させる(例):
class Customer {
  Customer({
    required this.name,
    required this.imageProvider,
    List<Item>? items,
  }) : items = items ?? [];

  final String name;
  final ImageProvider imageProvider;
  final List<Item> items;

  String get formattedTotalItemPrice {
    final totalPriceCents =
        items.fold<int>(0, (prev, item) => prev + item.totalPriceCents);
    return '\$${(totalPriceCents / 100.0).toStringAsFixed(2)}';
  }
}

 ここでは、CustomerCartウィジェットは、Customerインスタンスに基づいて、顧客の写真、名前、合計、アイテム数を表示しています。

 メニューがドロップされた時に顧客のカートを更新する為に、関連するCustomerオブジェクトにドロップされたアイテム(customer.items)を追加します。

メニューがドロップされた時に顧客のカートを更新する為に、関連するCustomerオブジェクトにドロップされたアイテム(customer.items)を追加する(例):
void _itemDroppedOnCustomerCart({
  required Item item,
  required Customer customer,
}) {
  setState(() {
    customer.items.add(item);
  });
}

 itemDroppedOnCustomerCartメソッドは、ユーザがメニュー項目をCustomerCartウィジェットにドロップすると、onAcceptWithDetails()内で呼び出されます。ドロップしたアイテムをcustomerオブジェクトに追加し、setState()を呼び出してレイアウトを更新すると、UIは新しい顧客の価格合計とアイテム数でリフレッシュされます。

 以上で、顧客の買物カートに食品を追加するドラッグ&ドロップのインタラクションができました。

4.Interactive example

アプリは以下の様な仕様で動作します:

  1. 食品項目をスクロールします。
  2. 指で押したままにするか、マウスでクリックして押したままにします。
  3. 持っている間、リストの上に食べ物の画像が表示されます。
  4. 画像をドラッグして、画面下部の人物の 1 人にドロップします。画像の下のテキストが更新され、その人物の料金が反映されます。食品を追加し続けると、料金が蓄積されていきます。

コメントを残す