Assume I have 3 Container
s on the screen that react touching by changing their color. When user's finger is on them, they should change color and when touching ends they should turn back to normal. What I want is that those containers to react when the user finger/pointer on them even if the touch started on a random area on the screen, outside of the containers. So basically what i am looking for is something just like css hover.
If i wrap each container with GestureDetector
seperately, they will not react to touch events which start outside of them. On another question (unfortunately i dont have the link now) it is suggested to wrap all the containers with one GestureDetector
and assign a GlobalKey
to each to differ them from each other.
Here is the board that detects touch gestures:
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(),
),
),
);
}
Here is the simple 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,
);
},
);
}
}
In the _MyBoardState
that's how i handle detecting which MyButton
is touched:
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;
}
I am using provider package and instead of rebuilding the whole MyBoard
with setState
only the related part of related MyButton
will be rebuilded. Still each button has a listener which rebuilds at every update and I am using GlobalKey
. On the flutter docs it says:
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.
However if we look at getTouchingButton
method I need GlobalKey
to get renderBox
object and to get currentWidget
. I also have to make a hitTest for each GlobalKey
. This computation repeats when the onPanUpdate
is triggered, which means when the user's finger moves a pixel.
UI is not updating fast enough. With a single tap (tapDown and tapUp in regular speed) you usually do not see any change on the MyButton
s.
If I need to sum up my question, How can i detect same single touch (no lifting) from different widgets when finger is hoverin on them in more efficient and elegant way?
Since no one answered yet, I am sharing my own solution which I figured out lately. Now I can see the visual reaction everytime i touch. It is fast enough, but still it feels like there is a little delay on the UI and it feels a little hacky instead of a concrete solution. If someone can give better solution, I can mark it as accepted answer.
My solution:
First things first; since we have to detect Pan gesture and we are using onPanUpdate
and onPanEnd
, I can erase onTapDown
and onTapUp
and instead just use onPanStart
. This can also detect non-moving simple tap touches.
I also do not use Provider
and Consumer
anymore, since it rebuilds all the Consumer
s at every update. This is really a big problem when the the number of MyButton
s increase. Instead of keeping MyButton
simple and dummy, I moved touch handling work into it. Each MyButton
hold the data of if they are touched at the moment.
Updated button is something like this:
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,
);
}
}
Notice that NewButtonState
is not private. We will be using globalKey.currentState.handleTouch(bool)
where we detect the touch gesture.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With