Flutter拖动widget并吸附侧方

639 阅读2分钟

背景

由于活动Icon一直固定在一侧会产生视线遮挡,期望能由用户自行拖动并在松手之后吸附在屏幕的一侧

实现方式一

import 'package:flutter/material.dart';

class DragBoxWidget extends StatefulWidget {
  /// 可拖动的Widget
  final Widget child;

  /// 可拖动的Widget宽度
  final double width;

  /// 可拖动的Widget高度
  final double height;

  /// 父级组件宽度
  final double parentWidth;

  /// 父级组件高度
  final double parentHeight;

  /// 初始化X轴
  final double offsetX;

  /// 初始化Y轴
  final double offsetY;

  /// 点击事件
  final GestureTapCallback onTap;

  const DragBoxWidget(
      {Key? key,
      required this.child,
      required this.width,
      required this.height,
      required this.parentWidth,
      required this.parentHeight,
      required this.offsetX,
      required this.offsetY,
      required this.onTap})
      : super(key: key);

  @override
  _DragBoxWidgetState createState() => _DragBoxWidgetState(
      child, width, height, parentWidth, parentHeight, offsetX, offsetY, onTap);
}

class _DragBoxWidgetState extends State<DragBoxWidget> {
  final Widget child;
  final double width;
  final double height;
  final double parentWidth;
  final double parentHeight;
  final double offsetX;
  final double offsetY;
  final GestureTapCallback onTap;
  Offset offset = const Offset(0, 0);

  _DragBoxWidgetState(this.child, this.width, this.height, this.parentWidth,
      this.parentHeight, this.offsetX, this.offsetY, this.onTap);

  @override
  void initState() {
    offset = Offset(offsetX, offsetY);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Positioned(
      left: offset.dx,
      top: offset.dy,
      child: GestureDetector(
          onPanUpdate: (detail) {
            setState(() {
              offset = _calOffset(offset, detail.delta);
            });
          },
          onTap: onTap,
          onPanEnd: (detail) {
            setState(() {
              // 移动位置小于父组件宽度一半,吸附在左侧
              if (offset.dx < parentWidth / 2 - width / 2) {
                offset = Offset(0, offset.dy);
              } else {
                // 移动位置大于父组件宽度一半,吸附在右侧
                offset = Offset(parentWidth - width, offset.dy);
              }
            });
          },
          child: child),
    );
  }

  /// 计算偏移量
  Offset _calOffset(Offset offset, Offset nextOffset) {
    double dx = 0;
    // 水平方向偏移量不能小于0不能大于父级组件宽度
    if (offset.dx + nextOffset.dx <= 0) {
      dx = 0;
    } else if (offset.dx + nextOffset.dx >= (parentWidth - width)) {
      dx = parentWidth - width;
    } else {
      dx = offset.dx + nextOffset.dx;
    }
    double dy = 0;
    // 垂直方向偏移量不能小于0不能大于父级组件高度
    if (offset.dy + nextOffset.dy >= (parentHeight - height)) {
      dy = parentHeight - height;
    } else if (offset.dy + nextOffset.dy <= 0) {
      dy = 0;
    } else {
      dy = offset.dy + nextOffset.dy;
    }
    return Offset(
      dx,
      dy,
    );
  }
}

实现方式二

import 'package:flutter/material.dart';

/// 覆盖悬浮窗视图
class OverlayUtil {
  // 用来保存OverlayEntry
  static OverlayEntry? _holder;

  // 要显示的视图
  static Widget? _view;

  // 左边距
  static double _left = 0;

  // 右边距
  static double _right = 0;

  // 左右分界线的X坐标
  static double _borderlineX = 0;

  // 视图高度
  static double _viewHeight = 0;

  // 当前顶部边距
  static double _curTop = 0;

  // 当前左边距
  static double? _curLeft;

  // 当前右边距
  static double? _curRight = 0;

  // 拖动开始回调
  static VoidCallback? _onDragStarted;

  // 拖动结束回调
  static DragEndCallback? _onDragEnd;

  // 是否初始化
  static bool _isInit = true;

  /// 显示悬浮窗
  static void show(
      {required BuildContext context,
      required Widget view,
      required double viewWidth,
      required double viewHeight,
      Offset initOffset = const Offset(800, 1200),
      double left = 0,
      double right = 0,
      double? borderlineX,
      VoidCallback? onDragStarted,
      DragEndCallback? onDragEnd}) {
    _left = left;
    _right = right;
    _borderlineX =
        borderlineX ?? (MediaQuery.of(context).size.width - viewWidth) / 2;
    _view = view;
    _viewHeight = viewHeight;
    _onDragStarted = onDragStarted;
    _onDragEnd = onDragEnd;
    if (_isInit) {
      _isInit = false;
      _calculatePosition(initOffset, context);
    }
    _remove();
    // 创建一个OverlayEntry对象
    OverlayEntry overlayEntry = OverlayEntry(
      builder: (context) {
        return Positioned(
          top: _curTop,
          left: _curLeft,
          right: _curRight,
          child: _buildDraggable(context),
        );
      },
    );
    // 往Overlay中插入插入OverlayEntry
    Overlay.of(context).insert(overlayEntry);
    _holder = overlayEntry;
  }

  /// 销毁悬浮窗
  static void dispose() {
    _isInit = true;
    _remove();
  }

  /// 移除悬浮窗
  static void _remove() {
    _holder?.remove();
    _holder = null;
    _onDragStarted = null;
    _onDragEnd = null;
  }

  /// 创建可拖动的视图
  static _buildDraggable(BuildContext context) {
    if (_view == null) {
      return;
    }

    return Draggable(
      feedback: _view!,
      onDragStarted: () {
        // 拖动开始
        _onDragStarted?.call();
      },
      onDragEnd: (detail) {
        // 拖动结束
        _createDragTarget(offset: detail.offset, context: context);
        _onDragEnd?.call(detail);
      },
      childWhenDragging: const SizedBox.shrink(),
      child: _view!,
    );
  }

  /// 计算位置
  static void _calculatePosition(Offset offset, BuildContext context) {
    bool isLeft = offset.dx < _borderlineX;
    double maxY = MediaQuery.of(context).size.height - _viewHeight;
    _curTop = offset.dy < 0 ? 0 : (offset.dy < maxY ? offset.dy : maxY);
    _curLeft = isLeft ? _left : null;
    _curRight = isLeft ? null : _right;
  }

  /// 创建拖动目标
  static void _createDragTarget(
      {required Offset offset, required BuildContext context}) {
    _remove();
    _holder = OverlayEntry(
      builder: (context) {
        _calculatePosition(offset, context);
        return Positioned(
          top: _curTop,
          left: _curLeft,
          right: _curRight,
          child: DragTarget(
            builder: (BuildContext context, List incoming, List rejected) {
              return _buildDraggable(context);
            },
          ),
        );
      },
    );
    Overlay.of(context).insert(_holder!);
  }
}