blob: 2567ef7dfdd6a52f1b991c4202db129ec1773902 [file] [log] [blame]
import 'dart:convert';
import 'dart:typed_data'; // ignore: unnecessary_import
import 'dart:ui' show Picture;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:xml/xml.dart';
import 'src/avd/xml_parsers.dart';
import 'src/avd_parser.dart';
import 'src/picture_provider.dart';
import 'src/picture_stream.dart';
import 'src/render_picture.dart';
import 'src/vector_drawable.dart';
/// Instance for [Avd]'s utility methods, which can produce [DrawableRoot] or
/// [PictureInfo] objects from [String] and [Uint8List]s.
final Avd avd = Avd._();
/// A set of helper methods for decoding Android Vector Drawables to [Drawable].
///
/// AVD support is experimental and very incomplete. Use at your own risk.
class Avd {
Avd._();
/// Decodes an Android Vector Drawable from a [Uint8List] to a [PictureInfo]
/// object.
Future<PictureInfo> avdPictureDecoder(
Uint8List raw,
bool allowDrawingOutsideOfViewBox,
ColorFilter? colorFilter,
String key) async {
final DrawableRoot avdRoot = await fromAvdBytes(raw, key);
final Picture pic = avdRoot.toPicture(
clipToViewBox: allowDrawingOutsideOfViewBox == true ? false : true,
colorFilter: colorFilter);
return PictureInfo(
picture: pic,
viewport: avdRoot.viewport.viewBoxRect,
compatibilityTester: const CacheCompatibilityTester(),
);
}
/// Decodes an Android Vector Drawable from a [String] to a [PictureInfo]
/// object.
Future<PictureInfo> avdPictureStringDecoder(
String raw,
bool allowDrawingOutsideOfViewBox,
ColorFilter? colorFilter,
String key,
) async {
final DrawableRoot avdRoot = fromAvdString(raw, key);
final Picture pic = avdRoot.toPicture(
clipToViewBox: allowDrawingOutsideOfViewBox == true ? false : true,
colorFilter: colorFilter,
);
return PictureInfo(
picture: pic,
viewport: avdRoot.viewport.viewBoxRect,
size: avdRoot.viewport.size,
compatibilityTester: const CacheCompatibilityTester(),
);
}
/// Produces a [Drawableroot] from a [Uint8List] of AVD byte data (assumes
/// UTF8 encoding).
///
/// The `key` parameter is used for debugging purposes.
Future<DrawableRoot> fromAvdBytes(Uint8List raw, String key) async {
// TODO(dnfield): do utf decoding in another thread?
// Might just have to live with potentially slow(ish) decoding, this is causing errors.
// See: https://github.com/dart-lang/sdk/issues/31954
// See: https://github.com/flutter/flutter/blob/bf3bd7667f07709d0b817ebfcb6972782cfef637/packages/flutter/lib/src/services/asset_bundle.dart#L66
// if (raw.lengthInBytes < 20 * 1024) {
return fromAvdString(utf8.decode(raw), key);
// } else {
// final String str =
// await compute(_utf8Decode, raw, debugLabel: 'UTF8 decode for SVG');
// return fromSvgString(str);
// }
}
// String _utf8Decode(Uint8List data) {
// return utf8.decode(data);
// }
/// Creates a [DrawableRoot] from a string of Android Vector Drawable data.
DrawableRoot fromAvdString(String rawSvg, String key) {
final XmlElement svg = XmlDocument.parse(rawSvg).rootElement;
final DrawableViewport viewBox = parseViewBox(svg.attributes);
final List<Drawable> children = svg.children
.whereType<XmlElement>()
.map((XmlElement child) => parseAvdElement(child, viewBox.viewBoxRect))
.toList();
// todo : style on root
return DrawableRoot(getAttribute(svg.attributes, 'id', def: ''), viewBox,
children, DrawableDefinitionServer(), null);
}
}
/// A widget that draws Android Vector Drawable data into a [Picture] using a
/// [PictureProvider].
///
/// Support for AVD is incomplete and experimental at this time.
class AvdPicture extends StatefulWidget {
/// Instantiates a widget that renders an AVD picture using the `pictureProvider`.
///
/// If `matchTextDirection` is set to true, the picture will be flipped
/// horizontally in [TextDirection.rtl] contexts.
///
/// The `allowDrawingOutsideOfViewBox` parameter should be used with caution -
/// if set to true, it will not clip the canvas used internally to the view box,
/// meaning the picture may draw beyond the intended area and lead to undefined
/// behavior or additional memory overhead.
///
/// A custom `placeholderBuilder` can be specified for cases where decoding or
/// acquiring data may take a noticeably long time, e.g. for a network picture.
const AvdPicture(
this.pictureProvider, {
Key? key,
this.matchTextDirection = false,
this.allowDrawingOutsideViewBox = false,
this.placeholderBuilder,
this.colorFilter,
}) : super(key: key);
/// Draws an [AvdPicture] from a raw string of XML.
AvdPicture.string(String bytes,
{bool matchTextDirection = false,
bool allowDrawingOutsideViewBox = false,
WidgetBuilder? placeholderBuilder,
Color? color,
BlendMode colorBlendMode = BlendMode.srcIn,
Key? key})
: this(
StringPicture(
allowDrawingOutsideViewBox == true
? (_) => avdStringDecoderOutsideViewBox
: (_) => avdStringDecoder,
bytes,
),
colorFilter: _getColorFilter(color, colorBlendMode),
matchTextDirection: matchTextDirection,
allowDrawingOutsideViewBox: allowDrawingOutsideViewBox,
placeholderBuilder: placeholderBuilder,
key: key);
/// Draws an [AvdPicture] from an asset.
AvdPicture.asset(String assetName,
{Key? key,
bool matchTextDirection = false,
AssetBundle? bundle,
String? package,
bool allowDrawingOutsideViewBox = false,
WidgetBuilder? placeholderBuilder,
Color? color,
BlendMode colorBlendMode = BlendMode.srcIn})
: this(
ExactAssetPicture(
allowDrawingOutsideViewBox == true
? (_) => avdStringDecoderOutsideViewBox
: (_) => avdStringDecoder,
assetName,
bundle: bundle,
package: package,
),
colorFilter: _getColorFilter(color, colorBlendMode),
matchTextDirection: matchTextDirection,
allowDrawingOutsideViewBox: allowDrawingOutsideViewBox,
placeholderBuilder: placeholderBuilder,
key: key);
/// The default placeholder for an AVD that may take time to parse or
/// retrieve, e.g. from a network location.
static WidgetBuilder defaultPlaceholderBuilder =
(BuildContext ctx) => const LimitedBox();
static ColorFilter? _getColorFilter(Color? color, BlendMode colorBlendMode) =>
color == null ? null : ColorFilter.mode(color, colorBlendMode);
/// A [PictureInfoDecoder] for [Uint8List]s that will clip to the viewBox.
static final PictureInfoDecoder<Uint8List> avdByteDecoder =
(Uint8List bytes, ColorFilter? colorFilter, String key) =>
avd.avdPictureDecoder(bytes, false, colorFilter, key);
/// A [PictureInfoDecoder] for strings that will clip to the viewBox.
static final PictureInfoDecoder<String> avdStringDecoder =
(String data, ColorFilter? colorFilter, String key) =>
avd.avdPictureStringDecoder(data, false, colorFilter, key);
/// A [PictureInfoDecoder] for [Uint8List]s that will not clip to the viewBox.
static final PictureInfoDecoder<Uint8List> avdByteDecoderOutsideViewBox =
(Uint8List bytes, ColorFilter? colorFilter, String key) =>
avd.avdPictureDecoder(bytes, true, colorFilter, key);
/// A [PictureInfoDecoder] for [String]s that will not clip to the viewBox.
static final PictureInfoDecoder<String> avdStringDecoderOutsideViewBox =
(String data, ColorFilter? colorFilter, String key) =>
avd.avdPictureStringDecoder(data, true, colorFilter, key);
/// The [PictureProvider] used to resolve the AVD.
final PictureProvider pictureProvider;
/// The placeholder to use while fetching, decoding, and parsing the AVD data.
final WidgetBuilder? placeholderBuilder;
/// If true, will horizontally flip the picture in [TextDirection.rtl] contexts.
final bool matchTextDirection;
/// If true, will allow the AVD to be drawn outside of the clip boundary of its
/// viewBox.
final bool allowDrawingOutsideViewBox;
/// The color filter, if any, to apply to this widget.
final ColorFilter? colorFilter;
@override
State<StatefulWidget> createState() => _AvdPictureState();
}
class _AvdPictureState extends State<AvdPicture> {
PictureInfo? _picture;
PictureStream? _pictureStream;
bool _isListeningToStream = false;
@override
void didChangeDependencies() {
_resolveImage();
if (TickerMode.of(context)) {
_listenToStream();
} else {
_stopListeningToStream();
}
super.didChangeDependencies();
}
@override
void didUpdateWidget(AvdPicture oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.pictureProvider != oldWidget.pictureProvider) {
_resolveImage();
}
}
@override
void reassemble() {
_resolveImage(); // in case the image cache was flushed
super.reassemble();
}
void _resolveImage() {
final PictureStream newStream = widget.pictureProvider
.resolve(createLocalPictureConfiguration(context));
assert(newStream != null); // ignore: unnecessary_null_comparison
_updateSourceStream(newStream);
}
void _handleImageChanged(PictureInfo? imageInfo, bool synchronousCall) {
setState(() {
_picture = imageInfo;
});
}
// Update _pictureStream to newStream, and moves the stream listener
// registration from the old stream to the new stream (if a listener was
// registered).
void _updateSourceStream(PictureStream newStream) {
if (_pictureStream?.key == newStream.key) {
return;
}
if (_isListeningToStream)
_pictureStream!.removeListener(_handleImageChanged);
_pictureStream = newStream;
if (_isListeningToStream) {
_pictureStream!.addListener(_handleImageChanged);
}
}
void _listenToStream() {
if (_isListeningToStream) {
return;
}
_pictureStream!.addListener(_handleImageChanged);
_isListeningToStream = true;
}
void _stopListeningToStream() {
if (!_isListeningToStream) {
return;
}
_pictureStream!.removeListener(_handleImageChanged);
_isListeningToStream = false;
}
@override
void dispose() {
assert(_pictureStream != null);
_stopListeningToStream();
super.dispose();
}
@override
Widget build(BuildContext context) {
late Widget child;
if (_picture != null) {
final Rect viewport = Offset.zero & _picture!.viewport.size;
child = SizedBox(
width: viewport.width,
height: viewport.height,
child: FittedBox(
clipBehavior: Clip.hardEdge,
child: SizedBox.fromSize(
size: viewport.size,
child: RawPicture(
_picture,
matchTextDirection: widget.matchTextDirection,
allowDrawingOutsideViewBox: widget.allowDrawingOutsideViewBox,
),
),
),
);
if (widget.pictureProvider.colorFilter == null &&
widget.colorFilter != null) {
child = ColorFiltered(
colorFilter: widget.colorFilter!,
child: child,
);
}
} else {
child = AvdPicture.defaultPlaceholderBuilder(context);
}
return child;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(
DiagnosticsProperty<PictureStream>('stream', _pictureStream),
);
}
}