1

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!

1 Answers1

1

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

Bach
  • 2,928
  • 1
  • 6
  • 16
  • 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 Feb 01 '21 at 15:20
  • 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. – robinwkurtz Feb 01 '21 at 15:30