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

Flutter可自由变换画布绘图应用实时显示问题求助

Flutter绘图应用实时轨迹显示问题

我正在开发一款Flutter绘图应用,需求如下:

  • 画布支持双指触发旋转、缩放和平移操作
  • 单指在画布滑动时执行绘图

目前已实现带图片的画布自由变换功能,但绘图时仅在抬手后才显示轨迹,希望实现单指绘图过程中实时显示轨迹。试过双Widget分别处理当前/历史路径的方案,但担心频繁状态重建不想使用;尝试过在历史路径末尾添加当前轨迹导致卡顿,canvas.save/restore方法也无效,求其他解决方案。


现有代码

Widget代码

import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:label_up_study/models/sketch.dart';

class TestScreen1 extends StatefulWidget {
  const TestScreen1({super.key});

  @override
  State<TestScreen1> createState() => _TestScreen1State();
}

class _TestScreen1State extends State<TestScreen1> {
  Offset _offset = Offset.zero;
  late Offset _startOffset;
  late Offset _previousOffset;
  double _scale = 0.75;
  late double _previousScale;
  double _rotation = 0;
  Sketch _sketches = Sketch(points: []);
  List<Sketch> _allSketches = [];
  List<int> _pointerCounts = [];

  @override
  void initState() {
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {});
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: GestureDetector(
        onScaleStart: (ScaleStartDetails details) {
          _pointerCounts.clear();
          _pointerCounts.add(details.pointerCount);
          if (details.pointerCount == 2) {
            _startOffset = details.focalPoint;
            _previousOffset = _offset;
            _previousScale = _scale;
          }
        },
        onScaleUpdate: (ScaleUpdateDetails details) {
          _pointerCounts.add(details.pointerCount);
          if (details.pointerCount == 2) {
            setState(() {
              _rotation = details.rotation;
              _scale = _previousScale * details.scale;
              final Offset normalizedOffset =
                  (_startOffset - _previousOffset) / _previousScale;
              _offset =
                  details.focalPoint - normalizedOffset * _scale;
            });
          }
        },
        child: Stack(
          alignment: Alignment.center,
          children: [
            Container(color: Colors.grey),
            Transform.translate(
              offset: _offset,
              child: Transform.rotate(
                angle: _rotation,
                child: Transform.scale(
                  scale: _scale,
                  child: RepaintBoundary(
                    child: Listener(
                      onPointerDown: (pointEvent) {
                        if (_pointerCounts.where((element) => element == 1).toList().length > 5){
                          setState(() {
                            _sketches.points.add(pointEvent.localPosition);
                          });
                        }
                      },
                      onPointerMove: (pointEvent) {
                        if (_pointerCounts.where((element) => element == 1).toList().length > 5){
                          setState(() {
                            _sketches.points.add(pointEvent.localPosition);
                          });
                        }
                      },
                      onPointerUp: (pointEvent) {
                        if (_pointerCounts.where((element) => element == 1).toList().length > 5){
                          setState(() {
                            _allSketches = List<Sketch>.from(_allSketches)..add(_sketches!);
                            _sketches = Sketch(points: []);
                          });
                        }
                      },
                      child: CustomPaint(
                        foregroundPainter: MyCustomPainter(
                          allSketches: _allSketches,
                          currentSketches: _sketches,
                        ),
                        child: Image.asset('asset/img/hydrocolloid_dressing.jpg'),
                      ),
                    ),
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class MyCustomPainter extends CustomPainter {
  final List<Sketch> allSketches;
  final Sketch currentSketches;

  MyCustomPainter({required this.allSketches, required this.currentSketches});

  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()..style = PaintingStyle.fill;

    for(int i=0; i < allSketches.length; i++){
      final points = allSketches[i].points;
      if(points.isEmpty){
        return;
      }
      final allPath = Path();
      allPath.moveTo(points.first.dx, points.first.dy);

      for (int j=1; j < points.length -1 ; ++j){
        final p0 = points[j];
        final p1 = points[j+1];
        allPath.quadraticBezierTo(p0.dx, p0.dy, (p0.dx + p1.dx)/2, (p0.dy + p1.dy)/2);
      }
      canvas.drawPath(allPath, paint);
    }
  }

  @override
  bool shouldRepaint(MyCustomPainter delegate) {
    return true;
  }
}

Sketch类代码

import 'package:flutter/material.dart';

class Sketch {
  final List<Offset> points;

  Sketch({required this.points});
}

解决方案

核心问题是CustomPainter未绘制当前实时轨迹,且手势判断逻辑冗余导致触发异常,以下是针对性修复:

1. 修复CustomPainter,添加实时轨迹绘制

修改MyCustomPainter的paint方法,单独处理当前正在绘制的路径,同时调整画笔样式为线条(原代码用填充会导致轨迹异常):

@override
void paint(Canvas canvas, Size size) {
  Paint paint = Paint()
    ..style = PaintingStyle.stroke
    ..strokeWidth = 2.0
    ..color = Colors.black;

  // 绘制历史路径
  for (Sketch sketch in allSketches) {
    final points = sketch.points;
    if (points.length < 2) continue;
    final path = Path();
    path.moveTo(points.first.dx, points.first.dy);
    for (int j = 1; j < points.length - 1; j++) {
      final p0 = points[j];
      final p1 = points[j + 1];
      path.quadraticBezierTo(
        p0.dx,
        p0.dy,
        (p0.dx + p1.dx) / 2,
        (p0.dy + p1.dy) / 2,
      );
    }
    canvas.drawPath(path, paint);
  }

  // 绘制当前实时轨迹
  final currentPoints = currentSketches.points;
  if (currentPoints.length >= 2) {
    final currentPath = Path();
    currentPath.moveTo(currentPoints.first.dx, currentPoints.first.dy);
    for (int j = 1; j < currentPoints.length - 1; j++) {
      final p0 = currentPoints[j];
      final p1 = currentPoints[j + 1];
      currentPath.quadraticBezierTo(
        p0.dx,
        p0.dy,
        (p0.dx + p1.dx) / 2,
        (p0.dy + p1.dy) / 2,
      );
    }
    canvas.drawPath(currentPath, paint);
  }
}

2. 简化手势判断逻辑

原代码的指针数量统计逻辑冗余,直接通过当前指针ID判断单指操作:

Listener(
  onPointerDown: (pointEvent) {
    // 仅单指触发绘图
    if (pointEvent.pointer == 1) {
      setState(() {
        _sketches.points.add(pointEvent.localPosition);
      });
    }
  },
  onPointerMove: (pointEvent) {
    if (pointEvent.pointer == 1) {
      setState(() {
        _sketches.points.add(pointEvent.localPosition);
      });
    }
  },
  onPointerUp: (pointEvent) {
    if (pointEvent.pointer == 1 && _sketches.points.isNotEmpty) {
      setState(() {
        _allSketches.add(_sketches);
        _sketches = Sketch(points: []);
      });
    }
  },
  // 子组件代码不变
)

3. 优化重绘效率

修改shouldRepaint逻辑,仅当数据变化时才重绘,减少不必要的状态重建:

import 'package:flutter/foundation.dart'; // 导入listEquals

@override
bool shouldRepaint(MyCustomPainter oldDelegate) {
  bool sketchesChanged = !listEquals(allSketches, oldDelegate.allSketches);
  bool currentChanged = !listEquals(currentSketches.points, oldDelegate.currentSketches.points);
  return sketchesChanged || currentChanged;
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 17:38:13