【交互 widget】Flutter Listener

大家好,我是17,今天的每日 widget 为大家介绍 Listener。

Listener 调用回调以响应 pointer 事件的。Listener 是底层的 pointer 事件处理,并不涉及到手势。

源码分析

Listener 自身的代码就简单,只是包了一个皮,点击测试的逻辑是它的父类完成的。

代码所在类 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;

hitTest 是 Listener 最重要的逻辑。 result 是测试结果列表,只有添加到 result 里才能响应 pointer 事件。

  1. 判断点击位置是否是 size 内,如果不在就返回 false,不会响应 pointer 事件。
  2. 点击位置如果在 size 内,child 的 点击测试和 自身的点击测试有一个通过即为通过。
  3. behavior == HitTestBehavior.translucent 无论怎样测试都通过,但返回值不变。
  4. behavior == HitTestBehavior.opaque 测试一定会通过。返回值一定为 true。

如果是多 child 的 widdget,hitTest 的逻辑是怎样的呢?

代码所在类 RenderBoxContainerDefaultsMixin

bool defaultHitTestChildren(BoxHitTestResult result, { required Offset position }) {
    ChildType? child = lastChild;
    while (child != null) {
      // The x, y parameters have the top left of the node's box as the origin.
      final ParentDataType childParentData = child.parentData! as ParentDataType;
      final bool isHit = result.addWithPaintOffset(
        offset: childParentData.offset,
        position: position,
        hitTest: (BoxHitTestResult result, Offset transformed) {
          assert(transformed == position - childParentData.offset);
          return child!.hitTest(result, position: transformed);
        },
      );
      if (isHit) {
        return true;
      }
      child = childParentData.previousSibling;
    }
    return false;
  }

在兄弟节点间,前面的节点先绘制,后面的节点后绘制,所以后面的会覆盖前面的,被覆盖的节点是不应该响应点击的,所以从最后一个 child 开始判断,如果 hitTest 通过,也就不用判断前面的了。

使用 Listener

如果不涉及到手势,只是响应 pointer 事件,Listener 再合适不过。

  const Listener({
    super.key,
    this.onPointerDown,
    this.onPointerMove,
    this.onPointerUp,
    this.onPointerHover,
    this.onPointerCancel,
    this.onPointerPanZoomStart,
    this.onPointerPanZoomUpdate,
    this.onPointerPanZoomEnd,
    this.onPointerSignal,
    this.behavior = HitTestBehavior.deferToChild,
    super.child,
  })

虽然响应的事件很多,但其实用起来都一样,我们就拿 onPointerDown 举例吧。需要体验的是 behavior,前面源码中已经分析过,behavior 对 pointer 事件会产生影响。

behavior 的默认值 HitTestBehavior.deferToChild,deferTo 的英文含义是遵从,实际的行为也确实如此,child hitTest 通过,就能响应 pointer 事件,否则没有任何响应。

为了方便看效果,我们自定义一个类。

class MyHitTest extends SingleChildRenderObjectWidget {
  const MyHitTest({super.key, super.child});

  @override
  RenderObject createRenderObject(BuildContext context) {
    return RenderMyHitTest();
  }
}

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

在 hitTest 中直接返回 false,表明 hitTest 失败。我们来测试下,看看 Listener 还能否响应 pointer 事件。

Listener(
      onPointerDown: ((event) {
        print(event);
      }),
      child: MyHitTest(
        child: Container(
          width: 100,
          height: 100,
          color: Colors.blue,
        ),
      ))

现在无论怎么点击都泥牛入海。

我们修改下 hitTest,让他 返回 true。

 bool hitTest(BoxHitTestResult result, {required Offset position}) {
    return true;
  }

现在可以响应点击了。但是 MyHitTest 自身并没有加入到点击列表中,所以自身是不能响应 pointer 事件的。

我们做个实验来验证这一点,override handleEvent ,看看能否接收到 event。

class RenderMyHitTest extends RenderProxyBox {
  @override
  void handleEvent(PointerEvent event, HitTestEntry entry) {
    print(event);
  }

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

结果无法接收到 event,虽然 hitTest 已经成功,但这仅仅是表明上层可以响应 pointer 事件,MyHitTest 自己是不能的。

为了能让 MyHitTest 也能响应 pointer 事件,把它加到列表中就好了。

  @override
  bool hitTest(BoxHitTestResult result, {required Offset position}) {
    result.add(HitTestEntry(this));
    return true;
  }

关于 HitTestBehavior.deferToChild 的效果我们都测试完成了,下面看下 HitTestBehavior.opaque 的效果。因为代码不多,下面给出完整代码。

class MyHitTest extends SingleChildRenderObjectWidget {
  const MyHitTest({super.key, super.child});

  @override
  RenderObject createRenderObject(BuildContext context) {
    return RenderMyHitTest();
  }
}

class RenderMyHitTest extends RenderProxyBox {

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

Listener(
  behavior: HitTestBehavior.opaque,
  onPointerDown: ((event) {
    print(event);
  }),
  child: MyHitTest(
    child: Container(
      width: 100,
      height: 100,
      color: Colors.blue,
    ),
));

HitTestBehavior.opaque 的作用就是:无论 child hitTest 是否成功,自身都 hitTest 成功,可以响应 pointer 事件。

HitTestBehavior.translucent 也能让自身一定响应 pointer 事件,但 hitTest 取决于 children 与自向的测试结果,这个结果会影响上层能否响应 pointer 事件。

© 版权声明
THE END
喜欢就支持一下吧
点赞6 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容