import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; // adapted from `RawImage`, `paintImage()` from `DecorationImagePainter`, etc. // to transition between 2 different fits during hero animation: // - BoxFit.cover at t=0 // - BoxFit.contain at t=1 class TransitionImage extends StatefulWidget { final ImageProvider image; final double width, height; final ValueListenable animation; final gaplessPlayback = false; const TransitionImage({ @required this.image, @required this.animation, this.width, this.height, }); @override _TransitionImageState createState() => _TransitionImageState(); } class _TransitionImageState extends State { ImageStream _imageStream; ImageInfo _imageInfo; bool _isListeningToStream = false; int _frameNumber; @override void initState() { super.initState(); } @override void dispose() { assert(_imageStream != null); _stopListeningToStream(); super.dispose(); } @override void didChangeDependencies() { _resolveImage(); if (TickerMode.of(context)) { _listenToStream(); } else { _stopListeningToStream(); } super.didChangeDependencies(); } @override void didUpdateWidget(TransitionImage oldWidget) { super.didUpdateWidget(oldWidget); if (_isListeningToStream) { _imageStream.removeListener(_getListener()); _imageStream.addListener(_getListener()); } if (widget.image != oldWidget.image) _resolveImage(); } @override void reassemble() { _resolveImage(); // in case the image cache was flushed super.reassemble(); } void _resolveImage() { final provider = widget.image; final newStream = provider.resolve(createLocalImageConfiguration( context, size: widget.width != null && widget.height != null ? Size(widget.width, widget.height) : null, )); assert(newStream != null); _updateSourceStream(newStream); } ImageStreamListener _getListener() { return ImageStreamListener( _handleImageFrame, onChunk: null, ); } void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) { setState(() { _imageInfo = imageInfo; _frameNumber = _frameNumber == null ? 0 : _frameNumber + 1; }); } // Updates _imageStream to newStream, and moves the stream listener // registration from the old stream to the new stream (if a listener was // registered). void _updateSourceStream(ImageStream newStream) { if (_imageStream?.key == newStream?.key) return; if (_isListeningToStream) _imageStream.removeListener(_getListener()); if (!widget.gaplessPlayback) { setState(() { _imageInfo = null; }); } setState(() { _frameNumber = null; }); _imageStream = newStream; if (_isListeningToStream) _imageStream.addListener(_getListener()); } void _listenToStream() { if (_isListeningToStream) return; _imageStream.addListener(_getListener()); _isListeningToStream = true; } void _stopListeningToStream() { if (!_isListeningToStream) return; _imageStream.removeListener(_getListener()); _isListeningToStream = false; } @override Widget build(BuildContext context) { return ValueListenableBuilder( valueListenable: widget.animation, builder: (context, t, child) => CustomPaint( painter: _TransitionImagePainter( // AssetImage(name).resolve(configuration) image: _imageInfo?.image, scale: _imageInfo?.scale ?? 1.0, t: t, ), ), ); } } class _TransitionImagePainter extends CustomPainter { final ui.Image image; final double scale; final double t; const _TransitionImagePainter({ @required this.image, @required this.scale, @required this.t, }); @override void paint(Canvas canvas, Size size) { final paint = Paint() ..isAntiAlias = false ..filterQuality = FilterQuality.low; const alignment = Alignment.center; final rect = ui.Rect.fromLTWH(0, 0, size.width, size.height); final inputSize = Size(image.width.toDouble(), image.height.toDouble()); final outputSize = rect.size; final coverSizes = applyBoxFit(BoxFit.cover, inputSize / scale, size); final containSizes = applyBoxFit(BoxFit.contain, inputSize / scale, size); final sourceSize = Size.lerp(coverSizes.source, containSizes.source, t) * scale; final destinationSize = Size.lerp(coverSizes.destination, containSizes.destination, t); final halfWidthDelta = (outputSize.width - destinationSize.width) / 2.0; final halfHeightDelta = (outputSize.height - destinationSize.height) / 2.0; final dx = halfWidthDelta + alignment.x * halfWidthDelta; final dy = halfHeightDelta + alignment.y * halfHeightDelta; final destinationPosition = rect.topLeft.translate(dx, dy); final destinationRect = destinationPosition & destinationSize; final sourceRect = alignment.inscribe( sourceSize, Offset.zero & inputSize, ); canvas.drawImageRect(image, sourceRect, destinationRect, paint); } @override bool shouldRepaint(CustomPainter oldDelegate) => true; }