概述

#

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

背景

#

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

已引入三个新的 PointerEventPointerPanZoomStartEventPointerPanZoomUpdateEventPointerPanZoomEndEvent。相关的 GestureRecognizer 已更新,以注册对触控板手势序列的兴趣,并将根据触控板上两根或更多手指的移动发出 onDragonPan 和/或 onScale 回调。

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

变更说明

#

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

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

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

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

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 之间的冲突。

以下示例展示了使用 ListenerGestureRecognizer 响应触控板交互的正确方法。

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 会自动处理触控板手势事件,并在识别后触发回调。

使用 GestureRecognizerListener

#

确保从 ListeneronPointerPanZoomStart 传递给每个识别器。必须调用 GestureRecognizeraddPointerPanZoom 方法,以便它表示兴趣并开始跟踪每个触控板手势。

迁移前的代码

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()
    );
  }
}

使用 GestureRecognizerListener

#

升级到 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