Flutter自定义叠加层

时间:2019-05-13 12:28:41

标签: 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(),
        ),
      ),
    );
  }
}

因此,我该如何拾取和计算相对于整个屏幕的当前点击点,以便做出覆盖方向的决定。 Sample Overlay

1 个答案:

答案 0 :(得分:0)

OverlayEntry确实是实现您提到的目标的不错选择。您可以使用GestureRecognizer包装小部件,该小部件应在长按时创建此叠加层。此小部件可以使用许多回调,这些回调也包含LongPressStartDetails之类的位置。在那里,您可以根据OverlayEntry和设备大小(globalPosition)计算MediaQuery.of(context).size的位置和大小。

提示:要删除OverlayEntry,您必须在remove()小部件本身上调用OverlayEntry!因此,请保留对此的引用。

详细答案

在对评论的回复进行一些修补之后,我提出了以下解决方案。

我使用了一个 Stack Widget ,该容器的最大宽度和高度都包装在 GestureDetector 中,以检测实际窗口小部件之外的所有点击。点击此容器会调用_overlayEntry.remove();关闭叠加层。

然后我将Tap Widget的距离与Screen Center进行了比较,以确定Overlay应该在其上方还是下方。

我最终使用了一个 Positioned Widget (定位的小部件),其 top 被计算为适合所选的重叠式广告位置。最终,我将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);
  }
}

结果 Result