Unable to reliably trigger animation with longPress on GestureDetector

404

You are using a Text widget to receive the hit within the GestureDetector, which have a small hit box compare to the thumb. This might be the reason why you might misclick the hit box occasionally.

You can use the debugPaintPointersEnabled to see the behavior more clearly (need to do a Hot Restart if the app is running):

import 'package:flutter/rendering.dart';

void main() {
  // Add the config here
  debugPaintPointersEnabled = true;
  runApp(App());
}

You can see that the hit box does not flash all the time, even when we think we hit the Text. To increase the accuracy, let's wrap a Container with size around the Text

GestureDetector(

      // ... other lines

      child: Container(
          width: 100,
          height: 50,
          color: Colors.blue,
          alignment: Alignment.center,
          child:
              Text('Value: ${_animationController.value.toStringAsFixed(2)}')),
    );

You can see that the hit box flashes everytime now

Share:
404
robinwkurtz
Author by

robinwkurtz

Updated on December 27, 2022

Comments

  • robinwkurtz
    robinwkurtz over 1 year

    I'm trying to create a button with a progress indicator (CircularProgressIndicator)

    Desired flow:

    1. The user taps on the button it should fire a function
    2. The user presses the button (and holds), it should trigger the animation and fire a function
    3. When the user releases their hold, it should reset the animation and fire a function

    At this point, my code works on the second time pressing (and holding) the element. The first time around, the animation controller's addListener prints 2-3 times and then stops, whereas the second time, it holds true and continues to print as the user holds the element. Ontap functionality works regardless.

    It's happening while running locally on an android and an ios device

    Stripped code block:

    import 'package:flutter/material.dart';
    import 'package:homi_frontend/constants/woopen_colors.dart';
    
    class ProgressButton extends StatefulWidget {
      ProgressButton({
        @required this.onTap,
        @required this.onLongPress,
        @required this.onLongPressUp,
        this.duration = const Duration(seconds: 60),
      });
    
      final Function onTap;
      final Function onLongPress;
      final Function onLongPressUp;
      final Duration duration;
    
      @override
      ProgressButtonState createState() => ProgressButtonState();
    }
    
    class ProgressButtonState extends State<ProgressButton>
        with SingleTickerProviderStateMixin {
      AnimationController _animationController;
      bool _beingPressed = false;
    
      @override
      void initState() {
        _animationController = AnimationController(
          vsync: this,
          duration: widget.duration,
        );
    
        _animationController.addListener(_animationListener);
        _animationController.addStatusListener(_animationStatusListener);
    
        super.initState();
      }
    
      void _animationListener() {
        print('Animation Controller Listener');
        setState(() {});
      }
    
      void _animationStatusListener(AnimationStatus status) {
        print('_animationStatusListener');
        if (status == AnimationStatus.completed) {
          print(
              'Completed duration of ${widget.duration}, fire _handleOnLongPressUp');
          _handleOnLongPressUp();
        }
    
        if (status == AnimationStatus.forward) {
          this.setState(() {
            _beingPressed = true;
          });
        }
      }
    
      void _handleOnLongPress() {
        print('_handleOnLongPress');
        try {
          _animationController.forward();
        } catch (e) {
          print('_handleOnLongPress error: ${e.toString()}');
        } finally {
          if (_animationController.status == AnimationStatus.forward) {
            print('Controller has been started, fire widget.onLongPress');
            widget.onLongPress();
          }
        }
      }
    
      void _handleOnLongPressUp() {
        print('_handleOnLongPressUp');
        try {
          this.setState(() {
            _beingPressed = false;
          });
          _animationController.reset();
        } catch (e) {
          print('_handleOnLongPressUp error: ${e.toString()}');
        } finally {
          if (_animationController.status == AnimationStatus.dismissed) {
            print('Controller has been dismissed, fire widget.onLongPressUp');
            widget.onLongPressUp();
          }
        }
      }
    
      @override
      dispose() {
        _animationController.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return GestureDetector(
          key: Key('progressButtonGestureDetector'),
          behavior: HitTestBehavior.opaque,
          onLongPress: _handleOnLongPress,
          onLongPressUp: _handleOnLongPressUp,
          onTap: widget.onTap,
          child: Container(
            width: 80,
            height: 80,
            child: Text(_animationController.value.toStringAsFixed(2)),
          ),
        );
      }
    }
    

    Output:

    flutter: _handleOnLongPress
    flutter: _animationStatusListener
    flutter: Controller has been started, fire widget.onLongPress
    (2) flutter: Animation Controller Listener
    
    # here it just seems to loose its connection, but if I press (and hold) again, I get:
    
    flutter: _handleOnLongPress
    flutter: _animationStatusListener
    flutter: Controller has been started, fire widget.onLongPress
    (326) flutter: Animation Controller Listener
    flutter: _handleOnLongPressUp
    flutter: Animation Controller Listener
    flutter: _animationStatusListener
    flutter: Controller has been dismissed, fire widget.onLongPressUp
    

    I've also looked briefly into RawGestureDetector but only my TapGestureRecognizer gestures seem to fire, the LongPressGestureRecognizer ones don't... even if TapGestureRecognizers are removed.

    _customGestures = Map<Type, GestureRecognizerFactory>();
    _customGestures[TapGestureRecognizer] =
        GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
      () => TapGestureRecognizer(debugOwner: this),
      (TapGestureRecognizer instance) {
        instance
          ..onTapDown = (TapDownDetails details) {
            print('onTapDown');
          }
          ..onTapUp = (TapUpDetails details) {
            print('onTapUp');
          }
          ..onTap = () {
            print('onTap');
          }
          ..onTapCancel = () {
            print('onTapCancel');
          };
      },
    );
    _customGestures[LongPressGestureRecognizer] =
        GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
      () => LongPressGestureRecognizer(
          duration: widget.duration, debugOwner: this),
      (LongPressGestureRecognizer instance) {
        instance
          ..onLongPress = () {
            print('onLongPress');
          }
          ..onLongPressStart = (LongPressStartDetails details) {
            print('onLongPressStart');
            _animationController.forward();
          }
          ..onLongPressMoveUpdate = (LongPressMoveUpdateDetails details) {
            print('onLongPressMoveUpdate');
          }
          ..onLongPressEnd = (LongPressEndDetails details) {
            print('onLongPressEnd');
            _animationController.reset();
          }
          ..onLongPressUp = () {
            print('onLongPressUp');
          };
      },
    );
    

    Please & thank you for your time!

  • robinwkurtz
    robinwkurtz over 3 years
    Thank you @bach in fact, my button was always a Container and not just a Text widget, a silly oversight when I stripped down my code for example. I've added in the debugPaintPointersEnabled flag, and I can indeed see the initial highlight, though it simply stops after a few miliseconds
  • robinwkurtz
    robinwkurtz over 3 years
    In fact, the debugPaintPointersEnabled made me realize I didn't isolate the issue... it appears I have a rerender above my button which is causing the first flash... I don't know why it wouldn't be an issue the second time around but this is a great lead.