触控板手势可触发 GestureRecognizer
概述
#大多数平台上的触控板手势现在会发送 PointerPanZoom
序列,并可以触发平移、拖动和缩放 GestureRecognizer
回调。
背景
#在 Flutter 3.3.0 版本之前的 Flutter 桌面版中,滚动使用的是 PointerScrollEvent
消息来表示离散的滚动距离。这个系统对于鼠标滚轮效果很好,但对于触控板滚动来说并不理想。触控板滚动应具有惯性,这不仅取决于滚动距离,还取决于手指从触控板上抬起的时间。此外,触控板捏合缩放也无法表示。
引入了三个新的 PointerEvent
:PointerPanZoomStartEvent
、PointerPanZoomUpdateEvent
和 PointerPanZoomEndEvent
。相关的 GestureRecognizer
已更新,以注册对触控板手势序列的兴趣,并将在检测到触控板上的两个或多个手指移动时,发出 onDrag
、onPan
和/或 onScale
回调。
这意味着,仅为触摸交互设计的代码可能会在触控板交互时触发,而为处理所有桌面滚动设计的代码现在可能仅在鼠标滚动时触发,而在触控板滚动时不再触发。
变更说明
#Flutter 引擎已在所有可能的平台上更新,以识别触控板手势,并将它们作为 PointerPanZoom
事件发送到框架,而不是作为 PointerScrollSignal
事件。PointerScrollSignal
事件仍将用于表示鼠标滚轮上的滚动。
根据平台和具体的触控板型号,如果平台 API 提供的 Flutter 引擎数据不足,则可能不会使用新系统。这包括 Windows(触控板手势支持取决于触控板驱动程序)和 Web 平台(浏览器 API 提供的数据不足,触控板滚动仍需使用旧的 PointerScrollSignal
系统)。
开发者应准备好接收这两种类型的事件,并确保他们的应用程序或软件包以适当的方式处理它们。
Listener
现在有三个新的回调:onPointerPanZoomStart
、onPointerPanZoomUpdate
和 onPointerPanZoomEnd
,可用于观察触控板滚动和缩放事件。
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
上调用 addPointerPanZoom
方法并传入 PointerPanZoomStartEvent
,将使识别器注册其对该触控板交互的兴趣,并解决可能响应该手势的多个 GestureRecognizer
之间的冲突。
以下示例展示了如何正确使用 Listener
和 GestureRecognizer
来响应触控板交互。
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
时,这是自动完成的,因此像下面的示例这样的代码将响应触摸和平移触控板手势来发出其手势更新回调。
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
方法,才能使其显示兴趣并开始跟踪每个触控板手势。
迁移前的代码
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()
);
}
}
迁移后的代码
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
事件以接收触控板手势数据。
迁移前的代码
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()
);
}
}
迁移后的代码
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
参数。
迁移前的代码
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)
// 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)
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
。
迁移前的代码
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()
);
}
}
迁移后的代码
// 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
才能使其跟踪手势。当滚动触控板时,以下代码将不会收到平移手势回调。
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