Flutter手势命中拦截插件flutter_gesture_hit_intercept的使用

Flutter手势命中拦截插件flutter_gesture_hit_intercept的使用

flutter_gesture_hit_intercept

Flutter 实现命中拦截, 用于解决手势冲突, 阻止其它小部件获取手势事件。

使用场景: 比如在ListView中, 有一个tile想要获取手势事件, 同时又想要ListView阻止滚动, 这时候就可以使用这个库。

screenshot

安装

安装最新版本:

flutter pub add flutter_gesture_hit_intercept

快速开始

导入对应的包, 使用 GestureHitInterceptScope 小部件包裹 ListView, 然后在需要拦截手势的RenderBox上使用 GestureHitInterceptScope.of(context)?.interceptHitBox = this 方法即可。

[@override](/user/override)
Widget build(BuildContext context) {
  return MaterialApp(
    home: Scaffold(
      body: GestureHitInterceptScope(
        child: ListView(
          physics: const AlwaysScrollableScrollPhysics(
            parent: BouncingScrollPhysics(),
          ),
          children: [
            const GestureTestWidget(), // 测试小部件
            buildItem(context),
            ...
          ],
        ),
      ),
    ),
  );
}

示例代码

以下是一个完整的示例代码,展示了如何使用 flutter_gesture_hit_intercept 插件来实现手势命中拦截。

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_gesture_hit_intercept/flutter_gesture_hit_intercept.dart';

void main() {
  runApp(const MainApp());
}

class MainApp extends StatelessWidget {
  const MainApp({super.key});

  Widget buildItem(BuildContext context) {
    return GestureDetector(
      onTap: () {
        debugPrint('点击了');
      },
      child: Container(
        height: 100,
        color: Colors.transparent,
        child: Stack(
          children: [
            const Text('在此区域的手势不会阻止`ListView`的滚动'),
            Align(
                alignment: Alignment.center,
                child: SizedBox(
                  width: 100,
                  height: 40,
                  child: FilledButton(
                    onPressed: () {
                      debugPrint('点击了');
                    },
                    child: const Text("按钮"),
                  ),
                )),
          ],
        ),
      ),
    );
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: GestureHitInterceptScope(
          child: ListView(
            physics: const AlwaysScrollableScrollPhysics(
              parent: BouncingScrollPhysics(),
            ),
            children: [
              const GestureTestWidget(),
              buildItem(context),
              for (var i = 0; i < 100; i++)
                if (i % 3 == 0)
                  const GestureTestWidget()
                else
                  buildItem(context),
            ],
          ),
        ),
      ),
    );
  }
}

class GestureTestWidget extends LeafRenderObjectWidget {
  const GestureTestWidget({super.key});

  [@override](/user/override)
  RenderObject createRenderObject(BuildContext context) {
    return GestureTestBox(context);
  }
}

Color randomColor({int min = 120, int max = 200}) => Color.fromARGB(
      255,
      nextInt(max, min: min),
      nextInt(max, min: min),
      nextInt(max, min: min),
    );

int nextInt(int max, {int min = 0}) => min + Random().nextInt(max);

class GestureTestBox extends RenderBox {
  /// 用来保存所有的手指事件
  final pointerMap = <int, PointerEvent>{};

  /// 用来保存所有的手指颜色
  final pointerColorMap = <int, Color>{};

  BuildContext context;

  GestureTestBox(this.context);

  Color getPointerColor(int pointer) {
    return pointerColorMap.putIfAbsent(pointer, () => randomColor());
  }

  [@override](/user/override)
  void performLayout() {
    size = Size(constraints.maxWidth, constraints.maxWidth / 2);
  }

  /// 如果为true, 会影响[PointerEvent.localPosition]位置信息
  [@override](/user/override)
  bool get isRepaintBoundary => false;

  [@override](/user/override)
  void paint(PaintingContext context, Offset offset) {
    //context.canvas.drawColor(Colors.redAccent, BlendMode.src);
    final canvas = context.canvas;

    canvas.drawRect(
      Rect.fromLTWH(
        paintBounds.left + offset.dx,
        paintBounds.top + offset.dy,
        paintBounds.width,
        paintBounds.height,
      ),
      Paint()..color = Colors.grey,
    );

    TextPainter(
      text: TextSpan(
          text: "在此区域的手势会阻止`ListView`的滚动\n${pointerMap.length}",
          style: const TextStyle(
            color: Colors.white,
            fontWeight: FontWeight.bold,
            fontSize: 12,
          )),
      textDirection: TextDirection.ltr,
    )
      ..layout(maxWidth: paintBounds.width)
      ..paint(canvas, offset);

    //绘制map
    const radius = 30.0;
    final paint = Paint()
      ..strokeWidth = 1
      ..style = PaintingStyle.stroke;

    //debugger();
    pointerMap.forEach((key, pointer) {
      paint.color = getPointerColor(key);
      canvas.drawLine(Offset(offset.dx, pointer.localPosition.dy),
          Offset(offset.dx + size.width, pointer.localPosition.dy), paint);
      canvas.drawLine(Offset(pointer.localPosition.dx, offset.dy),
          Offset(pointer.localPosition.dx, offset.dy + size.height), paint);
      canvas.drawCircle(pointer.localPosition, radius, paint);
    });
  }

  [@override](/user/override)
  bool hitTestSelf(Offset position) {
    return true;
  }

  /// 只有命中通过之后, 才会回调事件
  /// [GestureBinding.handlePointerEvent] -> [GestureBinding._handlePointerEventImmediately] -> [HitTestResult.addWithPaintTransform]
  [@override](/user/override)
  void handleEvent(PointerEvent event, covariant BoxHitTestEntry entry) {
    //debugger();

    final hitInterceptBox = GestureHitInterceptScope.of(context);
    hitInterceptBox?.interceptHitBox = this;
    if (!event.synthesized) {
      pointerMap[event.pointer] = event;
    }

    if (event is PointerUpEvent || event is PointerCancelEvent) {
      pointerMap.remove(event.pointer);
    }
    markNeedsPaint();
    super.handleEvent(event, entry);
  }
}

更多关于Flutter手势命中拦截插件flutter_gesture_hit_intercept的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter手势命中拦截插件flutter_gesture_hit_intercept的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


flutter_gesture_hit_intercept 是一个用于处理Flutter中手势命中的插件,它允许你在多个手势识别器之间进行命中测试的拦截和处理。这在一些复杂的UI交互场景中非常有用,特别是当你有重叠的Widget,并且需要控制哪个Widget应该优先响应手势事件时。

安装

首先,你需要在 pubspec.yaml 文件中添加依赖:

dependencies:
  flutter:
    sdk: flutter
  flutter_gesture_hit_intercept: ^1.0.0  # 请使用最新版本

然后运行 flutter pub get 来安装依赖。

基本使用

flutter_gesture_hit_intercept 提供了一个 GestureHitIntercept 组件,它可以包裹在你的Widget树中,允许你控制手势的命中测试。

import 'package:flutter/material.dart';
import 'package:flutter_gesture_hit_intercept/flutter_gesture_hit_intercept.dart';

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Gesture Hit Intercept Example'),
      ),
      body: GestureHitIntercept(
        onIntercept: (PointerEvent event, GlobalKey key) {
          // 在这里处理命中拦截逻辑
          print('Intercepted gesture at ${event.position}');
          return true; // 返回 true 表示拦截手势
        },
        child: Center(
          child: Container(
            width: 200,
            height: 200,
            color: Colors.blue,
            child: GestureDetector(
              onTap: () {
                print('Container tapped');
              },
              child: Center(
                child: Text('Tap Me'),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

参数解释

  • onIntercept: 一个回调函数,当手势事件发生时,这个回调会被调用。它接收两个参数:PointerEventGlobalKey。你可以在这里决定是否要拦截手势。如果返回 true,手势将被拦截,不会传递给子Widget;如果返回 false,手势将继续传递给子Widget。

  • child: 你要包裹的Widget树。

示例场景

假设你有两个重叠的Widget,一个是一个大背景按钮,另一个是一个小的覆盖按钮。你希望当用户点击小的覆盖按钮时,只有小的按钮响应点击事件,而背景按钮不响应。你可以使用 GestureHitIntercept 来实现这个逻辑。

GestureHitIntercept(
  onIntercept: (event, key) {
    if (key == smallButtonKey) {
      return false; // 不拦截小按钮的点击事件
    }
    return true; // 拦截背景按钮的点击事件
  },
  child: Stack(
    children: [
      GestureDetector(
        key: backgroundButtonKey,
        onTap: () {
          print('Background button tapped');
        },
        child: Container(
          width: 300,
          height: 300,
          color: Colors.green,
        ),
      ),
      Positioned(
        top: 100,
        left: 100,
        child: GestureDetector(
          key: smallButtonKey,
          onTap: () {
            print('Small button tapped');
          },
          child: Container(
            width: 100,
            height: 100,
            color: Colors.red,
          ),
        ),
      ),
    ],
  ),
)
回到顶部