Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

flutter hover-like touch handling - detect touch from different widgets without lifting the finger

Assume I have 3 Containers 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 MyButtons.

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?

like image 509
buraky Avatar asked Nov 10 '19 17:11

buraky


1 Answers

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 Consumers at every update. This is really a big problem when the the number of MyButtons 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.

like image 100
buraky Avatar answered Oct 19 '22 16:10

buraky