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

Flutter中如何实现仅点击Canvas绘制对象后才可拖动

解决Flutter算盘珠子拖动范围限制问题

你的问题核心在于当前的GestureDetector覆盖了整个CustomPaint区域,任何在该区域内的拖动操作都会触发珠子位置更新,没有判断初始点击是否命中珠子本身。以下是具体的解决方案:

关键改动思路

  • 添加状态变量标记是否正在拖动珠子
  • 在拖动开始时,判断点击位置是否落在珠子的圆形范围内
  • 仅确认拖动的是珠子时,才更新其位置
  • 拖动结束后重置状态,避免误触发

修改后的完整代码

import 'package:abacus_game/functions.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  SystemChrome.setPreferredOrientations([
    DeviceOrientation.landscapeLeft,
  ]).then((value) => runApp(const MyApp()));
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(primarySwatch: Colors.blue),
      home: MyPainter(),
    );
  }
}

class MyPainter extends StatefulWidget {
  @override
  State<MyPainter> createState() => _MyPainterState();
}

class _MyPainterState extends State<MyPainter> {
  Offset _offset = Offset(0, 0);
  bool isFirst = true;
  bool _isDraggingBead = false; // 标记是否正在拖动珠子
  final double _beadRadius = 20; // 珠子半径,与绘制逻辑保持一致

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    final screenWidth = getScreenWidth(context);
    final screenHeight = getScreenHeight(context);
    
    if (isFirst) {
      _offset = Offset(screenWidth / 2, screenHeight / 2);
      isFirst = false;
    }

    return Scaffold(
      appBar: AppBar(title: const Text('Abacus')),
      body: Center(
        child: Container(
          decoration: BoxDecoration(border: Border.all(color: Colors.blueAccent)),
          child: Stack(
            alignment: Alignment.center,
            children: [
              Container(width: 10, height: 400, color: Colors.brown),
              GestureDetector(
                onVerticalDragStart: (details) {
                  // 计算点击位置到珠子圆心的距离
                  final beadCenter = Offset(screenWidth / 2, _offset.dy);
                  final distance = (details.localPosition - beadCenter).distance;
                  // 距离≤半径则判定点击到珠子,开启拖动
                  _isDraggingBead = distance <= _beadRadius;
                },
                onVerticalDragUpdate: (details) {
                  // 仅在拖动珠子状态下更新位置
                  if (_isDraggingBead) {
                    setState(() {
                      // 固定x坐标(沿竖杆移动),仅更新y坐标
                      _offset = Offset(screenWidth / 2, details.localPosition.dy);
                    });
                  }
                },
                onVerticalDragEnd: (details) {
                  // 拖动结束后重置状态
                  _isDraggingBead = false;
                },
                child: CustomPaint(
                  painter: BeadPainter(_offset),
                  // 指定手势检测区域大小,与算盘可视范围匹配
                  child: const SizedBox(width: double.infinity, height: 400),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class BeadPainter extends CustomPainter {
  final Offset offset;

  BeadPainter(this.offset) : super();

  @override
  void paint(Canvas canvas, Size size) {
    var paint = Paint()
      ..color = Colors.teal
      ..strokeWidth = 5
      ..strokeCap = StrokeCap.round;

    canvas.drawCircle(offset, 20, paint);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => true;
}

核心逻辑说明

  1. 拖动判定:在onVerticalDragStart中通过勾股定理计算点击点到珠子圆心的距离,只有距离小于等于珠子半径时,才开启拖动状态
  2. 位置更新限制:onVerticalDragUpdate仅在_isDraggingBead为true时才更新珠子位置,同时固定x坐标,确保珠子沿竖杆移动
  3. 状态重置:onVerticalDragEnd重置拖动状态,避免下次拖动无需点击珠子就触发移动
  4. 优化绘制逻辑:移除了Painter中重复的x坐标计算,直接使用状态中已固定的offset,代码更简洁

内容的提问来源于stack exchange,提问作者Rilos

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 23:27:00