Reputation: 1087
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?
Upvotes: 10
Views: 6297
Reputation: 1087
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.
Upvotes: 6