'Flutter Execute Method so long the button pressed
I want to execute a method while a user is pressing down on a button. In pseudocode:
while (button.isPressed) {
executeCallback();
}
In other words, the executeCallback
method should fire repeatedly as long as the user is pressing down on the button, and stop firing when the button is released. How can I achieve this in Flutter?
Solution 1:[1]
Use a Listener
and a stateful widget. I also introduced a slight delay after every loop:
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(brightness: Brightness.dark),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
bool _buttonPressed = false;
bool _loopActive = false;
void _increaseCounterWhilePressed() async {
// make sure that only one loop is active
if (_loopActive) return;
_loopActive = true;
while (_buttonPressed) {
// do your thing
setState(() {
_counter++;
});
// wait a bit
await Future.delayed(Duration(milliseconds: 200));
}
_loopActive = false;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Listener(
onPointerDown: (details) {
_buttonPressed = true;
_increaseCounterWhilePressed();
},
onPointerUp: (details) {
_buttonPressed = false;
},
child: Container(
decoration: BoxDecoration(color: Colors.orange, border: Border.all()),
padding: EdgeInsets.all(16.0),
child: Text('Value: $_counter'),
),
),
),
);
}
}
Solution 2:[2]
A simpler way, without the listener, is as follows:
GestureDetector(
child: InkWell(
child: Icon(Icons.skip_previous_rounded),
onTap: widget.onPrevious,
),
onLongPressStart: (_) async {
isPressed = true;
do {
print('long pressing'); // for testing
await Future.delayed(Duration(seconds: 1));
} while (isPressed);
},
onLongPressEnd: (_) => setState(() => isPressed = false),
);
}
Solution 3:[3]
Building on the solution from ThinkDigital, my observation is that InkWell
contains all the events necessary to do this without an extra GestureDetector
(I find that the GestureDetector
interferes with the ink animation on long press). Here's a control I implemented for a pet project that fires its event with a decreasing delay when held (this is a rounded button with an icon, but anything using InkWell
will do):
/// A round button with an icon that can be tapped or held
/// Tapping the button once simply calls [onUpdate], holding
/// the button will repeatedly call [onUpdate] with a
/// decreasing time interval.
class TapOrHoldButton extends StatefulWidget {
/// Update callback
final VoidCallback onUpdate;
/// Minimum delay between update events when holding the button
final int minDelay;
/// Initial delay between change events when holding the button
final int initialDelay;
/// Number of steps to go from [initialDelay] to [minDelay]
final int delaySteps;
/// Icon on the button
final IconData icon;
const TapOrHoldButton(
{Key? key,
required this.onUpdate,
this.minDelay = 80,
this.initialDelay = 300,
this.delaySteps = 5,
required this.icon})
: assert(minDelay <= initialDelay,
"The minimum delay cannot be larger than the initial delay"),
super(key: key);
@override
_TapOrHoldButtonState createState() => _TapOrHoldButtonState();
}
class _TapOrHoldButtonState extends State<TapOrHoldButton> {
/// True if the button is currently being held
bool _holding = false;
@override
Widget build(BuildContext context) {
var shape = CircleBorder();
return Material(
color: Theme.of(context).dividerColor,
shape: shape,
child: InkWell(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
widget.icon,
color:
Theme.of(context).textTheme.headline1?.color ?? Colors.white70,
size: 36,
),
),
onTap: () => _stopHolding(),
onTapDown: (_) => _startHolding(),
onTapCancel: () => _stopHolding(),
customBorder: shape,
),
);
}
void _startHolding() async {
// Make sure this isn't called more than once for
// whatever reason.
if (_holding) return;
_holding = true;
// Calculate the delay decrease per step
final step =
(widget.initialDelay - widget.minDelay).toDouble() / widget.delaySteps;
var delay = widget.initialDelay.toDouble();
while (_holding) {
widget.onUpdate();
await Future.delayed(Duration(milliseconds: delay.round()));
if (delay > widget.minDelay) delay -= step;
}
}
void _stopHolding() {
_holding = false;
}
}
Here it is in action:
Solution 4:[4]
To improve Elte Hupkes's solution, I fixed an issue where the number of clicks and the number of calls to the onUpdate
callback did not match when tapping consecutively.
_tapDownCount
variable is additionally used.
import 'package:flutter/material.dart';
/// A round button with an icon that can be tapped or held
/// Tapping the button once simply calls [onUpdate], holding
/// the button will repeatedly call [onUpdate] with a
/// decreasing time interval.
class TapOrHoldButton extends StatefulWidget {
/// Update callback
final VoidCallback onUpdate;
/// Minimum delay between update events when holding the button
final int minDelay;
/// Initial delay between change events when holding the button
final int initialDelay;
/// Number of steps to go from [initialDelay] to [minDelay]
final int delaySteps;
/// Icon on the button
final IconData icon;
const TapOrHoldButton(
{Key? key,
required this.onUpdate,
this.minDelay = 80,
this.initialDelay = 300,
this.delaySteps = 5,
required this.icon})
: assert(minDelay <= initialDelay, "The minimum delay cannot be larger than the initial delay"),
super(key: key);
@override
_TapOrHoldButtonState createState() => _TapOrHoldButtonState();
}
class _TapOrHoldButtonState extends State<TapOrHoldButton> {
/// True if the button is currently being held
bool _holding = false;
int _tapDownCount = 0;
@override
Widget build(BuildContext context) {
var shape = const CircleBorder();
return Material(
color: Theme.of(context).dividerColor,
shape: shape,
child: InkWell(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
widget.icon,
color: Theme.of(context).textTheme.headline1?.color ?? Colors.white70,
size: 36,
),
),
onTap: () => _stopHolding(),
onTapDown: (_) => _startHolding(),
onTapCancel: () => _stopHolding(),
customBorder: shape,
),
);
}
void _startHolding() async {
// Make sure this isn't called more than once for
// whatever reason.
widget.onUpdate();
_tapDownCount += 1;
final int myCount = _tapDownCount;
if (_holding) return;
_holding = true;
// Calculate the delay decrease per step
final step = (widget.initialDelay - widget.minDelay).toDouble() / widget.delaySteps;
var delay = widget.initialDelay.toDouble();
while (true) {
await Future.delayed(Duration(milliseconds: delay.round()));
if (_holding && myCount == _tapDownCount) {
widget.onUpdate();
} else {
return;
}
if (delay > widget.minDelay) delay -= step;
}
}
void _stopHolding() {
_holding = false;
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | boformer |
Solution 2 | |
Solution 3 | |
Solution 4 | Leo |