在 CustomPainter 中 setState 和 shouldRepaint 如何耦合?

问题描述

最少的可重复代码

void main() => runApp(MaterialApp(home: HomePage()));

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final List<Offset> _points = [];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onpressed: () => setState(() {}),// This setState works
        child: Icon(Icons.refresh),),body: GestureDetector(
        onPanUpdate: (details) => setState(() => _points.add(details.localPosition)),// but this doesn't...
        child: CustomPaint(
          painter: MyCustomPainter(_points),size: Size.infinite,);
  }
}

class MyCustomPainter extends CustomPainter {
  final List<Offset> points;
  MyCustomPainter(this.points);

  @override
  void paint(Canvas canvas,Size size) {
    final paint = Paint()..color = Colors.red;
    for (var i = 0; i < points.length; i++) {
      if (i + 1 < points.length) {
        final p1 = points[i];
        final p2 = points[i + 1];
        canvas.drawLine(p1,p2,paint);
      }
    }
  }

  @override
  bool shouldRepaint(MyCustomPainter oldDelegate) => false;
}

尝试通过在屏幕上长时间拖动来绘制一些东西,您将看不到任何绘制出来的东西。现在,按下 FAB 将显示绘制的绘画,这可能是因为 FAB 调用 setStateonPanUpdate调用 setState 并且该调用不会在屏幕上绘制任何内容。为什么?

注意:我不是在寻找有关如何启用绘画的解决方案,一个简单的 return true 就可以完成这项工作。我需要知道的是为什么一个 setState 有效(在屏幕上绘制)但另一个失败。

解决方法

要了解为什么 onPanUpdate 中的 setState() 不起作用,您可能需要查看小部件绘制渲染器,即 CustomPaint

在完成该帧的渲染后,CustomPaint(也如文档所述)访问画家对象(在您的情况下为 MyCustomPainter)。为了确认我们可以检查 CustomPainter 的来源。我们可以看到 markNeedsPaint() 仅在我们通过 setter 访问 painter 对象时被调用。为了更清楚,您可能需要查看 RenderCustomPaint 的来源,您一定会理解它:

void _didUpdatePainter(CustomPainter? newPainter,CustomPainter? oldPainter) {
    // Check if we need to repaint.
    if (newPainter == null) {
      assert(oldPainter != null); // We should be called only for changes.
      markNeedsPaint();
    } else if (oldPainter == null ||
        newPainter.runtimeType != oldPainter.runtimeType ||
        newPainter.shouldRepaint(oldPainter)) { //THIS
      markNeedsPaint();
    }

    .
    .
    .
}

虽然在每次 setState 调用时,您的点都在更新,但每次创建“MyCustomPainter”的新实例时,都会创建小部件树,但由于上述原因,画家尚未绘制。

这就是为什么调用 markNeedPaint()(即绘制对象)的唯一方法是将 true 返回到 shouldRepaint 或者 oldDeleagate 为 null发生和 CustomPainter 的 Fist UI 构建,您可以验证这一点,在列表中提供一些默认点。

还说明了

[paint] 方法可能会被调用,即使 [shouldRepaint] 返回 false(例如,如果祖先或后代 需要重新粉刷)。 [paint] 方法也有可能 将在 [shouldRepaint] 根本没有被调用的情况下被调用(例如,如果 盒子会改变大小)。

所以 Fab 的 setState 在这里工作的唯一原因(接缝有效)是 Fab 正在以某种方式重建自定义画家的任何父级。您也可以尝试在“web build”或使用 dartpad 中调整 UI 大小,您会发现随着父级重建自身,点将变得可见,因此 setState 直接与 shouldRepaint 无关。即使将鼠标悬停在 fab(在 dartpad 中)按钮上也会导致 ui 重建,因此点将可见。

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...