问题
I use dart FFI to retrieve data from native side, and show the data with flutter CustomPaint
.
I use ValueNotifier
to control CustomPaint
repaint.
Code: Poll data at a rate
With a state class, I poll data from native side periodically, and assign it to the ValueNotifier
.
class _ColorViewState extends State<ColorView> {
ValueNotifier<NativeColor> _notifier;
Timer _pollTimer;
@override
void initState() {
// TODO: implement initState
super.initState();
ffiInit();
// initialize notifier
_notifier = ValueNotifier<NativeColor>(ffiGetColor().ref);
_pollTimer = Timer.periodic(Duration(milliseconds: 16), _pollColor);
}
_pollColor(Timer t) {
setState(() {
print('polling ...');
_notifier.value = ffiGetColor().ref;
print('polled: ${_notifier.value.r}, ${_notifier.value.g}, ${_notifier.value.b}');
});
}
....
}
Note that I poll at the rate of about 60fps.
And I bind the notifier to the CustomPaint repaint
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(10),
width: double.infinity,
height: double.infinity,
color: widget.clrBackground,
child: ClipRect(
child: CustomPaint(
painter: _ColorViewPainter(
context: context,
notifier: _notifier,
clrBackground: Color.fromARGB(255, 255, 0, 255)
)
)
)
);
}
Code: Repaint CustomPaint reactively
Then with the CustomPaint's repaint bound to the ValueNotifier
, I paint the screen with the retrieved colour.
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");
}
}
Then I noticed that visually the colour updates are at a lower rate than the polling. This is observed by looking at my logging and the phone screen at the same time, although the repaint works.
Question
How should I achieve perceived simultaneous updates?
I should also add that the native backend simulation switches colours between red/green/blue at an 1-second interval.
Since the polling is much more frequent, I expect to see a fairly stable colour changing at about 1-second interval. But right now the colours change at a longer interval. Sometimes repaint is called very rarely, which could be several seconds, all the while the polling returns quite stable data updates.
Update
According to my test I should keep setState
otherwise the repaint simply stops. Also by switching the data update to dart land I found that everything works as expected. So it must be something on the native side or in the FFI interface. Here is the modified dart code that works as expected when no FFI is involved.
Basically I use a constant colour collection and iterate through it.
class _ColorViewState extends State<ColorView> {
ValueNotifier<NativeColor> _notifier;
Timer _pollTimer;
var _colors;
int _step = 0;
@override
void initState() {
// TODO: implement initState
super.initState();
ffiInit();
// constant colour collection
_colors = [
[255, 0, 0],
[0, 255, 0],
[0, 0, 255]
];
_notifier = ValueNotifier<NativeColor>(ffiGetColor().ref);
_pollTimer = Timer.periodic(Duration(milliseconds: 1000), _pollColor);
}
_pollColor(Timer t) {
setState(() {
print('polling ...');
// _notifier.value = ffiGetColor().ref;
_notifier.value.r = _colors[_step][0];
_notifier.value.g = _colors[_step][1];
_notifier.value.b = _colors[_step][2];
print('polled: ${_notifier.value.r}, ${_notifier.value.g}, ${_notifier.value.b}');
if (++_step >= _colors.length) {
_step = 0;
}
});
}
On the native side, I have a producer/consumer thread model working. The producer is looping through the colour collection at a fixed rate. And the consumer gets it whenever pinged by the producer.
#include <cstdlib>
#include <ctime>
#include <chrono>
#include <condition_variable>
#include <mutex>
#include <thread>
#ifdef __cplusplus
#define EXTERNC extern "C" __attribute__((visibility("default"))) __attribute__((used))
#else
#define EXTERNC
#endif // #ifdef __cplusplus
struct NativeColor {
int r;
int g;
int b;
};
NativeColor* gpColor = nullptr;
NativeColor gWorker = {255, 0, 255};
// producer / consumer thread tools
std::thread gThread;
std::mutex gMutex;
std::condition_variable gConVar;
int gColors[][3] = {
{255, 0, 0},
{0, 255, 0},
{0, 0, 255}
};
int gCounter = 0;
int gCounterPrev = 0;
EXTERNC void ffiinit() {
if(!gpColor) {
gpColor = (struct NativeColor*)malloc(sizeof(struct NativeColor));
}
if(!gThread.joinable()) {
gThread = std::thread([&]() {
while(true) {
std::this_thread::sleep_for (std::chrono::seconds(1));
std::unique_lock<std::mutex> lock(gMutex);
gWorker.r = gColors[gCounter][0];
gWorker.g = gColors[gCounter][1];
gWorker.b = gColors[gCounter][2];
if(++gCounter == 3) {
gCounter = 0;
gCounterPrev = gCounter;
}
lock.unlock();
gConVar.notify_one();
}
});
}
}
EXTERNC struct NativeColor* ffiproduce() {
// get yellow
gpColor->r = 255;
gpColor->g = 255;
gpColor->b = 255;
std::unique_lock<std::mutex> lock(gMutex);
gConVar.wait(lock, [&]{
return gCounter > gCounterPrev;
//return true;
});
*gpColor = gWorker;
gCounterPrev = gCounter;
lock.unlock();
return gpColor;
}
ffiproduce()
is bound to the dart-side ffiGetColor()
function. So I assume this consumer function works in the main thread.
So one idea I have is that maybe the thread coordination on C++ side affected the way flutter renders through CustomPaint
.
But I have no idea how to prove that at this point.
回答1:
I made some progress by playing around with the sleep function on the native side.
Here are my findings:
- I used to use 1-sec interval to churn out data from the native producer thread, and consume it from main thread also on the native side. The consumer must wait for the producer ping. At this rate, flutter rendering thread seems to be on halt.
- By reducing the sleep all the way to below 20ms, the rendering starts working as expected.
- With even 20ms sleep, there are hiccups in the rendering.
So I believe my data generation model must adapt to flutter to ensure that the polling should not block or should deliver at the rate around flutter's preferred rate, say, 60fps.
来源:https://stackoverflow.com/questions/62859088/dart-flutter-custompaint-updates-at-a-lower-rate-than-valuenotifiers-value-upd