aves/lib/widgets/common/fx/transition_image.dart
2021-05-13 19:34:23 +09:00

192 lines
5.2 KiB
Dart

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<double> animation;
final bool gaplessPlayback = false;
const TransitionImage({
required this.image,
required this.animation,
this.width,
this.height,
});
@override
_TransitionImageState createState() => _TransitionImageState();
}
class _TransitionImageState extends State<TransitionImage> {
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(covariant 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,
));
_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<double>(
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) {
if (image == null) return;
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;
}