#flutter #dart
Вопрос:
Я пытаюсь захватить PNG
изображение виджета Flutter для использования в MapBox
экземпляре. Код требует, чтобы я определял, когда процесс сборки завершен, и отрисованная область готова к рисованию. Вот мой Widget
:
typedef void ImageCreatedCallback(UI.Image image);
class POIWidget extends StatefulWidget {
const POIWidget({Key key, this.poi, this.imageCreatedCallback})
: super(key: key);
final POI poi;
final ImageCreatedCallback imageCreatedCallback;
@override
_POIWidgetState createState() => _POIWidgetState();
}
class _POIWidgetState extends State<POIWidget> {
/**
* The only purpose of this widget is to provide an Image version of itself
* that can then be used in the MapBox map.
*/
GlobalKey globalKey = GlobalKey();
Future<void> _capturePng() async {
RenderRepaintBoundary boundary =
globalKey.currentContext.findRenderObject() as RenderRepaintBoundary;
if (boundary == null) {
// print("Waiting for boundary to be painted.");
await Future.delayed(const Duration(milliseconds: 100));
return _capturePng();
}
MediaQueryData queryData = MediaQuery.of(context);
double devicePixelRatio = queryData.devicePixelRatio;
UI.Image image = await boundary.toImage(pixelRatio: devicePixelRatio);
widget.imageCreatedCallback(image);
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback(
(_) async {
await Future.delayed(const Duration(milliseconds: 100))
.then((_) => _capturePng());
},
);
}
@override
Widget build(BuildContext context) {
return RepaintBoundary(
key: globalKey,
child: ClipOval(
child: Material(
color: widget.poi.iconBackgroundColor,
child: SizedBox(
width: widget.poi.iconSize.size * 2,
height: widget.poi.iconSize.size * 2,
child: widget.poi.iconName != null
? IconManager.instance.getIconFromName(
widget.poi.iconName,
size: widget.poi.iconSize.size,
color: widget.poi.iconColor,
)
: IconManager.instance.iconMapPin,
),
),
),
);
}
}
Вы заметите, что в _capturePNG
методе есть эта строка:
RenderRepaintBoundary boundary =
globalKey.currentContext.findRenderObject() as RenderRepaintBoundary;
и сразу же после этого я проверяю, так ли это null
. Я думаю, что это игнорируется, поскольку это не null
так, а что-то другое. Однако это не то, что мне нужно для создания изображения позже в методе.
Я получаю это сообщение об ошибке из этого кода:
[ERROR:flutter/lib/ui/ui_dart_state.cc(186)] Unhandled Exception: Null check operator used on a null value
E/flutter (21697): #0 RenderRepaintBoundary.toImage (package:flutter/src/rendering/proxy_box.dart:3089)
E/flutter (21697): #1 _POIWidgetState._capturePng (package:screens/map/widgets/POIWidget.dart:38)
E/flutter (21697): #2 _POIWidgetState.initState.<anonymous closure>.<anonymous closure> (package:screens/map/widgets/POIWidget.dart:48)
E/flutter (21697): #3 _rootRunUnary (dart:async/zone.dart:1362)
E/flutter (21697): #4 _CustomZone.runUnary (dart:async/zone.dart:1265)
E/flutter (21697): <asynchronous suspension>
E/flutter (21697): #5 _POIWidgetState.initState.<anonymous closure> (package:screens/map/widgets/POIWidget.dart:47)
E/flutter (21697): <asynchronous suspension>
Как я могу быть уверен, что виджет построен и RenderRepaintBoundary
готов к захвату?
Комментарии:
1. создайте класс, который расширяет
SingleChildRenderObjectWidget
и создает пользовательскийRenderProxyBox
— этот пользовательскийRenderProxyBox
должен переопределятьbool get isRepaintBoundary => true;
и внутри своегоvoid paint(PaintingContext context, Offset offset)
метода захватывать изображение с помощьюOffsetLayer ol = layer; ol.toImage(...)
2. конечно, вы должны позвонить
ol.toImage(...)
послеsuper.paint(PaintingContext context, Offset offset)
того, как вам позвонили3. Итак, проблема здесь в том, что виджет должен быть отображен на экране, прежде чем изображение может быть захвачено?
4. скорее всего, что у вас там внутри
rendering/proxy_box.dart:3089
? так ли этоfinal OffsetLayer offsetLayer = layer! as OffsetLayer;
?
Ответ №1:
вместо использования RepaintBoundary
с Future.delayed
/ WidgetsBinding.instance.addPostFrameCallback
etc проверьте, как RepaintBoundary
реализовано
через 5-10 минут вы могли бы придумать что-то вроде этого:
class ImageGrab extends SingleChildRenderObjectWidget {
final Function(ByteData) onImageGrab;
ImageGrab({Key key, @required Widget child, @required this.onImageGrab}) : super(key: key, child: child);
@override
RenderImageGrab createRenderObject(BuildContext context) => RenderImageGrab(onImageGrab);
// TODO implement @override updateRenderObject ???
}
class RenderImageGrab extends RenderProxyBox {
final Function(ByteData) onImageGrab;
bool imageGrabbed;
RenderImageGrab(this.onImageGrab);
@override
bool get isRepaintBoundary => true;
@override
void paint(PaintingContext context, Offset offset) {
print('=========== paint $size ===========');
super.paint(context, offset);
imageGrabbed ??= grabImage();
}
bool grabImage() {
(layer as OffsetLayer)
.toImage(Offset.zero amp; size)
.then((image) => image.toByteData(format: ImageByteFormat.png))
.then(onImageGrab);
return true;
}
}
пример использования:
child: ImageGrab(
child: SomeWidgetToGrab(),
onImageGrab: (data) => print('png image length: ${data.lengthInBytes}'),
),
РЕДАКТИРОВАТЬ странная вещь: я тестировал этот код с помощью относительно сложного виджета, и он работал нормально, но с простым Container(color: Colors.red)
'package:flutter/src/rendering/layer.dart': Failed assertion: line 497 pos 12: 'picture != null': is not true.
исключением, поэтому решение состоит в том, чтобы «футурировать» grabImage
метод с помощью:
bool grabImage() {
Future(_grabImage);
return true;
}
_grabImage() {
(layer as OffsetLayer)
.toImage(Offset.zero amp; size)
.then((image) => image.toByteData(format: ImageByteFormat.png))
.then(onImageGrab);
}
Комментарии:
1. Спасибо, что нашли для этого время. Я проверю это. Тем временем я решил проблему, используя этот пакет для определения видимости: pub.dev/packages/visibility_detector — но я вижу, что ваше расширение намного элегантнее.
2. ваше приветствие, кстати, вы могли бы изменить
final Function(ByteData) onImageGrab
наfinal Function(Uint8List) onImageGrab
Uint8List
более удобное в различных классах флаттера (например, вImage.memory
ctor илиFile.writeAsBytes
)
Ответ №2:
Проблема в том, что виджет должен быть видимым и окрашенным, прежде чем вы сможете захватить изображение, поэтому в документах рекомендуется использовать событие жеста для инициирования захвата изображения.
Чтобы решить свою проблему, я использовал эту библиотеку для определения видимости: https://pub.dev/packages/visibility_detector
Обновленный код виджета:
import 'dart:async';
import 'dart:ui' as UI;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:visibility_detector/visibility_detector.dart';
import 'package:weald_ar_trails/model/POI.dart';
import 'package:weald_ar_trails/utils/managers/IconManager.dart';
typedef void ImageCreatedCallback(UI.Image image);
class POIWidget extends StatefulWidget {
const POIWidget({Key key, this.poi, this.imageCreatedCallback})
: super(key: key);
final POI poi;
final ImageCreatedCallback imageCreatedCallback;
@override
_POIWidgetState createState() => _POIWidgetState();
}
class _POIWidgetState extends State<POIWidget> {
/**
* The only purpose of this widget is to provide an Image version of itself
* that can then be used in the MapBox map.
*/
GlobalKey globalKey = GlobalKey();
Future<void> _capturePng() async {
await Future.delayed(const Duration(milliseconds: 100));
RenderRepaintBoundary boundary =
globalKey.currentContext.findRenderObject();
MediaQueryData queryData = MediaQuery.of(context);
double devicePixelRatio = queryData.devicePixelRatio;
UI.Image image = await boundary.toImage(pixelRatio: devicePixelRatio);
widget.imageCreatedCallback(image);
}
@override
Widget build(BuildContext context) {
return VisibilityDetector(
key: globalKey,
onVisibilityChanged: (visibilityInfo) {
var visiblePercentage = visibilityInfo.visibleFraction * 100;
debugPrint(
'Widget ${visibilityInfo.key} is ${visiblePercentage}% visible');
if (visiblePercentage == 100) {
_capturePng();
}
},
child: RepaintBoundary(
key: globalKey,
child: ClipOval(
child: Material(
color: widget.poi.iconBackgroundColor,
child: SizedBox(
width: widget.poi.iconSize.size * 2,
height: widget.poi.iconSize.size * 2,
child: widget.poi.iconName != null
? IconManager.instance.getIconFromName(
widget.poi.iconName,
size: widget.poi.iconSize.size,
color: widget.poi.iconColor,
)
: IconManager.instance.iconMapPin,
),
),
),
),
);
}
}
Это работает, но я бы рекомендовал попробовать ответ выше и расширить SingleChildRenderObjectWidget
его как более элегантное решение, которое не будет полагаться на сторонние пакеты.