跳至主要内容

触控板手势可以触发 GestureRecognizer

摘要

#

大多数平台上的触控板手势现在会发送 PointerPanZoom 序列,并且可以触发平移、拖动和缩放 GestureRecognizer 回调。

上下文

#

在 Flutter 桌面版 3.3.0 之前的版本中,滚动使用 `PointerScrollEvent` 消息来表示离散的滚动增量。此系统对于鼠标滚轮效果很好,但并不适合触控板滚动。触控板滚动预期会产生动量,这不仅取决于滚动增量,还取决于手指从触控板上抬起的时间。此外,触控板捏合缩放无法表示。

引入了三个新的 `PointerEvent`:`PointerPanZoomStartEvent`、`PointerPanZoomUpdateEvent` 和 `PointerPanZoomEndEvent`。相关的 `GestureRecognizer` 已更新以注册对触控板手势序列的兴趣,并将响应触控板上两个或更多手指的移动发出 `onDrag`、`onPan` 和/或 `onScale` 回调。

这意味着,仅针对触摸交互设计的代码可能会在触控板交互时触发,而旨在处理所有桌面滚动的代码现在可能仅在鼠标滚动时触发,而不是触控板滚动时触发。

更改说明

#

Flutter 引擎已在所有可能的平台上更新,以识别触控板手势并将它们作为 `PointerPanZoom` 事件而不是 `PointerScrollSignal` 事件发送到框架。`PointerScrollSignal` 事件仍将用于表示鼠标滚轮上的滚动。

根据平台和特定的触控板型号,如果平台 API 未向 Flutter 引擎提供足够的数据,则可能不会使用新系统。这包括 Windows,其中触控板手势支持依赖于触控板的驱动程序,以及 Web 平台,其中浏览器 API 未提供足够的数据,并且触控板滚动必须仍然使用旧的 `PointerScrollSignal` 系统。

开发人员应准备好接收这两种类型的事件,并确保其应用程序或软件包以适当的方式处理它们。

`Listener` 现在有三个新的回调:`onPointerPanZoomStart`、`onPointerPanZoomUpdate` 和 `onPointerPanZoomEnd`,可用于观察触控板滚动和缩放事件。

dart
void main() => runApp(Foo());

class Foo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Listener(
      onPointerSignal: (PointerSignalEvent event) {
        if (event is PointerScrollEvent) {
          debugPrint('mouse scrolled ${event.scrollDelta}');
        }
      },
      onPointerPanZoomStart: (PointerPanZoomStartEvent event) {
        debugPrint('trackpad scroll started');
      },
      onPointerPanZoomUpdate: (PointerPanZoomUpdateEvent event) {
        debugPrint('trackpad scrolled ${event.panDelta}');
      },
      onPointerPanZoomEnd: (PointerPanZoomEndEvent event) {
        debugPrint('trackpad scroll ended');
      },
      child: Container()
    );
  }
}

`PointerPanZoomUpdateEvent` 包含一个 `pan` 字段来表示当前手势的累积平移,一个 `panDelta` 字段来表示自上次事件以来平移的变化,一个 `scale` 事件来表示当前手势的累积缩放,以及一个 `rotation` 事件来表示当前手势的累积旋转(以弧度为单位)。

`GestureRecognizer` 现在有方法来处理来自一个连续触控板手势的所有触控板事件。在 `GestureRecognizer` 上使用 `PointerPanZoomStartEvent` 调用 `addPointerPanZoom` 方法将导致识别器注册其对该触控板交互的兴趣,并解决可能响应手势的多个 `GestureRecognizer` 之间的冲突。

以下示例显示了使用 `Listener` 和 `GestureRecognizer` 响应触控板交互的正确方法。

dart
void main() => runApp(Foo());

class Foo extends StatefulWidget {
  late final PanGestureRecognizer recognizer;

  @override
  void initState() {
    super.initState();
    recognizer = PanGestureRecognizer()
    ..onStart = _onPanStart
    ..onUpdate = _onPanUpdate
    ..onEnd = _onPanEnd;
  }

  void _onPanStart(DragStartDetails details) {
    debugPrint('onStart');
  }

  void _onPanUpdate(DragUpdateDetails details) {
    debugPrint('onUpdate');
  }

  void _onPanEnd(DragEndDetails details) {
    debugPrint('onEnd');
  }

  @override
  Widget build(BuildContext context) {
    return Listener(
      onPointerDown: recognizer.addPointer,
      onPointerPanZoomStart: recognizer.addPointerPanZoom,
      child: Container()
    );
  }
}

当使用 `GestureDetector` 时,这是自动完成的,因此以下示例之类的代码将响应触摸和平移触控板发出其手势更新回调。

dart
void main() => runApp(Foo());

class Foo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onPanStart: (details) {
        debugPrint('onStart');
      },
      onPanUpdate: (details) {
        debugPrint('onUpdate');
      },
      onPanEnd: (details) {
        debugPrint('onEnd');
      }
      child: Container()
    );
  }
}

迁移指南

#

迁移步骤取决于您是否希望应用程序中的每个手势交互都可通过触控板使用,或者是否应将其限制为仅触摸和鼠标使用。

适用于适合触控板使用的交互手势

#

使用 `GestureDetector`

#

无需更改,`GestureDetector` 自动处理触控板手势事件并在识别时触发回调。

使用 `GestureRecognizer` 和 `Listener`

#

确保 `onPointerPanZoomStart` 从 `Listener` 传递到每个识别器。必须为 `GestureRecognizer` 调用 `addPointerPanZoom` 方法,以使其显示兴趣并开始跟踪每个触控板手势。

迁移前的代码

dart
void main() => runApp(Foo());

class Foo extends StatefulWidget {
  late final PanGestureRecognizer recognizer;

  @override
  void initState() {
    super.initState();
    recognizer = PanGestureRecognizer()
    ..onStart = _onPanStart
    ..onUpdate = _onPanUpdate
    ..onEnd = _onPanEnd;
  }

  void _onPanStart(DragStartDetails details) {
    debugPrint('onStart');
  }

  void _onPanUpdate(DragUpdateDetails details) {
    debugPrint('onUpdate');
  }

  void _onPanEnd(DragEndDetails details) {
    debugPrint('onEnd');
  }

  @override
  Widget build(BuildContext context) {
    return Listener(
      onPointerDown: recognizer.addPointer,
      child: Container()
    );
  }
}

迁移后的代码

dart
void main() => runApp(Foo());

class Foo extends StatefulWidget {
  late final PanGestureRecognizer recognizer;

  @override
  void initState() {
    super.initState();
    recognizer = PanGestureRecognizer()
    ..onStart = _onPanStart
    ..onUpdate = _onPanUpdate
    ..onEnd = _onPanEnd;
  }

  void _onPanStart(DragStartDetails details) {
    debugPrint('onStart');
  }

  void _onPanUpdate(DragUpdateDetails details) {
    debugPrint('onUpdate');
  }

  void _onPanEnd(DragEndDetails details) {
    debugPrint('onEnd');
  }

  @override
  Widget build(BuildContext context) {
    return Listener(
      onPointerDown: recognizer.addPointer,
      onPointerPanZoomStart: recognizer.addPointerPanZoom,
      child: Container()
    );
  }
}

使用原始 `Listener`

#

以下使用 `PointerScrollSignal` 的代码将不再在所有桌面滚动时被调用。应捕获 `PointerPanZoomUpdate` 事件以接收触控板手势数据。

迁移前的代码

dart
void main() => runApp(Foo());

class Foo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Listener(
      onPointerSignal: (PointerSignalEvent event) {
        if (event is PointerScrollEvent) {
          debugPrint('scroll wheel event');
        }
      }
      child: Container()
    );
  }
}

迁移后的代码

dart
void main() => runApp(Foo());

class Foo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Listener(
      onPointerSignal: (PointerSignalEvent event) {
        if (event is PointerScrollEvent) {
          debugPrint('scroll wheel event');
        }
      },
      onPointerPanZoomUpdate: (PointerPanZoomUpdateEvent event) {
        debugPrint('trackpad scroll event');
      }
      child: Container()
    );
  }
}

请注意:以这种方式使用原始 `Listener` 可能会导致与其他手势交互冲突,因为它不参与手势消歧领域。

适用于不适合触控板使用的交互手势

#

使用 `GestureDetector`

#

如果使用 Flutter 3.3.0,可以使用 `RawGestureDetector` 代替 `GestureDetector`,以确保 `GestureDetector` 创建的每个 `GestureRecognizer` 的 `supportedDevices` 设置为排除 `PointerDeviceKind.trackpad`。从 3.4.0 版开始,`GestureDetector` 上直接有一个 `supportedDevices` 参数。

迁移前的代码

dart
void main() => runApp(Foo());

class Foo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onPanStart: (details) {
        debugPrint('onStart');
      },
      onPanUpdate: (details) {
        debugPrint('onUpdate');
      },
      onPanEnd: (details) {
        debugPrint('onEnd');
      }
      child: Container()
    );
  }
}

迁移后的代码 (Flutter 3.3.0)

dart
// Example of code after the change.
void main() => runApp(Foo());

class Foo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RawGestureDetector(
      gestures: {
        PanGestureRecognizer:
            GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
          () => PanGestureRecognizer(
            supportedDevices: {
              PointerDeviceKind.touch,
              PointerDeviceKind.mouse,
              PointerDeviceKind.stylus,
              PointerDeviceKind.invertedStylus,
              // Do not include PointerDeviceKind.trackpad
            }
          ),
          (recognizer) {
            recognizer
              ..onStart = (details) {
                debugPrint('onStart');
              }
              ..onUpdate = (details) {
                debugPrint('onUpdate');
              }
              ..onEnd = (details) {
                debugPrint('onEnd');
              };
          },
        ),
      },
      child: Container()
    );
  }
}

迁移后的代码:(Flutter 3.4.0)

dart
void main() => runApp(Foo());

class Foo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      supportedDevices: {
        PointerDeviceKind.touch,
        PointerDeviceKind.mouse,
        PointerDeviceKind.stylus,
        PointerDeviceKind.invertedStylus,
        // Do not include PointerDeviceKind.trackpad
      },
      onPanStart: (details) {
        debugPrint('onStart');
      },
      onPanUpdate: (details) {
        debugPrint('onUpdate');
      },
      onPanEnd: (details) {
        debugPrint('onEnd');
      }
      child: Container()
    );
  }
}

使用 `RawGestureRecognizer`

#

明确确保 `supportedDevices` 不包含 `PointerDeviceKind.trackpad`。

迁移前的代码

dart
void main() => runApp(Foo());

class Foo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RawGestureDetector(
      gestures: {
        PanGestureRecognizer:
            GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
          () => PanGestureRecognizer(),
          (recognizer) {
            recognizer
              ..onStart = (details) {
                debugPrint('onStart');
              }
              ..onUpdate = (details) {
                debugPrint('onUpdate');
              }
              ..onEnd = (details) {
                debugPrint('onEnd');
              };
          },
        ),
      },
      child: Container()
    );
  }
}

迁移后的代码

dart
// Example of code after the change.
void main() => runApp(Foo());

class Foo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RawGestureDetector(
      gestures: {
        PanGestureRecognizer:
            GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
          () => PanGestureRecognizer(
            supportedDevices: {
              PointerDeviceKind.touch,
              PointerDeviceKind.mouse,
              PointerDeviceKind.stylus,
              PointerDeviceKind.invertedStylus,
              // Do not include PointerDeviceKind.trackpad
            }
          ),
          (recognizer) {
            recognizer
              ..onStart = (details) {
                debugPrint('onStart');
              }
              ..onUpdate = (details) {
                debugPrint('onUpdate');
              }
              ..onEnd = (details) {
                debugPrint('onEnd');
              };
          },
        ),
      },
      child: Container()
    );
  }
}

使用 `GestureRecognizer` 和 `Listener`

#

升级到 Flutter 3.3.0 后,行为不会发生变化,因为必须在每个 `GestureRecognizer` 上调用 `addPointerPanZoom` 以允许它跟踪手势。当滚动触控板时,以下代码将不会接收平移手势回调

dart
void main() => runApp(Foo());

class Foo extends StatefulWidget {
  late final PanGestureRecognizer recognizer;

  @override
  void initState() {
    super.initState();
    recognizer = PanGestureRecognizer()
    ..onStart = _onPanStart
    ..onUpdate = _onPanUpdate
    ..onEnd = _onPanEnd;
  }

  void _onPanStart(DragStartDetails details) {
    debugPrint('onStart');
  }

  void _onPanUpdate(DragUpdateDetails details) {
    debugPrint('onUpdate');
  }

  void _onPanEnd(DragEndDetails details) {
    debugPrint('onEnd');
  }

  @override
  Widget build(BuildContext context) {
    return Listener(
      onPointerDown: recognizer.addPointer,
      // recognizer.addPointerPanZoom is not called
      child: Container()
    );
  }
}

时间轴

#

包含在版本中:3.3.0-0.0.pre
稳定版本:3.3.0

参考文献

#

API 文档

设计文档

相关问题

相关 PR