Flutter получает результат BuildContext.findRenderObject

#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 его как более элегантное решение, которое не будет полагаться на сторонние пакеты.