前言

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;
  }
}