Flutter中实现GestureDetector长按重复执行方块下移动作的问题
Flutter俄罗斯方块长按下移功能修复方案
我用Flutter开发了俄罗斯方块游戏,现在在GestureDetector的使用上遇到问题——希望长按方块下移按钮时,方块能快速持续下移,也就是长按时重复执行:
keyGlobal.currentState!.transformBrick(move: const Offset(0, sizePerSquare));
以下是相关代码片段:
class _TetrisGamePageState extends State<TetrisGamePage> { GlobalKey<_TetrisWidgetState> keyGlobal = GlobalKey(); ValueNotifier<List<BrickObjectPos>> brickObjectPosValue = ValueNotifier<List<BrickObjectPos>>(List<BrickObjectPos>.from([])); ValueNotifier<Scoring> scoring = ValueNotifier<Scoring>(Scoring()); bool endLoop = false; @override void initState() { super.initState(); } @override Widget build(BuildContext context) { const double sizePerSquare = 25; return Scaffold( body: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ color.AppColorMode.firstColor, color.AppColorMode.secondColor ])), alignment: Alignment.center, child: SafeArea(child: LayoutBuilder(builder: (context, constraints) { return Column( children: [ SizedBox( height: MediaQuery.of(context).size.height * 0.2, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ SizedBox( width: MediaQuery.of(context).size.width * 0.6, child: Column( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ValueListenableBuilder( valueListenable: scoring, builder: (context, Scoring value, child) { return Column( children: [ Text('Scores: ${value.score}', style: const TextStyle(color: Colors.white, fontSize: 18)), const SizedBox(height: 6), Text('Multipler: ${value.multipler}x', style: const TextStyle(color: Colors.white, fontSize: 18)), ], ); } ), ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ ElevatedButton( onPressed: () { keyGlobal.currentState!.resetGame(); }, style: ButtonStyle( backgroundColor: MaterialStateColor.resolveWith( (states) => Colors.blue)), child: const Text('Start', style: TextStyle(color: Colors.white))), ElevatedButton( onPressed: () { keyGlobal.currentState!.pauseGame(); }, style: ButtonStyle( backgroundColor: MaterialStateColor.resolveWith( (states) => Colors.blue)), child: const Text('Pause', style: TextStyle(color: Colors.white))) ], ) ], ), ), SizedBox( width: MediaQuery.of(context).size.width * 0.4, child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.center, children: [ const Text('Next: ', style: TextStyle(color: Colors.white)), ValueListenableBuilder( valueListenable: brickObjectPosValue, builder: (context, List<BrickObjectPos> value, child) { BrickShapeEnum tempShapeEnum = value.isNotEmpty ? value.last.shapeEnum : BrickShapeEnum.line; int rotation = value.isNotEmpty ? value.last.rotation : 0; return BrickShape( BrickShapeStatic.getListBrickOnEnum( tempShapeEnum, direction: rotation)); }) ], ), ), ], ), ), const SizedBox(height: 25), Container( alignment: Alignment.center, width: 300, height: 400, child: LayoutBuilder(builder: (context, constraints) { return TetrisWidget( constraints.biggest, key: keyGlobal, sizePerSquare: sizePerSquare, setCallbackScore: (Scoring score) { scoring.value = score; scoring.notifyListeners(); }, setNextBrick: (List<BrickObjectPos> brickObjectPos) { brickObjectPosValue.value = brickObjectPos; brickObjectPosValue.notifyListeners(); }); }), ), const SizedBox(height: 25), Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ GestureDetector( onTap: () => keyGlobal.currentState!.transformBrick(move: const Offset(-sizePerSquare, 0)), child: Container( width: 45, height: 45, decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(13)) ), child: const Icon(FontAwesomeIcons.arrowLeft) ), ), GestureDetector( onLongPress: () { setState(() { endLoop = false; }); while(endLoop == false) { keyGlobal.currentState!.transformBrick(move: const Offset(0, sizePerSquare)); } }, onLongPressUp: () { setState(() { endLoop = true; }); }, onTap: () => keyGlobal.currentState!.transformBrick(move: const Offset(0, sizePerSquare)), child: Container( width: 45, height: 45, decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(13)) ), child: const Icon(FontAwesomeIcons.arrowDown) ), ), GestureDetector( onTap: () => keyGlobal.currentState!.transformBrick(rotate: true), child: Container( width: 45, height: 45, decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(13)) ), child: const Icon(FontAwesomeIcons.rotate), ), ), GestureDetector( onTap: () => keyGlobal.currentState!.transformBrick(move: const Offset(sizePerSquare, 0)), child: Container( width: 45, height: 45, decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(13)) ), child: const Icon(FontAwesomeIcons.arrowRight) ), ) ], ), ], ); })), ), ); } }
问题分析
当前代码用while循环直接调用下移方法,会阻塞Flutter的UI线程,导致界面卡死;同时endLoop的状态更新无法及时触发循环终止,因为UI线程被占用,无法处理onLongPressUp的状态变更。
修复方案
用Timer.periodic实现定时重复下移,既不会阻塞UI线程,还能精准控制下移速度:
- 新增
Timer? _moveTimer变量保存定时任务; - 修改
GestureDetector的长按回调逻辑:onLongPress:启动定时任务,每隔固定间隔执行下移;onLongPressUp和onLongPressCancel:取消定时任务,停止下移;
修改后的核心代码片段:
class _TetrisGamePageState extends State<TetrisGamePage> { GlobalKey<_TetrisWidgetState> keyGlobal = GlobalKey(); ValueNotifier<List<BrickObjectPos>> brickObjectPosValue = ValueNotifier<List<BrickObjectPos>>(List<BrickObjectPos>.from([])); ValueNotifier<Scoring> scoring = ValueNotifier<Scoring>(Scoring()); Timer? _moveTimer; final double sizePerSquare = 25; @override void dispose() { _moveTimer?.cancel(); super.dispose(); } void _moveDown() { keyGlobal.currentState!.transformBrick(move: Offset(0, sizePerSquare)); } @override Widget build(BuildContext context) { return Scaffold( // 其余代码保持不变 Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ // 左右按钮代码不变 GestureDetector( onTap: _moveDown, onLongPress: () { _moveTimer = Timer.periodic(const Duration(milliseconds: 100), (timer) { _moveDown(); }); }, onLongPressUp: () { _moveTimer?.cancel(); _moveTimer = null; }, onLongPressCancel: () { _moveTimer?.cancel(); _moveTimer = null; }, child: Container( width: 45, height: 45, decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(13)) ), child: const Icon(FontAwesomeIcons.arrowDown) ), ), // 旋转按钮代码不变 ], ), // 其余代码保持不变 ); } }
额外优化点
- 将
sizePerSquare改为类成员变量,避免在build中重复定义; - 封装
_moveDown方法,减少代码重复; - 在
dispose中取消定时任务,防止内存泄漏; - 增加
onLongPressCancel回调,处理手指滑出按钮的场景,确保定时任务能被终止。
内容的提问来源于stack exchange,提问作者FabioTambu
相关产品推荐
相关产品推荐

