Flutter:如何通知CustomPainter重新绘制?

3
在以下代码中,我尝试模拟画布上的小部件。

three buttons

当小部件/按钮被触摸时,它会在两种颜色之间翻转。
import 'package:flutter/material.dart';

void main() { runApp(MyApp()); }

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Diagram',
      theme: ThemeData(
        appBarTheme: AppBarTheme(color: Colors.blueAccent,),
      ),
      home: Diagram(),
    );
  }
}

class Diagram extends StatefulWidget {
  @override
  _DiagramState createState() => new _DiagramState();
}

class _DiagramState extends State<Diagram> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Canvas Widgets'),
        centerTitle: true,
      ),
      body: Center(
        child: GestureDetector(
          onTapUp: (TapUpDetails details) {
            setState(() {
                ;
            });
          },
          child: AspectRatio(
            aspectRatio: 1.0,
            child: Container(
              child: CustomPaint(
                painter: DiagramPainter(),
              ),
            ),
          ),
        ),
      ),
    );
  }

}

class DiagramPainter extends CustomPainter {
// class DiagramPainter extends ChangeNotifier implements CustomPainter {
  DiagramPainter() {
    for(int i=0; i<_numberOfButtons; i++) {
      state.add(false);
    }
  }

  static final int _numberOfButtons = 3;
  static final double _margin = 60;
  static final double _gap = _margin / 2.0 / _numberOfButtons.toDouble();
  double width;
  double y;

  static Canvas currentCanvas;

  var buttons = [];
  var state = [];

  final bluePaint = Paint()
    ..color = Colors.blue;
  final yellowPaint = Paint()
    ..color = Colors.yellow;

  @override
  void paint(Canvas canvas, Size size) {
    currentCanvas = canvas;
    width = (size.width - _margin) /_numberOfButtons.toDouble();
    y = size.height / 2.0 - width / 2.0;

    Iterable<int>.generate(_numberOfButtons).toList().forEach((index) {
      buttons.add(drawButton(canvas, index));
    });
  }

  Rect drawButton(Canvas canvas, int index) {
    double left = _gap + index * (width + _gap * 2.0);

    var rect = Rect.fromLTWH(left, y, width, width);
    var paint = state[index] ? yellowPaint : bluePaint;
    canvas.drawOval(rect, paint);
    return rect;
  }

  @override
  bool hitTest(Offset position) {
    for(int i=0; i<_numberOfButtons; i++) {
      if (buttons[i].contains(position)) {
        state[i] = !state[i];
        // trigger redraw
        // notifyListeners();
        return true;
      }
    }
    return false;
  }

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

我了解到需要使用 class DiagramPainter extends ChangeNotifier implements CustomPainter 而不是 class DiagramPainter extends CustomPainter
我还了解到需要通过 notifyListeners() 触发重绘。在这种情况下,谁被通知了?仍然是 DiagramPainter,对吗?我错过了什么吗?
相关链接: 1, 2.

@pskink 嗯,事实上我正在寻找一个完全动态的小部件集合在画布中。所以 :-) 要么1)我接受Chunhunghan的答案,这是完全可以的,考虑到我提出问题的方式,并且提出一个续篇问题;要么2)你通过撰写一个答案向我们展示细节(魔鬼就在细节中)。 - undefined
@pskink 完整地修正了许多代码中的不良习惯。太棒了! - undefined
@pskink,我在你的代码中看到了几个优点,但我仍然困惑。仅仅通过ChangeNotifier mixin的存在,如何使notifyListeners()调用paint()方法呢?例如,paint()是否以某种方式声明为CustomPainter的变化通知代理呢? - undefined
这是最重要的地方:https://chromium.googlesource.com/external/github.com/flutter/flutter/+/v0.9.2/packages/flutter/lib/src/rendering/custom_paint.dart#485 - 你可以在自定义绘制器中重写 void addListener(listener) { 方法,并调用 print(listener); 来查看传递的参数。 - undefined
@pskink 考虑一下:我们不仅想要改变每个小部件的颜色,还想要改变其他属性。比如说,每个小部件都是算盘中的珠子(二进制)。它们通过位置(上/下)来显示自己的状态。我们可以继续处理onTapDown事件,但用户的需求越来越高。他们希望看到珠子移动。现在的问题是,处理onVerticalDragStart和onVerticalDragUpdate事件是否足够,或者是否需要更重的手段(比如AnimationController)。 - undefined
显示剩余14条评论
2个回答

5

您可以复制下面的完整代码并运行
步骤1: DiagramPainter({Listenable repaint}) : super(repaint: repaint)
步骤2: 将ValueNotifier<int>(0);传递给DiagramPainter(repaint: _counter)
步骤4: 在onTapUp中调用_counter.value++;
代码片段

class _DiagramState extends State<Diagram> {
  final _counter = ValueNotifier<int>(0);

  @override
  Widget build(BuildContext context) {
    ...
        child: GestureDetector(
          onTapUp: (TapUpDetails details) {
            _counter.value++;               
          },
          child: ...
              child: CustomPaint(
                painter: DiagramPainter(repaint: _counter),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

class DiagramPainter extends CustomPainter {    
  DiagramPainter({Listenable repaint}) : super(repaint: repaint) {

工作演示

enter image description here

完整代码

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Diagram',
      theme: ThemeData(
        appBarTheme: AppBarTheme(
          color: Colors.blueAccent,
        ),
      ),
      home: Diagram(),
    );
  }
}

class Diagram extends StatefulWidget {
  @override
  _DiagramState createState() => new _DiagramState();
}

class _DiagramState extends State<Diagram> {
  final _counter = ValueNotifier<int>(0);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Canvas Widgets'),
        centerTitle: true,
      ),
      body: Center(
        child: GestureDetector(
          onTapUp: (TapUpDetails details) {
            _counter.value++;
            /*setState(() {
              ;
            });*/
          },
          child: AspectRatio(
            aspectRatio: 1.0,
            child: Container(
              child: CustomPaint(
                painter: DiagramPainter(repaint: _counter),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

class DiagramPainter extends CustomPainter {
// class DiagramPainter extends ChangeNotifier implements CustomPainter {
  DiagramPainter({Listenable repaint}) : super(repaint: repaint) {
    for (int i = 0; i < _numberOfButtons; i++) {
      state.add(false);
    }
  }

  static final int _numberOfButtons = 3;
  static final double _margin = 60;
  static final double _gap = _margin / 2.0 / _numberOfButtons.toDouble();
  double width;
  double y;

  static Canvas currentCanvas;

  var buttons = [];
  var state = [];

  final bluePaint = Paint()..color = Colors.blue;
  final yellowPaint = Paint()..color = Colors.yellow;

  @override
  void paint(Canvas canvas, Size size) {
    currentCanvas = canvas;
    width = (size.width - _margin) / _numberOfButtons.toDouble();
    y = size.height / 2.0 - width / 2.0;

    Iterable<int>.generate(_numberOfButtons).toList().forEach((index) {
      buttons.add(drawButton(canvas, index));
    });
  }

  Rect drawButton(Canvas canvas, int index) {
    double left = _gap + index * (width + _gap * 2.0);

    var rect = Rect.fromLTWH(left, y, width, width);
    var paint = state[index] ? yellowPaint : bluePaint;
    canvas.drawOval(rect, paint);
    return rect;
  }

  @override
  bool hitTest(Offset position) {
    for (int i = 0; i < _numberOfButtons; i++) {
      if (buttons[i].contains(position)) {
        state[i] = !state[i];
        // trigger redraw
        // notifyListeners();
        return true;
      }
    }
    return false;
  }

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

2

感谢@chunhunghan的回答并点赞。如果有人需要,我在这里放了一些新代码,已更新到最新的flutter版本,稍微简化了一下,并添加了一些注释。

import 'package:flutter/material.dart';

// based on @chunhunghan in https://dev59.com/7l0KtIcB2Jgan1znALau

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Diagram',
      home: Diagram(),
    );
  }
}

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

  @override
  DiagramState createState() => DiagramState();
}

class DiagramState extends State<Diagram> {

  final _counter = ValueNotifier<int>(0);  // 1. the important bit

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Canvas Widgets'),
        centerTitle: true,
      ),
      body: SizedBox(
          width: 300,
          height:300,
          child: GestureDetector(
          onTapUp: (TapUpDetails details) {
            _counter.value++; // 2. another important bit
          },
          child: CustomPaint(
            painter: DiagramPainter(notifier: _counter),
          ),
        )),
    );
  }
}

class DiagramPainter extends CustomPainter {
  // 3. constructor has, and passes to super, the listenable - the final important bit
  DiagramPainter({required this.notifier}) : super(repaint: notifier);
  // Keeping the notified as a variable here is not required, unless you
  // are interested in the notified data.
  // Passing the notifier to super is sufficient to get the repaint 
  // on every change of the object inside ValueNotifier
  ValueNotifier<int> notifier;
  var _state = false;
  final bluePaint = Paint()..color = Colors.blue;
  final yellowPaint = Paint()..color = Colors.yellow;

  @override
  void paint(Canvas canvas, Size size) {
    print("counter has value = ${notifier.value}");
    _drawButton(canvas);
  }

  void _drawButton(Canvas canvas) {
    var rect = const Rect.fromLTWH(100, 100, 100, 100);
    var paint = _state ? yellowPaint : bluePaint;
    _state = !_state;
    canvas.drawOval(rect, paint);
  }

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接