Flutter 自定义叠加层

Isa*_*lla 3 flutter flutter-layout

我正在开发一个需要根据点击坐标定位的自定义叠加层的应用程序。然后,覆盖层应填充该位置的其余空间。填充空间方向基于从单击点到顶部或底部的距离,使用距离较大的方向,并且应在其表面外部单击时关闭。

我已经查看了Overlay Widget,但是我似乎不明白如何使用它来实现所描述的属性。

到目前为止,我已经创建了一个 StatefulWidget,其中包含 Overlay 描述。

   class ScriptureDisplay extends StatefulWidget {
  @override
  _ScriptureDisplayState createState() => _ScriptureDisplayState();
}

class _ScriptureDisplayState extends State<ScriptureDisplay> {

  OverlayEntry _overlayEntry;


  OverlayEntry _createOverlayEntry() {
    RenderBox renderBox = context.findRenderObject();
    var size = renderBox.size;
    var offset = renderBox.localToGlobal(Offset.zero);

    return OverlayEntry(
        builder: (context) => Positioned(
              left: offset.dx,
              top: offset.dy + size.height + 5.0,
              width: size.width,
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  nip(),
                  body(context, offset.dy),
                ],
              ),
            ));
  }

  @override
  Widget build(BuildContext context) {

  return RaisedButton(
    child: Text("Click Me"),
    onPressed: (){
      this._overlayEntry = this._createOverlayEntry();
      Overlay.of(context).insert(this._overlayEntry);
    },
  );
  }

  Widget body(BuildContext context, double offset) {
    return Material(
      borderRadius: BorderRadius.all(
        Radius.circular(8.0),
      ),
//      color: this.color,
      elevation: 4.0,
      child: Container(
        width: MediaQuery.of(context).size.width,
        padding: const EdgeInsets.symmetric(horizontal: 8.0),
        child: ListView(
          padding: EdgeInsets.zero,
          shrinkWrap: true,
          children: ["First", "Second", "Third"]
              .map((String s) => ListTile(
                    subtitle: Text(s),
                  ))
              .toList(growable: false),
        ),
      ),
    );
  }

  Widget nip() {
    return FittedBox(
      child: Container(
        height: 20.0,
        width: 20.0,
        margin: EdgeInsets.only(left:10),
        child: CustomPaint(
          painter: OpenPainter(),
        ),
      ),
    );
  }
}
Run Code Online (Sandbox Code Playgroud)

那么我如何拾取并计算相对于整个屏幕的当前点击点,以便我可以决定覆盖方向。 示例叠加

kou*_*nex 6

OverlayEntry确实是实现你提到的一个不错的选择。您可以包装您的小部件,它应该在例如长按时创建此覆盖,并带有GestureRecognizer. 许多回调可以与这个小部件一起使用,它也包含像这样的位置LongPressStartDetailsOverlayEntry您可以在那里计算基于globalPosition设备尺寸 ( )的位置和尺寸MediaQuery.of(context).size

提示:要删除OverlayEntry您必须调用remove()OverlayEntry部件本身!所以保留对它的引用。

详细解答

在对评论中的回复进行一些拼凑之后,我想出了以下解决方案。

我使用了一个Stack Widget,其初始容器的最大宽度和高度包裹在GestureDetector中,以检测实际小部件之外的任何点击。单击此容器将调用_overlayEntry.remove();来关闭叠加层。

然后,我将点击小部件与屏幕中心的距离进行比较,以确定叠加层应该位于其上方还是下方。

我最终使用了一个Positioned Widget ,其顶部经过计算以适合所选的 Overlay 位置。最后我计算了覆盖体高度为从水龙头到屏幕末端的距离。

enum OVERLAY_POSITION { TOP, BOTTOM }

class ScriptureDisplay extends StatefulWidget {
  @override
  _ScriptureDisplayState createState() => _ScriptureDisplayState();
}

class _ScriptureDisplayState extends State<ScriptureDisplay> {
  TapDownDetails _tapDownDetails;
  OverlayEntry _overlayEntry;
  OVERLAY_POSITION _overlayPosition;

  double _statusBarHeight;
  double _toolBarHeight;

  OverlayEntry _createOverlayEntry() {
    RenderBox renderBox = context.findRenderObject();

    var size = renderBox.size;

    var offset = renderBox.localToGlobal(Offset.zero);
    var globalOffset = renderBox.localToGlobal(_tapDownDetails.globalPosition);

    _statusBarHeight = MediaQuery.of(context).padding.top;

    // TODO: Calculate ToolBar Height Using MediaQuery
    _toolBarHeight = 50;
    var screenHeight = MediaQuery.of(context).size.height;

    var remainingScreenHeight = screenHeight - _statusBarHeight - _toolBarHeight;

    if (globalOffset.dy > remainingScreenHeight / 2) {
      _overlayPosition = OVERLAY_POSITION.TOP;
    } else {
      _overlayPosition = OVERLAY_POSITION.BOTTOM;
    }
    return OverlayEntry(builder: (context) {
      return Stack(
        children: <Widget>[
          GestureDetector(
            onTap: () {
              _overlayEntry.remove();
            },
            child: Container(
              width: MediaQuery.of(context).size.width,
              height: MediaQuery.of(context).size.height,
              color: Colors.blueGrey.withOpacity(0.1),
            ),
          ),
          Positioned(
            left: 10,
            top: _overlayPosition == OVERLAY_POSITION.TOP
                ? _statusBarHeight + _toolBarHeight
                : offset.dy + size.height - 5.0,
            width: MediaQuery.of(context).size.width - 20,
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                // ignore: sdk_version_ui_as_code
                if (_overlayPosition == OVERLAY_POSITION.BOTTOM)
                  nip(),
                body(context, offset.dy),
                // ignore: sdk_version_ui_as_code
                if (_overlayPosition == OVERLAY_POSITION.TOP)
                  nip(),
              ],
            ),
          )
        ],
      );
    });
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.only(top: 400, left: 10, right: 100),
      child: GestureDetector(
        child: Text("C"),
        onTapDown: (TapDownDetails tapDown) {
          setState(() {
            _tapDownDetails = tapDown;
          });
          this._overlayEntry = this._createOverlayEntry();
          Overlay.of(context).insert(this._overlayEntry);
        },
      ),
    );
  }

  Widget body(BuildContext context, double offset) {
    return Material(
      borderRadius: BorderRadius.all(
        Radius.circular(8.0),
      ),
      elevation: 4.0,
      child: Container(
        width: MediaQuery.of(context).size.width,
        height: _overlayPosition == OVERLAY_POSITION.BOTTOM
            ? MediaQuery.of(context).size.height -
                _tapDownDetails.globalPosition.dy -
                20
            : _tapDownDetails.globalPosition.dy -
                _toolBarHeight -
                _statusBarHeight -
                15,
        padding: const EdgeInsets.symmetric(horizontal: 8.0),
        child: ListView(
          padding: EdgeInsets.zero,
          shrinkWrap: true,
          children: [
            "First",
            "Second",
            "Third",
            "First",
            "Second",
            "Third",
            "First",
            "Second",
            "Third"
          ]
              .map((String s) => ListTile(
                    subtitle: Text(s),
                  ))
              .toList(growable: false),
        ),
      ),
    );
  }

  Widget nip() {
    return Container(
      height: 10.0,
      width: 10.0,
      margin: EdgeInsets.only(left: _tapDownDetails.globalPosition.dx),
      child: CustomPaint(
        painter: OpenPainter(_overlayPosition),
      ),
    );
  }
}

class OpenPainter extends CustomPainter {
  final OVERLAY_POSITION overlayPosition;

  OpenPainter(this.overlayPosition);

  @override
  void paint(Canvas canvas, Size size) {
    switch (overlayPosition) {
      case OVERLAY_POSITION.TOP:
        var paint = Paint()
          ..style = PaintingStyle.fill
          ..color = Colors.white
          ..isAntiAlias = true;

        _drawThreeShape(canvas,
            first: Offset(0, 0),
            second: Offset(20, 0),
            third: Offset(10, 15),
            size: size,
            paint: paint);

        break;
      case OVERLAY_POSITION.BOTTOM:
        var paint = Paint()
          ..style = PaintingStyle.fill
          ..color = Colors.white
          ..isAntiAlias = true;

        _drawThreeShape(canvas,
            first: Offset(15, 0),
            second: Offset(0, 20),
            third: Offset(30, 20),
            size: size,
            paint: paint);

        break;
    }

    canvas.save();
    canvas.restore();
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => false;

  void _drawThreeShape(Canvas canvas,
      {Offset first, Offset second, Offset third, Size size, paint}) {
    var path1 = Path()
      ..moveTo(first.dx, first.dy)
      ..lineTo(second.dx, second.dy)
      ..lineTo(third.dx, third.dy);
    canvas.drawPath(path1, paint);
  }

  void _drawTwoShape(Canvas canvas,
      {Offset first, Offset second, Size size, paint}) {
    var path1 = Path()
      ..moveTo(first.dx, first.dy)
      ..lineTo(second.dx, second.dy);
    canvas.drawPath(path1, paint);
  }
}
Run Code Online (Sandbox Code Playgroud)

结果 结果