【Flutter 元件集錄】Draggable 與 DragTarget

張風捷特烈發表於2021-08-30
前言:

這是我參與8月更文挑戰的第 30 天,活動詳情檢視:8月更文挑戰。為應掘金的八月更文挑戰,我準備在本月挑選 31 個以前沒有介紹過的元件,進行全面分析和屬性介紹。這些文章將來會作為 Flutter 元件集錄 的重要素材。希望可以堅持下去,你的支援將是我最大的動力~

本系列元件文章列表
1.NotificationListener2.Dismissible3.Switch
4.Scrollbar5.ClipPath6.CupertinoActivityIndicator
7.Opacity8.FadeTransition9. AnimatedOpacity
10. FadeInImage11. Offstage12. TickerMode
13. Visibility14. Padding15. AnimatedContainer
16.CircleAvatar17.PhysicalShape18.Divider
19.Flexible、Expanded 和 Spacer 20.Card21.SizedBox
22.ConstrainedBox23.Stack24.Positioned
25.OverflowBox26.SizedOverflowBox27. DecoratedBox
28. BackdropFilter29.ImageFiltered 與 ColorFiltered30.Draggable 與 DragTarget

一、認識 Draggable 元件

Draggable 顧名思義,是可拖動的元件,它繼承自 StatefulWidget ,且可接受一個泛型。 構造方法有非常多的入參,其中必須傳入的是 childfeedback 兩個元件。

final Widget child;
final Widget feedback;
複製程式碼

1. 拖動的方向: axis

下面先通過一個小案例認識一下 Draggable:下面是三個 Draggable 元件,其中 child 是藍色小圓,feedback 是紅色小圓,三者的區別在於 axis 屬性不同。左邊 axisnull ,表示不限定軸向,可以自由拖動;中間 axisvertical ,只能在豎直方向拖動;中間 axishorizontal ,只能在水平方向拖動。

class CustomDraggable extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    List<Axis?> axis = [null, Axis.vertical, Axis.horizontal];
    return Wrap(
        spacing: 30,
        children: axis
            .map((Axis? axis) => Draggable(
                  axis: axis,
                  child: buildContent(),
                  feedback: buildFeedback(),
                ))
            .toList());
  }

  Widget buildContent() {
    return Container(
      width: 30,
      height: 30,
      alignment: Alignment.center,
      decoration: BoxDecoration(
        color: Colors.blue,
        shape: BoxShape.circle,
      ),
    );
  }

  Widget buildFeedback() {
    return Container(
      width: 30,
      height: 30,
      decoration: BoxDecoration(
        color: Colors.red,
        shape: BoxShape.circle,
      ),
    );
  }
}
複製程式碼

2.拖動時原位置元件: childWhenDragging

Draggable 可以通過 childWhenDragging 屬性指定在拖拽過程中原來位置的元件。如下,拖動時原來的位置顯示為 橙色小圓 和 刪除圖示。

Draggable(
  axis: axis,
  childWhenDragging: buildWhenDragging(),
  child: buildContent(),
  feedback: buildFeedback(),
))
  
Widget buildWhenDragging() {
  return Container(
    width: 30,
    height: 30,
    decoration: BoxDecoration(
      color: Colors.orange,
      shape: BoxShape.circle,
    ),
    child: Icon(
      Icons.delete_outline,
      size: 20,
      color: Colors.white,
    ),
  );
}
複製程式碼

二、Draggable 與 DragTarget 聯合使用

1. 綜合測試案例

下面通過一個示例測試一下 DraggableDragTarget 的聯合使用。如下,上面的小球是 Draggable ,下面的區域是 DragTarget 。可以拖動小球來為 DragTarget 著色,並且顯示當前操作的資訊。

Draggable 可以監聽五個回撥:

  • onDragStarted :開始拖動時回撥,無回撥資料。
  • onDragEnd:結束拖動時回撥,可以獲取 DraggableDetails 資料。
  • onDragUpdate:拖動更新時回撥,可以獲取 DraggableDetails 資料。
  • onDragCompleted : 拖入目標區域,並鬆手完成時回撥,無回撥資料。
  • onDraggableCanceled:未在目標區域,拖拽取消回撥,可以獲取 VelocityOffset資料。
List<Widget> _buildDraggable() {
  return colors.map(
        (Color color) => Draggable<Color>(
            onDragStarted: _onDragStarted,
            onDragEnd: _onDragEnd,
            onDragUpdate: _onDragUpdate,
            onDragCompleted: _onDragCompleted,
            onDraggableCanceled: _onDraggableCanceled,
            childWhenDragging: childWhenDragging(colors.indexOf(color).toString()),
            child: buildContent(color),
            data: color,
            feedback: buildFeedback(color)),
      ).toList();
}

void _onDragUpdate(DragUpdateDetails details) {
  print('座標:'
      '(${details.localPosition.dx.toStringAsFixed(1)},'
      '${details.localPosition.dy.toStringAsFixed(1)})');
}

void _onDraggableCanceled(Velocity velocity, Offset offset) {
  _info = '拖拽取消';
}

void _onDragCompleted() {
  _info = '拖拽完成';
}

void _onDragEnd(DraggableDetails details) {
  setState(() => _info = '結束拖拽');
}

void _onDragStarted() {
     setState(() => _info = '開始拖拽');
}
複製程式碼

DragTarget 會通過 builder 回撥來構建元件,其中會回撥 candidateDatarejectedData 兩個列表,其中包含接受拒絕 的資料。由於 Draggable 支援多個同時拖動,使用是資料列表。

DragTarget<Color>(
  onLeave: _onLeave,
  onAccept: _onAccept,
  onWillAccept: _onWillAccept,
  builder: _buildTarget,
)
  
Widget _buildTarget(BuildContext context, List<Color?> candidateData, List rejectedData) {
  return Container(
      width: 150.0,
      height: 50.0,
      color: _color,
      child: Center(
        child: Text(
          _info,
          style: TextStyle(color: Colors.white),
        ),
      ));
}

void _onLeave(Color? data) {
  print("onLeave: data = $data ");
  setState(() => _info = 'onLeave');
}

void _onAccept(Color data) {
  print("onAccept: data = $data ");
  setState(() => _color = data);
}

bool _onWillAccept(Color? data) {
  print("onWillAccept: data = $data ");
  setState(() => _info = 'onWillAccept');
  return data != null;
}
複製程式碼

onWillAcceptDragTarget 中比較重要的一個回撥,當拖動的元件到達目標區域後,onWillAccept 會觸發。從下面原始碼中可以看出 _candidateAvatars_rejectedAvatarsonWillAccept 的返回值有關。如果 onWillAccept 返回 false ,則資料會被簡入到 _rejectedAvatars

builder 中的回撥入參 candidateDatarejectedData 就是根據上面兩個列表計算的。


2.拖拽刪除案例

如下示例,通過擴充元件目標到指定位置進行移除,通過 Draggable 和 DragTarget 聯合就很容易實現。

程式碼實現如下,通過顏色陣列 colors 生成不同顏色的 Draggable ,並擁有 int 泛型,傳遞的數值為可拖拽元件的索引,這樣在 DragTargetonAccept 中可以獲取拖入進的索引資料,從而實現刪除功能。

class DeleteDraggable extends StatefulWidget {
  @override
  _DeleteDraggableState createState() => _DeleteDraggableState();
}

class _DeleteDraggableState extends State<DeleteDraggable> {
  List<Color> colors = [
    Colors.red, Colors.yellow, Colors.blue, Colors.green,
    Colors.orange, Colors.purple, Colors.cyanAccent];

  @override
  Widget build(BuildContext context) {
    return  Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          Wrap(
            children: _buildDraggable(),
            spacing: 10,
          ),
          SizedBox(
            height: 20,
          ),
          DragTarget<int>(
              onAccept: _onAccept,
              onWillAccept: (data) => data != null,
              builder: buildTarget
          )
        ],
    );
  }

  Widget buildTarget(context, candidateData, rejectedData) => Container(
      width: 40.0,
      height: 40.0,
      decoration: BoxDecoration(color: Colors.red, shape: BoxShape.circle),
      child: Center(
        child: Icon(Icons.delete_sweep, color: Colors.white),
      ));

  List<Widget> _buildDraggable() => colors
      .map((Color color) => Draggable<int>(
            child: buildContent(color),
            data: colors.indexOf(color),
            childWhenDragging: buildWhenDragging(),
            feedback: buildFeedback(color)),
      ).toList();

  Widget buildContent(Color color) {
    return Container(
      width: 30,
      height: 30,
      alignment: Alignment.center,
      child: Text(
        colors.indexOf(color).toString(),
        style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
      ),
      decoration: BoxDecoration(color: color, shape: BoxShape.circle),
    );
  }

  Widget buildFeedback(Color color) {
    return Container(
      width: 25,
      height: 25,
      decoration:
          BoxDecoration(color: color.withAlpha(100), shape: BoxShape.circle),
    );
  }

  Widget buildWhenDragging() {
    return Container(
      width: 30,
      height: 30,
      decoration: BoxDecoration(color: Colors.red, shape: BoxShape.circle),
      child: Icon(Icons.delete_outline, size: 20, color: Colors.white,
      ),
    );
  }

  void _onAccept(int data) {
    setState(() {
      colors.removeAt(data);
    });
  }
}
複製程式碼

通過Draggable 和 DragTarget 的聯合使用,不需要我們自己去實現拖拽邏輯,可以很輕鬆解決很多目標拖拽的問題。那本文到這裡就結束了,謝謝觀看,明天見~

相關文章