1

I'm testing flutter FFI by getting colour data from C/C++ side and painting the screen using CustomPainter.

But to my surprise, even though I set the painter to always repaint, the paint() function is only called twice.

Code

class _ColorViewPainter extends CustomPainter {
  Color clrBackground;

  _ColorViewPainter({
    this.clrBackground = Colors.black
  });

  @override
  bool shouldRepaint(_ColorViewPainter old) => true;

  @override
  void paint(Canvas canvas, Size size) {
    print("paint: start");

    final color = ffiGetColor().ref;

    final r = color.r;
    final g = color.g;
    final b = color.b;

    print("color: $r, $g, $b");

    final paint = Paint()
        ..strokeJoin = StrokeJoin.round
        ..strokeWidth = 1.0
        ..color = Color.fromARGB(255, r, g, b)
        ..style = PaintingStyle.fill;

    final width = size.width;
    final height = size.height;
    final content = Offset(0.0, 0.0) & Size(width, height);
    canvas.drawRect(content, paint);

    print("paint: end");
  }
}

The ffiGetColor() function simply retrieves a colour RGB struct from C/C++ side. I can see the screen being updated twice and the log says:

I/flutter (12096): paint: start
I/flutter (12096): color: 255, 0, 0
I/flutter (12096): paint: end
I/flutter (12096): paint: start
I/flutter (12096): color: 0, 255, 0
I/flutter (12096): paint: end
I/Surface (12096): opservice is null false

But that's it. Even though I clearly want it to repaint with shouldRepaint. flutter failed to do so.

What's wrong?

here is my environment

$ flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel dev, v1.18.0-dev.5.0, on Mac OS X 10.15.5 19F101, locale en-CA)
 
[✓] Android toolchain - develop for Android devices (Android SDK version 30.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 11.5)
[✓] Android Studio (version 4.0)
[✗] Cannot determine if IntelliJ is installed
    ✗ Directory listing failed
[✓] VS Code (version 1.44.2)
[✓] Connected device (1 available)

! Doctor found issues in 1 category.
kakyo
  • 10,460
  • 14
  • 76
  • 140
  • check `repaint` parameter passed to `CustomPainter` constructor – pskink Jul 11 '20 at 13:17
  • *"But that's it. Even though I clearly want it to repaint with shouldRepaint. flutter failed to do so."* - so did you check when `shouldRepaint` is called? – pskink Jul 11 '20 at 17:55
  • @pskink good point. `shouldRepaint` was only called twice. I'll take a look at the listenable idea first and report back. Thanks! – kakyo Jul 12 '20 at 00:53
  • @pskink thanks for you tips. I now have a new problem regarding update rate. Would you have a moment to take a look at this: https://stackoverflow.com/questions/62859088/dart-flutter-custompaint-updates-at-a-lower-rate-than-valuenotifiers-value-upd ?? Thanks a lot! – kakyo Jul 12 '20 at 09:09

1 Answers1

3

Thanks to @pskink 's tips, I managed to make my CustomPainter repaint continuously, although it's still not perfect.

See dart/flutter: CustomPaint updates at a lower rate than ValueNotifier's value update

Because there is still an update rate problem with the current solution, I'm just reporting what I've got briefly:

I basically have to construct a notifier and assign ffi-retrieved values to it at a proper location via polling.


// in initState() of State class
_notifier = ValueNotifier<NativeColor>(ffiGetColor().ref);

...

// in a timer method
_notifier.value = ffiGetColor().ref;

Then with CustomPaint, bind notifier to repaint in its constructor

  @override
  Widget build(BuildContext context) {
    return Container(

      ...


        child: CustomPaint(
          painter: _ColorViewPainter(
              context: context,
              notifier: _notifier,
              ...
          )
        )
    );
  }

class _ColorViewPainter extends CustomPainter {
  ValueNotifier<NativeColor> notifier;
  BuildContext context;
  Color clrBackground;

  _ColorViewPainter({this.context, this.notifier, this.clrBackground})
    : super(repaint: notifier) {
  }

  @override
  bool shouldRepaint(_ColorViewPainter old) {
    print('should repaint');
    return true;
  }

  @override
  void paint(Canvas canvas, Size size) {
    print("paint: start");
    final r = notifier.value.r;
    final g = notifier.value.g;
    final b = notifier.value.b;
    print("color: $r, $g, $b");
    final paint = Paint()
        ..strokeJoin = StrokeJoin.round
        ..strokeWidth = 1.0
        ..color = Color.fromARGB(255, r, g, b)
        ..style = PaintingStyle.fill;

    final width = size.width;
    final height = size.height;
    final content = Offset(0.0, 0.0) & Size(width, height);
    canvas.drawRect(content, paint);
    print("paint: end");
  }

}

GOTCHAS

ValueNotifier relies on the following conditions to be able to fire up notification to its listeners

  • The value type it binds to has defined the operator ==
  • The ValueNotifier's value field, which your data object of choice binds to, gets explicitly reassigned with a NEW data object on changes.

This is crucial when we bind the notifier to a custom class. With a custom class, we must

  • overload its operator ==.
  • ASSIGN a NEW object to ValueNotifier<MyClass>.value instead of modifying the data object's value by calling its own regular methods.

Otherwise the CustomPaint's paint() will not be called on desired changes.

To give an example, this custom class is not qualified to bind with a ValueNotifier, because there is no overloaded operator==:

class MyClass {
  int prop = 0;

  void changeValue(newValue) {
    prop = newValue;
  }

}

To give another example, assume we have a custom class:

class MyClass {
  int prop = 0;

  @override
  bool operator ==(covariant MyClass other) {
    return other is MyClass && prop != other. prop;
  }

  void changeValue(newValue) {
    prop = newValue;
  }

}

This will work:


class _MyViewState extends State<MyView> {
  ValueNotifier<MyClass> notifier;
  Timer _timer;

  ....

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

    notifier = ValueNotifier<MyClass>(MyClass());
    _timer = Timer.periodic(Duration(milliseconds: 10), _updateData);
  }

  _updateData(Timer t) {
     var myObj = MyClass(newValue);
     notifier.value = myObj;
  }

}

But this won't work.

class _MyViewState extends State<MyView> {
  ValueNotifier<MyClass> notifier;
  Timer _timer;

  ....

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

    notifier = ValueNotifier<MyClass>(MyClass());
    _timer = Timer.periodic(Duration(milliseconds: 10), _updateData);
  }


  _updateData(Timer t) {
     var newValue = getNewValueSomewhere();
     notifier.value.changeValue(newValue);
  }


kakyo
  • 10,460
  • 14
  • 76
  • 140