在 Flutter 中,定位的 Widget 如何感受到其父 Stack 区域之外的点击?

问题描述 投票:0回答:7

A

Stack
MyWidget
中包含
Positioned

Stack(
  overflow: Overflow.visible,
  children: [
    Positioned(
    top: 0.0,
    left: 0.0,
    child: MyWidget(),
  )],
);

由于溢出是

Overflow.visible
并且
MyWidget
Stack
大,所以它显示在
Stack
之外,这就是我想要的。

但是,我无法点击

MyWidget
区域之外的
Stack
区域。它只是忽略那里的水龙头。

如何确保

MyWidget
接受那里的手势?

flutter gesture flutter-layout
7个回答
17
投票

发生此行为是因为堆栈在检查子级是否被击中之前检查指针是否在其边界内:

类:RenderBox(RenderStack 扩展)

bool hitTest(BoxHitTestResult result, { @required Offset position }) {

    ...

    if (_size.contains(position)) {
      if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
        result.add(BoxHitTestEntry(this, position));
        return true;
      }
    }
    return false;
}

我的解决方法是删除

if (_size.contains(position))

检查。 不幸的是,如果不从框架复制代码,这是不可能的。

这就是我所做的:

  • 复制 Stack 类并将其命名为 Stack2
  • 复制 RenderStack 并将其命名为 RenderStack2
  • 使 Stack2 参考 RenderStack2
  • 添加了上面的 hitTest 方法,没有 _size.contains 检查
  • 复制 Positioned 并将其命名为 Positioned2 并使其引用 Stack2 作为其泛型参数
  • 在我的代码中使用了 Stack2 和 Positioned2

此解决方案绝不是最佳解决方案,但它实现了所需的行为。


13
投票

我也有类似的问题。基本上,由于堆栈的子级不使用完全溢出的框大小进行命中测试,因此我使用了嵌套堆栈和任意大的高度,以便我可以捕获嵌套堆栈的溢出框的点击。不确定它是否适合你,但这里什么也没有:)

所以在你的例子中也许你可以尝试类似的事情

Stack(
  clipBehavior: Clip.none,
  children: [
    Positioned(
    top: 0.0,
    left: 0.0,
    height : 500.0 // biggest possible child size or just very big 
    child: Stack(
      children: [MyWidget()]
    ),
  )],
);

7
投票

可以考虑使用继承来复制

hitTest
方法来打破命中规则,例子

class Stack2 extends Stack {
  Stack2({
    Key key,
    AlignmentGeometry alignment = AlignmentDirectional.topStart,
    TextDirection textDirection,
    StackFit fit = StackFit.loose,
    Overflow overflow = Overflow.clip,
    List<Widget> children = const <Widget>[],
  }) : super(
          key: key,
          alignment: alignment,
          textDirection: textDirection,
          fit: fit,
          overflow: overflow,
          children: children,
        );

  @override
  RenderStack createRenderObject(BuildContext context) {
    return RenderStack2(
      alignment: alignment,
      textDirection: textDirection ?? Directionality.of(context),
      fit: fit,
      overflow: overflow,
    );
  }
}

class RenderStack2 extends RenderStack {
  RenderStack2({
    List<RenderBox> children,
    AlignmentGeometry alignment = AlignmentDirectional.topStart,
    TextDirection textDirection,
    StackFit fit = StackFit.loose,
    Overflow overflow = Overflow.clip,
  }) : super(
          children: children,
          alignment: alignment,
          textDirection: textDirection,
          fit: fit,
          overflow: overflow,
        );

  @override
  bool hitTest(BoxHitTestResult result, {Offset position}) {
    if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
      result.add(BoxHitTestEntry(this, position));
      return true;
    }
    return false;
  }
}

6
投票

好吧,我对此做了一个解决方法,基本上我在父级上添加了一个

GestureDetector
并实现了
onTapDown
。 此外,您还必须使用
Widget
来跟踪您的
GlobalKey
以获得当前位置。

当检测到父级别的

Tap
时,检查点击位置是否在您的小部件内部。

以下代码:

final GlobalKey key = new GlobalKey();

      void onTapDown(BuildContext context, TapDownDetails details) {
        final RenderBox box = context.findRenderObject();
        final Offset localOffset = box.globalToLocal(details.globalPosition);
        final RenderBox containerBox = key.currentContext.findRenderObject();
        final Offset containerOffset = containerBox.localToGlobal(localOffset);
        final onTap = containerBox.paintBounds.contains(containerOffset);
        if (onTap){
          print("DO YOUR STUFF...");
        }
      }

      @override
      Widget build(BuildContext context) {
        return GestureDetector(
          onTapDown: (TapDownDetails details) => onTapDown(context, details),
          child: Container(
            color: Colors.red,
            width: MediaQuery.of(context).size.width,
            height: MediaQuery.of(context).size.height,
            child: Align(
              alignment: Alignment.topLeft,
                      child: SizedBox(
                width: 200.0,
                height: 400.0,
                child: Container(
                  color: Colors.black,
                    child: Stack(
                      overflow: Overflow.visible,
                      children: [
                        Positioned(
                          top: 0.0, left: 0.0,
                                          child: Container(
                            key: key,
                            width: 500.0,
                            height: 200.0,
                            color: Colors.blue,
                          ),
                        ),
                      ],
                    ),

                ),
              ),
            ),
          ),
        );
      } 

4
投票

有时你可以使用

Column
来构建一个特殊的
Stack

重点:

  1. 垂直方向向上。
  2. 向下变换顶部小部件。

下面是我的代码,你可以复制测试一下:

Column(
  verticalDirection: VerticalDirection.up,
  children: [
    Container(
      width: 200,
      height: 100,
      color: Colors.red,
    ),
    Transform.translate(
      offset: const Offset(0, 30),
      child: GestureDetector(
        onTap: () {
          print('tap orange view');
        },
        child: Container(
          width: 60,
          height: 60,
          color: Colors.orange,
        ),
      ),
    ),
  ],
),

2
投票

可以通过使用

OverlayEntry
小部件作为
Stack
的父级来解决此限制(因为
OverlayEntry
填满整个屏幕,所有子级也都经过了命中测试)。这是 DartPad 上的概念验证解决方案

创建一个返回 Future 的自定义小部件:

    Widget build(BuildContext context) {
    Future(showOverlay);
    return Container();
  }

这个未来应该删除

OverlayEntry
的任何先前实例,并使用您的自定义小部件插入
Stack

  void showOverlay() {
    hideOverlay();
    RenderBox? renderBox = context.findAncestorRenderObjectOfType<RenderBox>();

    var parentSize = renderBox!.size;
    var parentPosition = renderBox.localToGlobal(Offset.zero);

    overlay = _overlayEntryBuilder(parentPosition, parentSize);
    Overlay.of(context)!.insert(overlay!);
  }

  void hideOverlay() {
    overlay?.remove();
  }

使用构建器函数生成

Stack
:

 OverlayEntry _overlayEntryBuilder(Offset parentPosition, Size parentSize) {  
    return OverlayEntry(
      maintainState: false,
      builder: (context) {
        return Stack(
          clipBehavior: Clip.none,
          children: [
            Positioned(
              left: parentPosition.dx + parentSize.width,
              top: parentPosition.dy + parentSize.height,
              child: Material(
                color: Colors.transparent,
                child: InkWell(
                  onTap: () {},
                  child: Container(),
                ),
              ),
            ),
          ],
        );
      },
    );
  }

-1
投票

我写了一个容器来解决这个问题,实现的不是很漂亮,但是可以使用,并且做了代码封装,方便使用。

这是工具:

import 'package:flutter/widgets.dart';

/// Creates a widget that can check its' overflow children's hitTest
///
/// [overflowKeys] is must, and there should be used on overflow widget's outermost widget those' sizes cover the overflow child, because it will [hitTest] its' children, but not [hitTest] its' parents. And i cannot found a way to check RenderBox's parent in flutter.
///
/// The [OverflowWithHitTest]'s size must contains the overflow widgets, so you can use it as outer as possible.
///
/// This will not reduce rendering performance, because it only overcheck the given widgets marked by [overflowKeys].
///
/// Demo:
///
/// class MyPage extends State<UserCenterPage> {
///
///   var overflowKeys = <GlobalKey>[GlobalKey()];
///
///   Widget build(BuildContext context) {
///     return Scaffold(
///       body: OverflowWithHitTest(
///
///         overflowKeys: overflowKeys,
///
///         child: Container(
///           height: 50,
///           child: UnconstrainedBox(
///             child: Container(
///               width: 200,
///               height: 50,
///               color: Colors.red,
///               child: OverflowBox(
///                 alignment: Alignment.topLeft,
///                 minWidth: 100,
///                 maxWidth: 200,
///                 minHeight: 100,
///                 maxHeight: 200,
///                 child: GestureDetector(
///                   key: overflowKeys[0],
///                   behavior: HitTestBehavior.translucent,
///                   onTap: () {
///                     print('==== onTap;');
///                   },
///                   child: Container(
///                     color: Colors.blue,
///                     height: 200,
///                     child: Text('aaaa'),
///                   ),
///                 ),
///               ),
///             ),
///           ),
///         ),
///       ),
///     );
///   }
/// }
///
///
class OverflowWithHitTest extends SingleChildRenderObjectWidget {
  const OverflowWithHitTest({
    required this.overflowKeys,
    Widget? child,
    Key? key,
  }) : super(key: key, child: child);

  final List<GlobalKey> overflowKeys;

  @override
  _OverflowWithHitTestBox createRenderObject(BuildContext context) {
    return _OverflowWithHitTestBox(overflowKeys: overflowKeys);
  }

  @override
  void updateRenderObject(
      BuildContext context, _OverflowWithHitTestBox renderObject) {
    renderObject.overflowKeys = overflowKeys;
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(
        DiagnosticsProperty<List<GlobalKey>>('overflowKeys', overflowKeys));
  }
}

class _OverflowWithHitTestBox extends RenderProxyBoxWithHitTestBehavior {
  _OverflowWithHitTestBox({required List<GlobalKey> overflowKeys})
      : _overflowKeys = overflowKeys,
        super(behavior: HitTestBehavior.translucent);

  /// Global keys of overflow children
  List<GlobalKey> get overflowKeys => _overflowKeys;
  List<GlobalKey> _overflowKeys;

  set overflowKeys(List<GlobalKey> value) {
    var changed = false;

    if (value.length != _overflowKeys.length) {
      changed = true;
    } else {
      for (var ind = 0; ind < value.length; ind++) {
        if (value[ind] != _overflowKeys[ind]) {
          changed = true;
        }
      }
    }
    if (!changed) {
      return;
    }
    _overflowKeys = value;
    markNeedsPaint();
  }

  @override
  bool hitTest(BoxHitTestResult result, {required Offset position}) {
    if (hitTestOverflowChildren(result, position: position)) {
      result.add(BoxHitTestEntry(this, position));
      return true;
    }
    bool hitTarget = false;
    if (size.contains(position)) {
      hitTarget =
          hitTestChildren(result, position: position) || hitTestSelf(position);
      if (hitTarget || behavior == HitTestBehavior.translucent)
        result.add(BoxHitTestEntry(this, position));
    }
    return hitTarget;
  }

  bool hitTestOverflowChildren(BoxHitTestResult result,
      {required Offset position}) {
    if (overflowKeys.length == 0) {
      return false;
    }
    var hitGlobalPosition = this.localToGlobal(position);
    for (var child in overflowKeys) {
      if (child.currentContext == null) {
        continue;
      }
      var renderObj = child.currentContext!.findRenderObject();
      if (renderObj == null || renderObj is! RenderBox) {
        continue;
      }

      var localPosition = renderObj.globalToLocal(hitGlobalPosition);
      if (renderObj.hitTest(result, position: localPosition)) {
        return true;
      }
    }
    return false;
  }
}

© www.soinside.com 2019 - 2024. All rights reserved.