在 Flutter 中创建可拖动的浮动操作按钮[Flutter专题15]

2021-12-07 15:12:55 浏览数 (1)

Flutter 允许您使用FloatingActionButton小部件添加浮动操作按钮。但是,它不允许您拖动按钮。如果你想让它可拖动怎么办。本教程有一个示例,说明您需要做什么才能创建浮动操作按钮,只要它位于父小部件内,就可以将其拖动到屏幕周围的任何位置。

创建可拖动的浮动操作按钮

我们将为这样的小部件创建一个类。我们需要处理的第一件事是使按钮可跟随指针拖动的能力。可以使用的小部件之一是Listener,它能够检测指针移动事件并提供移动细节。基本上,按钮需要包装为Listener.

Listener小部件具有onPointerMove可用于反馈当指针移动时的事件,这将被称为参数。回调函数必须有一个参数PointerMoveEvent,其中包含 x 和 y 方向(delta.dxdelta.dy)的移动增量。必须根据移动增量更新按钮的偏移量。

一个浮动的动作按钮通常可以在点击时执行一个动作,所以我们添加一个名为onPressed( VoidCallback) 的参数作为参数。该Listener部件有onPointerUp参数当用户释放的指针将被调用。因此,我们可以使用它来传递调用onPressed回调的回调函数。但你需要小心。通常,所需的行为是onPressed仅在点击按钮时调用回调,而不是在拖动结束时调用。然而,当拖动结束时,指针向上事件也会被触发。作为解决方案,我们需要跟踪按钮是否被拖动。该_isDragging状态变量是为此目的而设立。它应该更新到true指针移动时。所以,我们可以检查内部onPointerUpcallback 仅onPressed在值为_isDraggingis 时调用回调false

下面是用于创建可拖动浮动操作按钮的类。它有一些参数,包括child(要设置为按钮的小部件)、initialOffset(移动前的初始偏移量)和onPressed(单击按钮时调用的回调)。child小部件使用Positioned基于当前偏移量的小部件呈现。它也被包装为Listener小部件的子级。还有一种方法_updatePosition可以根据移动增量更新当前偏移量。

代码语言:javascript复制
class DraggableFloatingActionButton extends StatefulWidget {
  
    final Widget child;
    final Offset initialOffset;
    final VoidCallback onPressed;
  
    DraggableFloatingActionButton({
      required this.child,
      required this.initialOffset,
      required this.onPressed,
    });
  
    @override
    State<StatefulWidget> createState() => _DraggableFloatingActionButtonState();
  }
  
  class _DraggableFloatingActionButtonState extends State<DraggableFloatingActionButton> {
  
    bool _isDragging = false;
    late Offset _offset;
  
    @override
    void initState() {
      super.initState();
      _offset = widget.initialOffset;
    }
  
    void _updatePosition(PointerMoveEvent pointerMoveEvent) {
      double newOffsetX = _offset.dx   pointerMoveEvent.delta.dx;
      double newOffsetY = _offset.dy   pointerMoveEvent.delta.dy;
 
      setState(() {
        _offset = Offset(newOffsetX, newOffsetY);
      });
    }
  
    @override
    Widget build(BuildContext context) {
      return Positioned(
        left: _offset.dx,
        top: _offset.dy,
        child: Listener(
          onPointerMove: (PointerMoveEvent pointerMoveEvent) {
            _updatePosition(pointerMoveEvent);
  
            setState(() {
              _isDragging = true;
            });
          },
          onPointerUp: (PointerUpEvent pointerUpEvent) {
            print('onPointerUp');
  
            if (_isDragging) {
              setState(() {
                _isDragging = false;
              });
            } else {
              widget.onPressed();
            }
          },
          child: widget.child,
        ),
      );
    }
  }

需要处理的另一件事是防止浮动操作按钮脱离父级框。如果我们忽略这一点,用户可以将按钮拖到父框之外。这意味着有必要知道父级的宽度和高度。您需要向父小部件添加一个键并将其传递给DraggableFloatingActionButton小部件从key中,你可以从currentContext属性中获取RenderBox,它有findRenderObject方法。然后,您可以从 RenderBox 的 size 属性中获取父级的大小。您必须小心,因为必须在构建树之后调用 findRenderObject 方法。因此,您需要使用 WidgetsBinding 的 addPostFrameCallback 来调用它。

获得父尺寸后,您可以计算水平和垂直轴上的最小和最大偏移量。不仅是父尺寸,您还需要考虑按钮尺寸来确定最大偏移量。因此,您需要为子小部件做类似的事情。对于子部件,可以将其包装为 Container 的子部件并将 GlobalKey 传递给 Container。

_updatePosition 方法也需要调整。如果新偏移量低于最小偏移量,则必须将该值设置为最小偏移量。如果新偏移量大于最大偏移量,则必须将该值设置为最大偏移量。您需要对 x 轴和 y 轴执行此操作。

代码语言:javascript复制
  class DraggableFloatingActionButton extends StatefulWidget {
  
    final Widget child;
    final Offset initialOffset;
    final VoidCallback onPressed;
  
    DraggableFloatingActionButton({
      required this.child,
      required this.initialOffset,
      required this.onPressed,
    });
  
    @override
    State<StatefulWidget> createState() => _DraggableFloatingActionButtonState();
  }
  
  class _DraggableFloatingActionButtonState extends State<DraggableFloatingActionButton> {
  
    final GlobalKey _key = GlobalKey();
  
    bool _isDragging = false;
    late Offset _offset;
    late Offset _minOffset;
    late Offset _maxOffset;
  
    @override
    void initState() {
      super.initState();
      _offset = widget.initialOffset;
  
      WidgetsBinding.instance?.addPostFrameCallback(_setBoundary);
    }
  
    void _setBoundary(_) {
      final RenderBox parentRenderBox = widget.parentKey.currentContext?.findRenderObject() as RenderBox;
      final RenderBox renderBox = _key.currentContext?.findRenderObject() as RenderBox;
  
      try {
        final Size parentSize = parentRenderBox.size;
        final Size size = renderBox.size;
  
        setState(() {
          _minOffset = const Offset(0, 0);
          _maxOffset = Offset(
            parentSize.width - size.width,
            parentSize.height - size.height
          );
        });
      } catch (e) {
        print('catch: $e');
      }
    }
  
    void _updatePosition(PointerMoveEvent pointerMoveEvent) {
      double newOffsetX = _offset.dx   pointerMoveEvent.delta.dx;
      double newOffsetY = _offset.dy   pointerMoveEvent.delta.dy;
  
      if (newOffsetX < _minOffset.dx) {
        newOffsetX = _minOffset.dx;
      } else if (newOffsetX > _maxOffset.dx) {
        newOffsetX = _maxOffset.dx;
      }
  
      if (newOffsetY < _minOffset.dy) {
        newOffsetY = _minOffset.dy;
      } else if (newOffsetY > _maxOffset.dy) {
        newOffsetY = _maxOffset.dy;
      }
  
      setState(() {
        _offset = Offset(newOffsetX, newOffsetY);
      });
    }
  
    @override
    Widget build(BuildContext context) {
      return Positioned(
        left: _offset.dx,
        top: _offset.dy,
        child: Listener(
          onPointerMove: (PointerMoveEvent pointerMoveEvent) {
            _updatePosition(pointerMoveEvent);
  
            setState(() {
              _isDragging = true;
            });
          },
          onPointerUp: (PointerUpEvent pointerUpEvent) {
            print('onPointerUp');
  
            if (_isDragging) {
              setState(() {
                _isDragging = false;
              });
            } else {
              widget.onPressed();
            }
          },
          child: Container(
            key: _key,
            child: widget.child,
          ),
        ),
      );
    }
  }

完整代码

下面是使用上述DraggableFloatingActionButton类的完整代码。一个简单的圆形小部件作为child参数传递,这意味着它成为可拖动的按钮。您可以为按钮使用任何小部件,包括 Flutter 的FloatingActionButton小部件。

代码语言:javascript复制
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '坚果前端',
      home: DraggableFloatingActionButtonExample(),
    );
  }
}

class DraggableFloatingActionButtonExample extends StatelessWidget {
  final GlobalKey _parentKey = GlobalKey();

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: AppBar(
        title: const Text('公众号:坚果前端'),
        centerTitle: true,
        backgroundColor: Colors.teal,
      ),
      body: Column(
        children: [
          Container(
            height: 100,
          ),
          Container(
            width: 300,
            height: 300,
            child: Stack(
              key: _parentKey,
              children: [
                Container(color: Colors.teal),
                Center(
                  child: const Text(
                    '坚果前端',
                    style: const TextStyle(color: Colors.white, fontSize: 24),
                  ),
                ),
                DraggableFloatingActionButton(
                  child: Container(
                    width: 50,
                    height: 50,
                    decoration: ShapeDecoration(
                      shape: CircleBorder(),
                      color: Colors.white,
                    ),
                    child:
                        Icon(Icons.flutter_dash, color: Colors.blue, size: 50),
                  ),
                  initialOffset: const Offset(100, 100),
                  parentKey: _parentKey,
                  onPressed: () {
                    print('Button is clicked');
                  },
                ),
              ],
            ),
          )
        ],
      ),
    );
  }
}

class DraggableFloatingActionButton extends StatefulWidget {
  final Widget child;
  final Offset initialOffset;
  final VoidCallback onPressed;
  final GlobalKey parentKey;

  DraggableFloatingActionButton({
    required this.child,
    required this.initialOffset,
    required this.onPressed,
    required this.parentKey,
  });

  @override
  State<StatefulWidget> createState() => _DraggableFloatingActionButtonState();
}

class _DraggableFloatingActionButtonState
    extends State<DraggableFloatingActionButton> {
  final GlobalKey _key = GlobalKey();

  bool _isDragging = false;
  late Offset _offset;
  late Offset _minOffset;
  late Offset _maxOffset;

  @override
  void initState() {
    super.initState();
    _offset = widget.initialOffset;

    WidgetsBinding.instance?.addPostFrameCallback(_setBoundary);
  }

  void _setBoundary(_) {
    final RenderBox parentRenderBox =
        widget.parentKey.currentContext?.findRenderObject() as RenderBox;
    final RenderBox renderBox =
        _key.currentContext?.findRenderObject() as RenderBox;

    try {
      final Size parentSize = parentRenderBox.size;
      final Size size = renderBox.size;

      setState(() {
        _minOffset = const Offset(0, 0);
        _maxOffset = Offset(
            parentSize.width - size.width, parentSize.height - size.height);
      });
    } catch (e) {
      print('catch: $e');
    }
  }

  void _updatePosition(PointerMoveEvent pointerMoveEvent) {
    double newOffsetX = _offset.dx   pointerMoveEvent.delta.dx;
    double newOffsetY = _offset.dy   pointerMoveEvent.delta.dy;

    if (newOffsetX < _minOffset.dx) {
      newOffsetX = _minOffset.dx;
    } else if (newOffsetX > _maxOffset.dx) {
      newOffsetX = _maxOffset.dx;
    }

    if (newOffsetY < _minOffset.dy) {
      newOffsetY = _minOffset.dy;
    } else if (newOffsetY > _maxOffset.dy) {
      newOffsetY = _maxOffset.dy;
    }

    setState(() {
      _offset = Offset(newOffsetX, newOffsetY);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Positioned(
      left: _offset.dx,
      top: _offset.dy,
      child: Listener(
        onPointerMove: (PointerMoveEvent pointerMoveEvent) {
          _updatePosition(pointerMoveEvent);

          setState(() {
            _isDragging = true;
          });
        },
        onPointerUp: (PointerUpEvent pointerUpEvent) {
          print('onPointerUp');

          if (_isDragging) {
            setState(() {
              _isDragging = false;
            });
          } else {
            widget.onPressed();
          }
        },
        child: Container(
          key: _key,
          child: widget.child,
        ),
      ),
    );
  }
}

输出:

概括

这就是如何在 Flutter 中创建可拖动的浮动操作按钮。基本上,您可以使用Listener小部件来检测指针移动事件并根据移动增量更新按钮偏移。该Listener小部件还支持检测应执行按钮操作的指针向上事件,除非它刚刚被拖动。您还需要获取父级和按钮的大小,以防止按钮脱离父级框。

0 人点赞