触控板手势可以触发 GestureRecognizer
概述
#大多数平台上的触控板手势现在会发送 PointerPanZoom
序列,并可以触发平移 (pan)、拖动 (drag) 和缩放 (scale) 的 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
上使用 PointerPanZoomStartEvent
调用 addPointerPanZoom
方法将使识别器注册对该触控板交互的兴趣,并解决可能响应此手势的多个 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
#确保从 Listener
将 onPointerPanZoomStart
传递给每个识别器。必须调用 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