颤动的悬停式触摸处理 - 无需抬起手指即可检测来自不同小部件的触摸
flutter hover-like touch handling - detect touch from different widgets without lifting the finger
假设我在屏幕上有 3 个 Container
可以通过改变颜色来响应触摸。当用户的手指放在它们上面时,它们应该改变颜色,当触摸末端时它们应该恢复正常。 我想要的是那些容器在用户 finger/pointer 对它们做出反应时即使触摸是在屏幕上容器外的随机区域开始的。 所以基本上是什么我正在寻找类似于 css 悬停的东西。
如果我用 GestureDetector
单独包装每个容器,它们将不会对在它们之外开始的触摸事件做出反应。在另一个问题上(不幸的是我现在没有 link),建议用一个 GestureDetector
包装所有容器并为每个容器分配一个 GlobalKey
以使它们彼此不同。
这是检测触摸手势的板:
class MyBoard extends StatefulWidget {
static final keys = List<GlobalKey>.generate(3, (ndx) => GlobalKey());
...
}
class _MyBoardState extends State<MyBoard> {
...
/// I control the number of buttons by the number of keys,
/// since all the buttons should have a key
List<Widget> makeButtons() {
return MyBoard.keys.map((key) {
// preapre some funcitonality here
var someFunctionality;
return MyButton(key, someFunctionality);
}).toList();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (tapDownDetails) {
handleTouch(context, tapDownDetails.globalPosition);
},
onTapUp: (tapUpDetails) {
finishTouch(context);
},
onPanUpdate: (dragUpdateDetails) {
handleTouch(context, dragUpdateDetails.globalPosition);
},
onPanEnd: (panEndDetails) {
finishTouch(context);
},
onPanCancel: () {
finishTouch(context);
},
child: Container(
color: Colors.green,
width: 300.0,
height: 600.0,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: makeButtons(),
),
),
);
}
这是简单的 MyButton
:
class MyButton extends StatelessWidget {
final GlobalKey key;
final Function someFunctionality;
MyButton(this.key, this.someFunctionality) : super(key: key);
@override
Widget build(BuildContext context) {
return Consumer<KeyState>(
builder: (buildContext, keyState, child) {
return Container(
width: 100.0,
height: 40.0,
color: keyState.touchedKey == this.key ? Colors.red : Colors.white,
);
},
);
}
}
在 _MyBoardState
中,这就是我处理检测哪个 MyButton
被触摸的方式:
MyButton getTouchingButton(Offset globalPosition) {
MyButton currentButton;
// result is outside of [isTouchingMyButtonKey] for performance
final result = BoxHitTestResult();
bool isTouchingButtonKey(GlobalKey key) {
RenderBox renderBox = key.currentContext.findRenderObject();
Offset offset = renderBox.globalToLocal(globalPosition);
return renderBox.hitTest(result, position: offset);
}
var key = MyBoard.keys.firstWhere(isTouchingButtonKey, orElse: () => null);
if (key != null) currentButton = key.currentWidget;
return currentButton;
}
我正在使用提供程序包,而不是用 setState
重建整个 MyBoard
,只会重建相关 MyButton
的相关部分。每个按钮仍然有一个在每次更新时重建的侦听器,我正在使用 GlobalKey
。 On the flutter docs 它说:
Global keys are relatively expensive. If you don't need any of the features listed above, consider using a Key, ValueKey, ObjectKey, or UniqueKey instead.
但是,如果我们查看 getTouchingButton
方法,我需要 GlobalKey
来获取 renderBox
对象并获取 currentWidget
。我还必须为每个 GlobalKey
进行一次 hitTest。当 onPanUpdate
被触发时重复此计算,这意味着当用户的手指移动一个像素时。
UI 更新速度不够快。单击一次(以正常速度点击向下和向上点击),您通常看不到 MyButton
s 上的任何变化。
如果我需要总结我的问题,当手指悬停在不同的小部件上时,我如何以更有效和优雅的方式检测到来自不同小部件的相同单点触摸(没有抬起)?
由于还没有人回答,我分享一下我最近想出的解决方案。现在我每次触摸都能看到视觉反应。它足够快,但仍然感觉 UI 上有一点延迟并且感觉有点 hacky 而不是具体的解决方案。如果有人能给出更好的解决方案,我可以将其标记为已接受的答案。
我的解决方案:
要事第一;因为我们必须检测平移手势并且我们正在使用 onPanUpdate
和 onPanEnd
,所以我可以删除 onTapDown
和 onTapUp
而只使用 onPanStart
。这也可以检测非移动的简单点击触摸。
我也不再使用 Provider
和 Consumer
,因为它会在每次更新时重建所有 Consumer
。当 MyButton
的数量增加时,这确实是一个大问题。我没有保持 MyButton
简单和虚拟,而是将触摸处理工作移入其中。每个MyButton
持有的数据,如果它们被触摸。
更新后的按钮是这样的:
class NewButton extends StatefulWidget {
NewButton(Key key) : super(key: key);
@override
NewButtonState createState() => NewButtonState();
}
class NewButtonState extends State<NewButton> {
bool isCurrentlyTouching = false;
void handleTouch(bool isTouching) {
setState(() {
isCurrentlyTouching = isTouching;
});
// some other functionality on touch
}
@override
Widget build(BuildContext context) {
return Container(
width: 100.0,
height: 40.0,
color: isTouched ? Colors.red : Colors.white,
);
}
}
注意 NewButtonState
不是私人的。我们将在检测触摸手势的地方使用 globalKey.currentState.handleTouch(bool)
。
假设我在屏幕上有 3 个 Container
可以通过改变颜色来响应触摸。当用户的手指放在它们上面时,它们应该改变颜色,当触摸末端时它们应该恢复正常。 我想要的是那些容器在用户 finger/pointer 对它们做出反应时即使触摸是在屏幕上容器外的随机区域开始的。 所以基本上是什么我正在寻找类似于 css 悬停的东西。
如果我用 GestureDetector
单独包装每个容器,它们将不会对在它们之外开始的触摸事件做出反应。在另一个问题上(不幸的是我现在没有 link),建议用一个 GestureDetector
包装所有容器并为每个容器分配一个 GlobalKey
以使它们彼此不同。
这是检测触摸手势的板:
class MyBoard extends StatefulWidget {
static final keys = List<GlobalKey>.generate(3, (ndx) => GlobalKey());
...
}
class _MyBoardState extends State<MyBoard> {
...
/// I control the number of buttons by the number of keys,
/// since all the buttons should have a key
List<Widget> makeButtons() {
return MyBoard.keys.map((key) {
// preapre some funcitonality here
var someFunctionality;
return MyButton(key, someFunctionality);
}).toList();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (tapDownDetails) {
handleTouch(context, tapDownDetails.globalPosition);
},
onTapUp: (tapUpDetails) {
finishTouch(context);
},
onPanUpdate: (dragUpdateDetails) {
handleTouch(context, dragUpdateDetails.globalPosition);
},
onPanEnd: (panEndDetails) {
finishTouch(context);
},
onPanCancel: () {
finishTouch(context);
},
child: Container(
color: Colors.green,
width: 300.0,
height: 600.0,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: makeButtons(),
),
),
);
}
这是简单的 MyButton
:
class MyButton extends StatelessWidget {
final GlobalKey key;
final Function someFunctionality;
MyButton(this.key, this.someFunctionality) : super(key: key);
@override
Widget build(BuildContext context) {
return Consumer<KeyState>(
builder: (buildContext, keyState, child) {
return Container(
width: 100.0,
height: 40.0,
color: keyState.touchedKey == this.key ? Colors.red : Colors.white,
);
},
);
}
}
在 _MyBoardState
中,这就是我处理检测哪个 MyButton
被触摸的方式:
MyButton getTouchingButton(Offset globalPosition) {
MyButton currentButton;
// result is outside of [isTouchingMyButtonKey] for performance
final result = BoxHitTestResult();
bool isTouchingButtonKey(GlobalKey key) {
RenderBox renderBox = key.currentContext.findRenderObject();
Offset offset = renderBox.globalToLocal(globalPosition);
return renderBox.hitTest(result, position: offset);
}
var key = MyBoard.keys.firstWhere(isTouchingButtonKey, orElse: () => null);
if (key != null) currentButton = key.currentWidget;
return currentButton;
}
我正在使用提供程序包,而不是用 setState
重建整个 MyBoard
,只会重建相关 MyButton
的相关部分。每个按钮仍然有一个在每次更新时重建的侦听器,我正在使用 GlobalKey
。 On the flutter docs 它说:
Global keys are relatively expensive. If you don't need any of the features listed above, consider using a Key, ValueKey, ObjectKey, or UniqueKey instead.
但是,如果我们查看 getTouchingButton
方法,我需要 GlobalKey
来获取 renderBox
对象并获取 currentWidget
。我还必须为每个 GlobalKey
进行一次 hitTest。当 onPanUpdate
被触发时重复此计算,这意味着当用户的手指移动一个像素时。
UI 更新速度不够快。单击一次(以正常速度点击向下和向上点击),您通常看不到 MyButton
s 上的任何变化。
如果我需要总结我的问题,当手指悬停在不同的小部件上时,我如何以更有效和优雅的方式检测到来自不同小部件的相同单点触摸(没有抬起)?
由于还没有人回答,我分享一下我最近想出的解决方案。现在我每次触摸都能看到视觉反应。它足够快,但仍然感觉 UI 上有一点延迟并且感觉有点 hacky 而不是具体的解决方案。如果有人能给出更好的解决方案,我可以将其标记为已接受的答案。
我的解决方案:
要事第一;因为我们必须检测平移手势并且我们正在使用 onPanUpdate
和 onPanEnd
,所以我可以删除 onTapDown
和 onTapUp
而只使用 onPanStart
。这也可以检测非移动的简单点击触摸。
我也不再使用 Provider
和 Consumer
,因为它会在每次更新时重建所有 Consumer
。当 MyButton
的数量增加时,这确实是一个大问题。我没有保持 MyButton
简单和虚拟,而是将触摸处理工作移入其中。每个MyButton
持有的数据,如果它们被触摸。
更新后的按钮是这样的:
class NewButton extends StatefulWidget {
NewButton(Key key) : super(key: key);
@override
NewButtonState createState() => NewButtonState();
}
class NewButtonState extends State<NewButton> {
bool isCurrentlyTouching = false;
void handleTouch(bool isTouching) {
setState(() {
isCurrentlyTouching = isTouching;
});
// some other functionality on touch
}
@override
Widget build(BuildContext context) {
return Container(
width: 100.0,
height: 40.0,
color: isTouched ? Colors.red : Colors.white,
);
}
}
注意 NewButtonState
不是私人的。我们将在检测触摸手势的地方使用 globalKey.currentState.handleTouch(bool)
。