当前位置: 首页 > article >正文

Flutter 仿iOS桌面悬浮球效果

Flutter 仿iOS桌面悬浮球效果

  • 效果图
  • 可拖动的基础按钮
  • 自定义一个可动画展开关闭的路由
  • 使用->创建OverlayEntry
  • demo

效果图

RPReplay_Final1724998086

可拖动的基础按钮

class DraggableFloatingActionButton extends StatefulWidget {
  final Widget child;
  final Size childSize;
  final Offset initialOffset;
  final VoidCallback onPressed;
  final double padding;
  final Function callBack;
  BuildContext parentContext;

  DraggableFloatingActionButton({
    required this.child,
    required this.initialOffset,
    required this.onPressed,
    required this.callBack,
    // required this.parentKey,
    required this.parentContext,
    required this.childSize,
    this.padding = 20,
  });

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

class _DraggableFloatingActionButtonState extends State<DraggableFloatingActionButton>
    with TickerProviderStateMixin, WidgetsBindingObserver {
  //托动按钮使用的Key
  final GlobalKey _childKey = GlobalKey();
  bool _isDragging = false;
  bool _isAnimating = false;
  late Offset _offset;
  late Offset _newOffset;
  late Offset _minOffset;
  late Offset _maxOffset;
  late Size _childSize;
  late Size _parentSize;

  late final AnimationController _controller;

  @override
  void initState() {
    super.initState();
    // 托动按钮的初始位置
    _offset = widget.initialOffset;
    _newOffset = widget.initialOffset;
    _childSize = widget.childSize;
    _parentSize = Size.zero;
    // 添加视图监听
    WidgetsBinding.instance.addObserver(this);
    WidgetsBinding.instance.addPostFrameCallback(_initBoundary);

    _controller = AnimationController(
      duration: const Duration(milliseconds: 250),
      vsync: this,
    );
    _controller.addListener(() {
      debugPrint('status=${_controller.status}');
      if (_controller.status == AnimationStatus.completed) {
        // setState(() {
        //   _controller.stop();
        //   _isAnimating = false;
        // });
      }
    });
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  double _keyboardHeight = 0;

  @override
  void didChangeMetrics() {
    super.didChangeMetrics();
    if (Platform.isAndroid) {
      WidgetsBinding.instance.addPostFrameCallback((_) {
        if (mounted) {
          _changeMenu();
        }
      });
    } else {
      _changeMenu();
    }
  }

  _changeMenu() {
    // 软键盘高度
    double newKeyboardHeight = MediaQuery.of(context).viewInsets.bottom;
    double pageHeight = MediaQuery.of(context).size.height;
    double originH = pageHeight - _offset.dy;

    if (newKeyboardHeight <= _keyboardHeight && originH > newKeyboardHeight) {
      return;
    }
    _keyboardHeight = newKeyboardHeight;

    double botX = (pageHeight - _keyboardHeight) - _childSize.height * 1.2 - 36;

    if (botX <= originH) {
      return;
    }

    Offset keyboard = Offset(_offset.dx, botX);
    _keybordUpdatePosition(keyboard);
    setState(() {
      _isDragging = true;
      _isAnimating = false;
    });
  }

  // 页面第一帧绘制完成后调用
  void _initBoundary(_) {
    // 获取获取组件的 RenderBox
    final RenderBox parentRenderBox = widget.parentContext.findRenderObject() as RenderBox;
    // 获取托动按钮组件的 RenderBox

    try {
      // 分别获取两者的大小 从而计算边界
      final Size parentSize = parentRenderBox.size;
      _parentSize = parentSize;

      setState(() {
        _minOffset = Offset(0, 0);
        _maxOffset = Offset(parentSize.width - widget.childSize.width, parentSize.height - widget.childSize.height);
        debugPrint(
            '全局按钮_initBoundary, _minOffset=$_minOffset, _maxOffset=$_maxOffset, parentSize=$parentSize, size=${widget.childSize}');
      });
    } 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);
      debugPrint(
          '_offset=$_offset, pointerMoveEvent.delta.dx=${pointerMoveEvent.delta.dx}, pointerMoveEvent.delta.dy=${pointerMoveEvent.delta.dy}');
    });
  }

  /// 根据键盘显示计算按钮位置
  void _keybordUpdatePosition(Offset keyboardOffset) {
    double newOffsetX = keyboardOffset.dx;
    double newOffsetY = keyboardOffset.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);
      debugPrint(
          '键盘_offset=$_offset, pointerMoveEvent.delta.dx=${keyboardOffset.dx}, pointerMoveEvent.delta.dy=${keyboardOffset.dy}');
    });
  }

  ///可托动的悬浮按钮
  @override
  Widget build(BuildContext context) {
    if ((!_isDragging && _isAnimating)) {
      _isAnimating = false;

      double childHeight = _childSize.height;
      double childWidth = _childSize.width;

      var beginRect = RelativeRect.fromSize(
        Rect.fromLTWH(_offset.dx, _offset.dy, childWidth, childHeight),
        _parentSize,
      );

      var endRect = RelativeRect.fromSize(
        Rect.fromLTWH(
          _newOffset.dx,
          _newOffset.dy,
          childWidth,
          childHeight,
        ),
        _parentSize,
      );

      final rectAnimation = RelativeRectTween(begin: beginRect, end: endRect).animate(_controller);

      debugPrint('biggest=$_parentSize, beginRect=$beginRect, endRect=$endRect, status=${_controller.status}');
      _offset = _newOffset;

      _controller.reset();
      _controller.forward();

      return PositionedTransition(rect: rectAnimation, child: buildChild());
    }
    return Positioned(left: _offset.dx, top: _offset.dy, child: buildChild());
  }

  ///上次点击时的坐标,与up事件后的坐标比对,如果实际上少于10像素,认为是主动的点击跳转行为(处理三星手机的异常)
  Offset? _lastPositionOffset;

  buildChild() {
    return Listener(
      onPointerDown: (event) {
        _lastPositionOffset = _newOffset;
        _isDragging = false;
        _isAnimating = false;
      },
      onPointerMove: (PointerMoveEvent pointerMoveEvent) {
        //更新位置
        if (pointerMoveEvent.delta.dx != 0 || pointerMoveEvent.delta.dy != 0) {
          _updatePosition(pointerMoveEvent);
          setState(() {
            _isDragging = true;
            _isAnimating = false;
            if (_lastPositionOffset != null) {
              double dx = _newOffset.dx - _lastPositionOffset!.dx;
              double dy = _newOffset.dy - _lastPositionOffset!.dy;
              //已经移动超过10像素,不管
              if ((dx > 10 || dx < -10) || (dy > 10 || dy < -10)) {
                _lastPositionOffset = null;
              }
            }
          });
        }
      },
      onPointerCancel: (event) {
        widget.onPressed();
      },
      onPointerUp: (PointerUpEvent pointerUpEvent) async {
        if (_isDragging) {
          _isDragging = false;

          if (_offset.dx < widget.padding) {
            _isAnimating = true;
            _newOffset = Offset(widget.padding, _offset.dy);
          } else if (_offset.dx >= (_parentSize.width - _childSize.width - widget.padding)) {
            _isAnimating = true;
            _newOffset = Offset(_parentSize.width - _childSize.width - widget.padding, _offset.dy);
          } else {
            if ((_offset.dx + _childSize.width / 2) > _parentSize.width / 2) {
              // 往右靠
              await Future.delayed(Duration(milliseconds: 100));
              _newOffset = Offset(_parentSize.width - _childSize.width - widget.padding, _offset.dy);
              _isAnimating = true;
            } else if ((_offset.dx + _childSize.width / 2) < _parentSize.width / 2) {
              // 往左靠
              await Future.delayed(Duration(milliseconds: 100));
              _newOffset = Offset(widget.padding, _offset.dy);
              _isAnimating = true;
            } else {
              _isAnimating = false;
              _newOffset = _offset;
            }
          }

          if (_offset.dy < kToolbarHeight) {
            _isAnimating = true;
            _newOffset = Offset(_newOffset.dx, kToolbarHeight);
          }
          widget.callBack(_newOffset);
          if (mounted) {
            setState(() {
              ///x+y少于10像素,认为是普通的点击事件
              if (_lastPositionOffset != null) {
                double dx = _newOffset.dx - _lastPositionOffset!.dx;
                double dy = _newOffset.dy - _lastPositionOffset!.dy;
                if ((dx <= 10 && dx >= -10) && (dy <= 10 && dy >= -10)) {
                  widget.onPressed();
                }
              }
            });
          }
        } else {
          widget.onPressed();
        }
      },
      child: Container(
        key: _childKey,
        child: widget.child,
      ),
    );
  }
}

自定义一个可动画展开关闭的路由

class _CusOpenContainerRoute<T> extends ModalRoute<T> {
  _CusOpenContainerRoute({
    required this.closedColor,
    required this.openColor,
    required this.middleColor,
    required double closedElevation,
    required this.openElevation,
    required ShapeBorder closedShape,
    required this.openShape,
    required this.closedBuilder,
    required this.openBuilder,
    required this.hideableKey,
    required this.closedBuilderKey,
    required this.transitionDuration,
    required this.transitionType,
    required this.useRootNavigator,
    required RouteSettings? routeSettings,
  })  : _elevationTween = Tween<double>(
          begin: closedElevation,
          end: openElevation,
        ),
        _shapeTween = ShapeBorderTween(
          begin: closedShape,
          end: openShape,
        ),
        _colorTween = _getColorTween(
          transitionType: transitionType,
          closedColor: closedColor,
          openColor: openColor,
          middleColor: middleColor,
        ),
        _closedOpacityTween = _getClosedOpacityTween(transitionType),
        _openOpacityTween = _getOpenOpacityTween(transitionType),
        super(settings: routeSettings);

  static _FlippableTweenSequence<Color?> _getColorTween({
    required ContainerTransitionType transitionType,
    required Color closedColor,
    required Color openColor,
    required Color middleColor,
  }) {
    switch (transitionType) {
      case ContainerTransitionType.fade:
        return _FlippableTweenSequence<Color?>(
          <TweenSequenceItem<Color?>>[
            TweenSequenceItem<Color>(
              tween: ConstantTween<Color>(closedColor),
              weight: 1 / 5,
            ),
            TweenSequenceItem<Color?>(
              tween: ColorTween(begin: closedColor, end: openColor),
              weight: 1 / 5,
            ),
            TweenSequenceItem<Color>(
              tween: ConstantTween<Color>(openColor),
              weight: 3 / 5,
            ),
          ],
        );
      case ContainerTransitionType.fadeThrough:
        return _FlippableTweenSequence<Color?>(
          <TweenSequenceItem<Color?>>[
            TweenSequenceItem<Color?>(
              tween: ColorTween(begin: closedColor, end: middleColor),
              weight: 1 / 5,
            ),
            TweenSequenceItem<Color?>(
              tween: ColorTween(begin: middleColor, end: openColor),
              weight: 4 / 5,
            ),
          ],
        );
    }
  }

  static _FlippableTweenSequence<double> _getClosedOpacityTween(ContainerTransitionType transitionType) {
    switch (transitionType) {
      case ContainerTransitionType.fade:
        return _FlippableTweenSequence<double>(
          <TweenSequenceItem<double>>[
            TweenSequenceItem<double>(
              tween: Tween<double>(begin: 1.0, end: 0.0),
              weight: 1 / 5,
            ),
            TweenSequenceItem<double>(
              tween: ConstantTween<double>(0.0),
              weight: 4 / 5,
            ),
          ],
        );
      case ContainerTransitionType.fadeThrough:
        return _FlippableTweenSequence<double>(<TweenSequenceItem<double>>[
          TweenSequenceItem<double>(
            tween: Tween<double>(begin: 1.0, end: 0.0),
            weight: 1 / 5,
          ),
          TweenSequenceItem<double>(
            tween: ConstantTween<double>(0.0),
            weight: 4 / 5,
          ),
        ]);
    }
  }

  static _FlippableTweenSequence<double> _getOpenOpacityTween(ContainerTransitionType transitionType) {
    switch (transitionType) {
      case ContainerTransitionType.fade:
        return _FlippableTweenSequence<double>(
          <TweenSequenceItem<double>>[
            TweenSequenceItem<double>(
              tween: ConstantTween<double>(0.0),
              weight: 1 / 5,
            ),
            TweenSequenceItem<double>(
              tween: Tween<double>(begin: 0.0, end: 1.0),
              weight: 1 / 5,
            ),
            TweenSequenceItem<double>(
              tween: ConstantTween<double>(1.0),
              weight: 3 / 5,
            ),
          ],
        );
      case ContainerTransitionType.fadeThrough:
        return _FlippableTweenSequence<double>(
          <TweenSequenceItem<double>>[
            TweenSequenceItem<double>(
              tween: ConstantTween<double>(0.0),
              weight: 1 / 5,
            ),
            TweenSequenceItem<double>(
              tween: Tween<double>(begin: 0.0, end: 1.0),
              weight: 4 / 5,
            ),
          ],
        );
    }
  }

  final Color closedColor;
  final Color openColor;
  final Color middleColor;
  final double openElevation;
  final ShapeBorder openShape;
  final CloseContainerBuilder closedBuilder;
  final CusOpenContainerBuilder<T> openBuilder;

  // See [_CusOpenContainerState._hideableKey].
  final GlobalKey<_HideableState> hideableKey;

  // See [_CusOpenContainerState._closedBuilderKey].
  final GlobalKey closedBuilderKey;

  @override
  final Duration transitionDuration;
  final ContainerTransitionType transitionType;

  final bool useRootNavigator;

  final Tween<double> _elevationTween;
  final ShapeBorderTween _shapeTween;
  final _FlippableTweenSequence<double> _closedOpacityTween;
  final _FlippableTweenSequence<double> _openOpacityTween;
  final _FlippableTweenSequence<Color?> _colorTween;

  static final TweenSequence<Color?> _scrimFadeInTween = TweenSequence<Color?>(
    <TweenSequenceItem<Color?>>[
      TweenSequenceItem<Color?>(
        tween: ColorTween(begin: Colors.transparent, end: Colors.transparent),
        weight: 1 / 5,
      ),
      TweenSequenceItem<Color>(
        tween: ConstantTween<Color>(Colors.transparent),
        weight: 4 / 5,
      ),
    ],
  );
  static final Tween<Color?> _scrimFadeOutTween = ColorTween(
    begin: Colors.transparent,
    end: Colors.transparent,
  );

  // Key used for the widget returned by [CusOpenContainer.openBuilder] to keep
  // its state when the shape of the widget tree is changed at the end of the
  // animation to remove all the craft that was necessary to make the animation
  // work.
  final GlobalKey _openBuilderKey = GlobalKey();

  // Defines the position and the size of the (opening) [CusOpenContainer] within
  // the bounds of the enclosing [Navigator].
  final RectTween _rectTween = RectTween();

  AnimationStatus? _lastAnimationStatus;
  AnimationStatus? _currentAnimationStatus;

  @override
  TickerFuture didPush() {
    _takeMeasurements(navigatorContext: hideableKey.currentContext!);

    animation!.addStatusListener((AnimationStatus status) {
      _lastAnimationStatus = _currentAnimationStatus;
      _currentAnimationStatus = status;
      switch (status) {
        case AnimationStatus.dismissed:
          _toggleHideable(hide: false);
          break;
        case AnimationStatus.completed:
          _toggleHideable(hide: true);
          break;
        case AnimationStatus.forward:
        case AnimationStatus.reverse:
          break;
      }
    });

    return super.didPush();
  }

  @override
  bool didPop(T? result) {
    _takeMeasurements(
      navigatorContext: subtreeContext!,
      delayForSourceRoute: true,
    );
    return super.didPop(result);
  }

  @override
  void dispose() {
    if (hideableKey.currentState?.isVisible == false) {
      // This route may be disposed without dismissing its animation if it is
      // removed by the navigator.
      SchedulerBinding.instance.addPostFrameCallback((Duration d) => _toggleHideable(hide: false));
    }
    super.dispose();
  }

  void _toggleHideable({required bool hide}) {
    if (hideableKey.currentState != null) {
      hideableKey.currentState!
        ..placeholderSize = null
        ..isVisible = !hide;
    }
  }

  void _takeMeasurements({
    required BuildContext navigatorContext,
    bool delayForSourceRoute = false,
  }) {
    final RenderBox navigator = Navigator.of(
      navigatorContext,
      rootNavigator: useRootNavigator,
    ).context.findRenderObject()! as RenderBox;
    final Size navSize = _getSize(navigator);
    _rectTween.end = Offset.zero & navSize;

    void takeMeasurementsInSourceRoute([Duration? _]) {
      if (!navigator.attached || hideableKey.currentContext == null) {
        return;
      }
      _rectTween.begin = _getRect(hideableKey, navigator);
      hideableKey.currentState!.placeholderSize = _rectTween.begin!.size;
    }

    if (delayForSourceRoute) {
      SchedulerBinding.instance.addPostFrameCallback(takeMeasurementsInSourceRoute);
    } else {
      takeMeasurementsInSourceRoute();
    }
  }

  Size _getSize(RenderBox render) {
    assert(render.hasSize);
    return render.size;
  }

  // Returns the bounds of the [RenderObject] identified by `key` in the
  // coordinate system of `ancestor`.
  Rect _getRect(GlobalKey key, RenderBox ancestor) {
    assert(key.currentContext != null);
    assert(ancestor.hasSize);
    final RenderBox render = key.currentContext!.findRenderObject()! as RenderBox;
    assert(render.hasSize);
    return MatrixUtils.transformRect(
      render.getTransformTo(ancestor),
      Offset.zero & render.size,
    );
  }

  bool get _transitionWasInterrupted {
    bool wasInProgress = false;
    bool isInProgress = false;

    switch (_currentAnimationStatus) {
      case AnimationStatus.completed:
      case AnimationStatus.dismissed:
        isInProgress = false;
        break;
      case AnimationStatus.forward:
      case AnimationStatus.reverse:
        isInProgress = true;
        break;
      case null:
        break;
    }
    switch (_lastAnimationStatus) {
      case AnimationStatus.completed:
      case AnimationStatus.dismissed:
        wasInProgress = false;
        break;
      case AnimationStatus.forward:
      case AnimationStatus.reverse:
        wasInProgress = true;
        break;
      case null:
        break;
    }
    return wasInProgress && isInProgress;
  }

  void closeContainer({T? returnValue}) {
    Navigator.of(subtreeContext!).pop(returnValue);
  }

  @override
  Widget buildPage(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
  ) {
    return Align(
      alignment: Alignment.topLeft,
      child: AnimatedBuilder(
        animation: animation,
        builder: (BuildContext context, Widget? child) {
          if (animation.isCompleted) {
            return SizedBox.expand(
              child: Material(
                color: openColor,
                elevation: openElevation,
                shape: openShape,
                type: MaterialType.transparency,
                child: Builder(
                  key: _openBuilderKey,
                  builder: (BuildContext context) {
                    return openBuilder(context, closeContainer);
                  },
                ),
              ),
            );
          }

          final Animation<double> curvedAnimation = CurvedAnimation(
            parent: animation,
            curve: Curves.fastOutSlowIn,
            reverseCurve: _transitionWasInterrupted ? null : Curves.fastOutSlowIn.flipped,
          );
          TweenSequence<Color?>? colorTween;
          TweenSequence<double>? closedOpacityTween, openOpacityTween;
          Animatable<Color?>? scrimTween;
          switch (animation.status) {
            case AnimationStatus.dismissed:
            case AnimationStatus.forward:
              closedOpacityTween = _closedOpacityTween;
              openOpacityTween = _openOpacityTween;
              colorTween = _colorTween;
              scrimTween = _scrimFadeInTween;
              break;
            case AnimationStatus.reverse:
              if (_transitionWasInterrupted) {
                closedOpacityTween = _closedOpacityTween;
                openOpacityTween = _openOpacityTween;
                colorTween = _colorTween;
                scrimTween = _scrimFadeInTween;
                break;
              }
              closedOpacityTween = _closedOpacityTween.flipped;
              openOpacityTween = _openOpacityTween.flipped;
              colorTween = _colorTween.flipped;
              scrimTween = _scrimFadeOutTween;
              break;
            case AnimationStatus.completed:
              assert(false); // Unreachable.
          }
          assert(colorTween != null);
          assert(closedOpacityTween != null);
          assert(openOpacityTween != null);
          assert(scrimTween != null);

          final Rect rect = _rectTween.evaluate(curvedAnimation)!;
          return SizedBox.expand(
            child: Container(
              color: scrimTween!.evaluate(curvedAnimation),
              child: Align(
                alignment: Alignment.topLeft,
                child: Transform.translate(
                  offset: Offset(rect.left, rect.top),
                  child: SizedBox(
                    width: rect.width,
                    height: rect.height,
                    child: Material(
                      clipBehavior: Clip.antiAlias,
                      animationDuration: Duration.zero,
                      type: MaterialType.transparency,
                      color: colorTween!.evaluate(animation),
                      shape: _shapeTween.evaluate(curvedAnimation),
                      elevation: _elevationTween.evaluate(curvedAnimation),
                      child: Stack(
                        fit: StackFit.passthrough,
                        children: <Widget>[
                          // Closed child fading out.
                          FittedBox(
                            fit: BoxFit.fitWidth,
                            alignment: Alignment.center,
                            child: SizedBox(
                              width: _rectTween.begin!.width,
                              height: _rectTween.begin!.height,
                              child: (hideableKey.currentState?.isInTree ?? false)
                                  ? null
                                  : FadeTransition(
                                      opacity: closedOpacityTween!.animate(animation),
                                      child: Builder(
                                        key: closedBuilderKey,
                                        builder: (BuildContext context) {
                                          // Use dummy "open container" callback
                                          // since we are in the process of opening.
                                          return closedBuilder(context, () {});
                                        },
                                      ),
                                    ),
                            ),
                          ),

                          // Open child fading in.
                          FittedBox(
                            fit: BoxFit.fitWidth,
                            alignment: Alignment.topLeft,
                            child: SizedBox(
                              width: _rectTween.end!.width,
                              height: _rectTween.end!.height,
                              child: FadeTransition(
                                opacity: openOpacityTween!.animate(animation),
                                child: Builder(
                                  key: _openBuilderKey,
                                  builder: (BuildContext context) {
                                    return openBuilder(context, closeContainer);
                                  },
                                ),
                              ),
                            ),
                          ),
                        ],
                      ),
                    ),
                  ),
                ),
              ),
            ),
          );
        },
      ),
    );
  }

  @override
  bool get maintainState => true;

  @override
  Color? get barrierColor => null;

  @override
  bool get opaque => false;

  @override
  bool get barrierDismissible => false;

  @override
  String? get barrierLabel => null;
}

使用->创建OverlayEntry

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

const double BALL_SIZE = 104;

class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {
  int _counter = 0;

  var workBallOffset = Offset(ScreenUtil().screenWidth - 28.w - BALL_SIZE.w,
          ScreenUtil().screenHeight - BALL_SIZE.w - 228.w)
      .obs;
  OverlayEntry? _windowBall;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    WidgetsBinding.instance.addObserver(this);

    WidgetsBinding.instance.addPostFrameCallback((Value) {
      insertWorkBall();
    });
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
    );
  }

  OverlayEntry buildWorkbenchEntry() {
    return OverlayEntry(builder: (context) {
      VoidCallback? open;
      return Stack(
        children: [
          DraggableFloatingActionButton(
            child: CusOpenContainer(
              transitionType: ContainerTransitionType.fade,
              openBuilder: (BuildContext context, VoidCallback _) {
                return WindowBall();
              },
              closedElevation: 0,
              closedShape: const RoundedRectangleBorder(
                borderRadius: BorderRadius.all(
                  Radius.circular((BALL_SIZE / 2)),
                ),
              ),
              closedColor: Theme.of(context).colorScheme.secondary,
              openColor: Colors.transparent,
              closedBuilder:
                  (BuildContext context, VoidCallback openContainer) {
                open = openContainer;
                return BallBtn();
              },
            ),
            callBack: (offset) {
              debugPrint('callback -->${offset}');
              workBallOffset.value = offset;
            },
            initialOffset: workBallOffset.value,
            // parentKey: _parentKey,
            parentContext: context,
            childSize: Size(BALL_SIZE.w, BALL_SIZE.w),
            onPressed: () {
              open?.call();
            },
          ),

          // ),
        ],
      );
    });
  }

  insertWorkBall() {
    if (_windowBall == null) {
      _windowBall = buildWorkbenchEntry();
      Overlay.of(context).insert(_windowBall!);
    }
  }

  // 移除工作台悬浮窗
  removeWorkBall() {
    if (_windowBall != null) {
      _windowBall?.remove();
      _windowBall = null;
    }
  }
}

demo

完整代码查看demo


http://www.kler.cn/a/290302.html

相关文章:

  • NCC前端调用查询弹框
  • DAY6 线程
  • 案例精选 | 河北省某检察院安全运营中异构日志数据融合的实践探索
  • GitLab实现 HTTP 访问和 SMTP 邮件发送
  • c/c++--struct对比
  • 网络安全技术在能源领域的应用
  • 【数学建模备赛】Ep07:灰色预测模型
  • 随手笔记【五】
  • 【扇贝编程】使用Selenium模拟浏览器获取动态内容笔记
  • AI证件照生成神器颠覆传统,轻松驾驭考研、考公与签证申请
  • PHP + Redis 实现抽奖算法(ThinkPHP5)
  • Spring6梳理6——依赖注入之Setter注入
  • 【drools】Rulesengine构建及intelj配置
  • 通过组合Self-XSS + CSRF得到存储型XSS
  • 跨境电商代购系统中前台基本功能介绍:帮助更快的了解跨境代购业务
  • 注册登陆(最新版)
  • IOS 18 发现界面(UITableView)Banner轮播图实现
  • 【话题】提升开发效率的秘密武器:探索高效编程工具
  • SpinalHDL之BlackBox(下篇)
  • C#如何使用外部别名Extern alias
  • 单向链表与双向链表
  • 8逻辑回归的代价函数
  • HTTP与TCP的关系是什么?HTTP 的端口有什么意义?
  • ComfyUI SDXL Prompt Styler 简介
  • Android Studio Koala下载并安装,测试helloworld.
  • 惠中科技:以 RDS 光伏自清洁技术开启光伏电站新未来