likes
comments
collection
share

Flutter 实现应用内小窗

作者站长头像
站长
· 阅读数 18

前言

Flutter是一种强大的跨平台移动应用开发框架,允许开发者构建美观且高性能的移动应用。在某些情况下,你可能需要在应用内实现小窗口功能,以改善用户体验或提供一些特定的功能。无论是用于显示通知、音乐播放器控制、或其他创意功能,应用内小窗口都是提高用户体验的有力工具。

本文将介绍如何在Flutter应用内创建小窗口,并展示一个简单的示例。

一般我们对小窗有如下要求:

  1. 跨页面显示
  2. 可以跟随手指拖动
  3. 放开自动侧边吸附

Flutter 实现应用内小窗

代码实现

完整代码

小窗的显示与隐藏

使用OverlayEntry进行跨页面显示

首先定义一个窗口管理器,进行窗口的显示和隐藏

class SmallWindowManager {
  static final SmallWindowManager _instance = SmallWindowManager._();

  factory SmallWindowManager() => _instance;

  SmallWindowManager._();

  ///浮窗
  OverlayEntry? overlayEntry;

  show(BuildContext context) {
    if (overlayEntry == null) {
      overlayEntry = OverlayEntry(builder: (BuildContext context) {
        return SmallWindowWidget(
          top: 160,
          left: 279,
          child: GestureDetector(
            onTap: () {
              hide();
            },
            child: Material(
              child: Container(
                color: Colors.red,
                width: 100,
                height: 200,
                child: const Center(
                  child: Text("small"),
                ),
              ),
            ),
          ),
        );
      });
      Overlay.of(context).insert(overlayEntry!);
    }
  }

  ///关闭小窗
  void hide() {
    overlayEntry?.remove();
    overlayEntry = null;
  }
}

小窗跟随与吸附

  1. 使用Positionedlefttop属性进行定位,通过key确定小窗和窗口的尺寸,判定小窗移动的边界
  2. 使用Gestedrector进行手势操作,手指移动时回调onPanUpdate,通过手指的偏移量计算定位,小窗即可跟随手指移动
  3. 松开手指时回调onPanEnd,计算开始位置和结束位置,执行吸附动画
class SmallWindowWidget extends StatefulWidget {
  final Duration duration;
  final Widget child;
  final double top;
  final double left;

  const SmallWindowWidget({
    super.key,
    this.duration = const Duration(milliseconds: 100),
    required this.child,
    required this.top,
    required this.left,
  });

  @override
  State<SmallWindowWidget> createState() => _SmallWindowWidgetState();
}

class _SmallWindowWidgetState extends State<SmallWindowWidget> with TickerProviderStateMixin {
  AnimationController? _controller;
  double left = 0;
  double top = 0;

  double maxX = 0;
  double maxY = 0;

  var parentKey = GlobalKey();
  var childKey = GlobalKey();

  var parentSize = const Size(0, 0);
  var childSize = const Size(0, 0);

  @override
  void initState() {
    left = widget.left;
    top = widget.top;
    WidgetsBinding.instance.addPostFrameCallback((d) {
      parentSize = getWidgetSize(parentKey);
      childSize = getWidgetSize(childKey);
      maxX = parentSize.width - childSize.width;
      maxY = parentSize.height - childSize.height;
    });
    super.initState();
  }

  @override
  void dispose() {
    _controller?.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      key: parentKey,
      fit: StackFit.expand,
      children: [
        Positioned(
          key: childKey,
          left: left,
          top: top,
          child: GestureDetector(
            onPanUpdate: (d) {
              var delta = d.delta;
              left += delta.dx;
              top += delta.dy;
              setState(() {});
            },
            onPanEnd: (d) {
              left = getValue(left, maxX);
              top = getValue(top, maxY);
              adsorb();
            },
            child: widget.child,
          ),
        )
      ],
    );
  }

  ///限制边界
  double getValue(double value, double max) {
    if (value < 0) {
      return 0;
    } else if (value > max) {
      return max;
    } else {
      return value;
    }
  }

  ///吸附
  void adsorb() {
    bool isLeft = (left + childSize.width / 2) < parentSize.width / 2;
    _controller = AnimationController(vsync: this)..duration = widget.duration;
    var animation = Tween<double>(begin: left, end: isLeft ? 0 : maxX).animate(_controller!);
    animation.addListener(() {
      left = animation.value;
      setState(() {});
    });
    _controller!.forward();
  }

  Size getWidgetSize(GlobalKey key) {
    final RenderBox renderBox = key.currentContext?.findRenderObject() as RenderBox;
    return renderBox.size;
  }
}