• Flutter实现可拖动悬浮按钮效果
  • 发布于 2个月前
  • 493 热度
    0 评论
一、需求来源
工作中遇到可拖动悬浮按钮的需求,顺手封装一个组件 NSuspension。核心原理是 Stack 拖动的同时不断更新 Postioned 位置。效果下:

二、使用示例
  buildBody() {
    return NSuspension(
      padding: EdgeInsets.only(left: 20, top: 30, right: 40, bottom: 50),
      childSize: Size(80, 80),
      child: Container(
        width: 80,
        height: 80,
        decoration: BoxDecoration(
          color: Colors.blue,
          border: Border.all(color: Colors.blue),
          borderRadius: BorderRadius.all(Radius.circular(8.w)),
        ),
      ),
      bgChild: Container(
        color: Colors.black.withOpacity(0.1), //Color.fromRGBO(242, 243, 248, 1),
      ),
    );
  }
二、NSuspension 组件源码
import 'package:flutter/material.dart';
/// 堆代码 duidaima.com
/// 悬浮组件
class NSuspension extends StatefulWidget {

  NSuspension({
    Key? key,
    required this.child,
    required this.bgChild,
    this.alignment = AlignmentDirectional.topStart,
    this.textDirection,
    this.fit = StackFit.loose,
    this.clipBehavior = Clip.hardEdge,
    this.padding = EdgeInsets.zero,
    this.childSize = const Size(100, 100),
  }) : super(key: key);


  final AlignmentGeometry alignment;

  final TextDirection? textDirection;

  final StackFit fit;

  final Clip clipBehavior;
  /// 底部组件
  final Widget bgChild;
  /// 悬浮组件
  final Widget child;
  /// 悬浮组件宽高
  final Size childSize;
  /// 距离四周边界
  final EdgeInsets padding;

  @override
  _NSuspensionState createState() => _NSuspensionState();
}

class _NSuspensionState extends State<NSuspension> {
  final _topVN = ValueNotifier(0.0);
  final _leftVN = ValueNotifier(0.0);

  @override
  Widget build(BuildContext context) {

    return LayoutBuilder(
      builder: (context, constraints){
        return Stack(
          alignment: widget.alignment,
          textDirection: widget.textDirection,
          fit: widget.fit,
          clipBehavior: widget.clipBehavior,
          children: [
            widget.bgChild,
            buildSuspension(
              child: widget.child,
              maxWidth: constraints.maxWidth,
              maxHeight: constraints.maxHeight,
            ),
          ],
        );
      }
    );
  }

  buildSuspension({
    required Widget child,
    required double maxWidth,
    required double maxHeight
  }) {
    return AnimatedBuilder(
      animation: Listenable.merge([
        _topVN,
        _leftVN,
      ]),
      child: child,
      builder: (context, child) {

        return Positioned(
          top: _topVN.value,
          left: _leftVN.value,
          child: GestureDetector(
            onTap: () {
              debugPrint("onTap");
            },
            onPanUpdate: (DragUpdateDetails e) {
              // debugPrint("e.delta:${e.delta.dx},${e.delta.dy}");

              //用户手指滑动时,更新偏移,重新构建
              //顶部
              if (_topVN.value < widget.padding.top && e.delta.dy < 0) {
                return;
              }
              // 左边
              if (_leftVN.value < widget.padding.left && e.delta.dx < 0) {
                return;
              }
              // 右边
              if (_topVN.value > (maxHeight - widget.childSize.height - widget.padding.bottom) && e.delta.dy > 0) {
                return;
              }
              // 下边
              if (_leftVN.value > (maxWidth - widget.childSize.width - widget.padding.right) && e.delta.dx > 0) {
                return;
              }
              _topVN.value += e.delta.dy;
              _leftVN.value += e.delta.dx;
              // debugPrint("xy:${_leftVN.value},${_topVN.value}");
            },
            child: child,
          )
        );
      }
    );
  }
}
总结
1、此组件封装的核心是通过 AnimatedBuilder 多项监听,拖动同时不断更新 top、left 进而实现拖动效果;
2、此组件不完美之处需要传入悬浮按钮的宽高 childSize,暂时没想到有什么完美的方法可以自动获取到。有知道的同学可以发消息,一起进步,共同成长。

用户评论