【Flutter】HitTestBehavior想点哪里点哪里

1,353 阅读4分钟

我正在参加「掘金·启航计划

点击事件响应

点击组件中的HitTestBehavior属性支持三个值:opaquetranslucentdeferToChild。其在命中测试起到一定作用可改变原有命中逻辑从而是实现不同点击触发事件。

HitTestBehavior属性值

HitTestBehaviorRenderProxyBoxWithHitTestBehavior有具体实现应用场景。

/flutter/packages/flutter/lib/src/rendering/proxy_box.dart:RenderProxyBoxWithHitTestBehavior

  @override
  bool hitTest(BoxHitTestResult result, { required Offset position }) {
    bool hitTarget = false;
    if (size.contains(position)) {
      hitTarget = hitTestChildren(result, position: position) || hitTestSelf(position); // 若behavior 等于HitTestBehavior.opaque可命中
      if (hitTarget || behavior == HitTestBehavior.translucent) { //  若behavior等于HitTestBehavior.translucent可命中
        result.add(BoxHitTestEntry(this, position));
      }
    }
    return hitTarget;
  }

  @override
  bool hitTestSelf(Offset position) => behavior == HitTestBehavior.opaque;

在检查子节点命中情况时判断是否opaque,在检查自身命中时判断translucent,因此可知HitTestBehavior.opaque>HitTestBehavior.translucent>HitTestBehavior.deferToChild

由于基础组件对于hitTesthitTestChildrenfalse因此存在将事件消费情况(例如SizedBox无子节点情况下默认false)。为了验证HitTestBehavior可行性就需要自定义一个组件重写hitTesthitTestChildren方法。这里以ColoredBox为基础自定义组件重写_RenderColoredBox的命中判定默认都为false

  @override
  bool hitTest(BoxHitTestResult result, {required Offset position}) {
    return false;
  }

  @override
  bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
    return false;
  }

Stack中设置两个重叠子节点:

  1. 默认情况下点击组件behaviorHitTestBehavior.deferToChild根据点击组件子节点命中测试要求而定
  2. 当为HitTestBehavior.deferToChild情况下,点击事件由最上层子节点响应
  3. 当为HitTestBehavior.translucent情况下,点击事件都透传所有子节点都响应
  4. 当为HitTestBehavior.deferToChild情况下,点击事件无响应(因为自定义_ColoredBoxWithNoHitTest忽略了命中测试)
Stack(
      alignment: Alignment.center,
      children: [
        Listener(
            behavior: value,
            onPointerDown: (down) {
              showSnackBarMsg(context, 'onPointerDown -> Listener -> 外',
                  clear: false, duration: const Duration(milliseconds500));
            },
            child: _ColoredBoxWithNoHitTest(
              color: Colors.red.withOpacity(0.5),
              child: const SizedBox(
                height200,
                width250,
              ),
            )),
        Listener(
          behavior: value,
          onPointerDown: (down) {
            showSnackBarMsg(context, 'onPointerDown -> Listener -> 内',
                clear: false, duration: const Duration(milliseconds500));
          },
          child: _ColoredBoxWithNoHitTest(
            color: Colors.red.withOpacity(0.5),
            child: const SizedBox(
              height100,
              width150,
            ),
          ),
        ),
      ],
    );

在使用HitTestBehavior也需要注意到所作用节点是否支持忽略命中,因为很多情况下有些组件默认情况下实现hitTesttrue状态。

实战举例

一个布局实现如下:最外层Container设置边框可视化点击区域,内部子节点是带有GestureDetectorContainer无边框无背景色其内部子节点有ImageText等。

Container(
      decoration: buildBoxDecorationBorder(),
      child: GestureDetector(
        // behavior: HitTestBehavior.deferToChild, // 设置后点击空白区域无响应
        behavior: HitTestBehavior.translucent,  // 设置后点击空白区域有响应
        onTap: () {
          showSnackBarMsg(context, 'onTap -> GestureDetector -> Container');
        },
        child: Container(
          alignment: Alignment.center,
          height150,
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              Image.asset('images/img_640_640.jpg', width50),
              const Text(
                "文本区域\n"
                "文本区域\n"
                "文本区域\n"
                "文本区域\n",
                style:
                    TextStyle(color: Colors.white, backgroundColor: Colors.red),
              ),
              ColoredBox(
                color: Colors.red.withOpacity(0.5),
                child: const SizedBox(
                  width150,
                  height100,
                  child: Center(
                    child: Text(
                      "behavior是opaque或translucent\n点击空白区域才能响应",
                      style: TextStyle(color: Colors.white),
                    ),
                  ),
                ),
              )
            ],
          ),
        ),
      ),
    );
  1. 默认情况下点击子节点Container内部子组件可以响应点击事件
  2. 点击子节点Container内部空白区域无法响应点击事件
  3. 修改GestureDetectorbehaviorHitTestBehavior.translucent点击边框内任务区域都能响应点击事件

分析缘由

Container是复合组件由多种其他组件嵌套而成,例如配置color会嵌套ColoredBox,增加边框decoration会嵌套DecoratedBox

ColoredBox属性

ColoredBox内部实现_RenderColoredBox,它的命中测试逻辑是由RenderProxyBoxWithHitTestBehavior判断

/flutter/packages/flutter/lib/src/rendering/proxy_box.dart:RenderProxyBoxWithHitTestBehavior

  @override
  bool hitTest(BoxHitTestResult result, { required Offset position }) {
    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;
  }

  @override
  bool hitTestSelf(Offset position) => behavior == HitTestBehavior.opaque;

是否命中测试以hitTestChildren作为判断依据,因为默认情况下behaviordeferToChild。一般情况而言子节点命中测试都是true,所以有ColorContainer是一般是命中测试的。
非也非也阅读_RenderColoredBox源码可知默认情况下behaviorHitTestBehavior.opaque状态,因此触摸组件监听时可以直接通过命中测试。

/flutter/packages/flutter/lib/src/widgets/basic.dart:_RenderColoredBox

class _RenderColoredBox extends RenderProxyBoxWithHitTestBehavior {
  _RenderColoredBox({ required Color color })
    : _color = color,
      super(behavior: HitTestBehavior.opaque);
      ...
}

DecoratedBox属性

DecoratedBox内部实现了RenderDecoratedBoxhitTestSelf方法实现由BoxDecoration接管,从内部实现看命中测试逻辑可知

/flutter/packages/flutter/lib/src/painting/box_decoration.dart:BoxDecoration

  @override
  bool hitTest(Size size, Offset position, { TextDirection? textDirection }) {
    ...
    switch (shape) {
      case BoxShape.rectangle: // 矩形求边框是否在范围内
        if (borderRadius != null) {
          final RRect bounds = borderRadius!.resolve(textDirection).toRRect(Offset.zero & size);
          return bounds.contains(position);
        }
        return true;
      case BoxShape.circle: // 圆形求半径是否在命中范围内
        final Offset center = size.center(Offset.zero);
        final double distance = (position - center).distance;
        return distance <= math.min(size.width, size.height) / 2.0;
    }
  }

因此设置了边框的Container只要在边框范围内命中测试都是true

小总结

如上所知当Container没有设置ColorDecoratedBox属性时若要让Container整体命中测试就必须为点击组件设置为translucent或者opaque,相反则只能响应Container内部子节点事件。