You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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线程,还能精准控制下移速度:

  1. 新增Timer? _moveTimer变量保存定时任务;
  2. 修改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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 23:22:08