#1172 viewer: hero pop/push diversion animation issue workaround
This commit is contained in:
parent
c9663fea19
commit
b97000e8e4
3 changed files with 28 additions and 152 deletions
|
@ -249,6 +249,11 @@ class _CollectionGridContentState extends State<_CollectionGridContent> {
|
||||||
Future<void> _goToViewer(CollectionLens collection, AvesEntry entry) async {
|
Future<void> _goToViewer(CollectionLens collection, AvesEntry entry) async {
|
||||||
// track viewer entry for dynamic hero placeholder
|
// track viewer entry for dynamic hero placeholder
|
||||||
final viewerEntryNotifier = context.read<ViewerEntryNotifier>();
|
final viewerEntryNotifier = context.read<ViewerEntryNotifier>();
|
||||||
|
|
||||||
|
// prevent navigating again to the same entry until fully back,
|
||||||
|
// as a workaround for the hero pop/push diversion animation issue
|
||||||
|
// (cf `ThumbnailImage` `Hero` usage)
|
||||||
|
if (viewerEntryNotifier.value == entry) return;
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => viewerEntryNotifier.value = entry);
|
WidgetsBinding.instance.addPostFrameCallback((_) => viewerEntryNotifier.value = entry);
|
||||||
|
|
||||||
final selection = context.read<Selection<AvesEntry>>();
|
final selection = context.read<Selection<AvesEntry>>();
|
||||||
|
|
|
@ -3,7 +3,6 @@ import 'dart:ui' as ui;
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:flutter/semantics.dart';
|
|
||||||
|
|
||||||
// adapted from Flutter `_ImageState` in `/widgets/image.dart`
|
// adapted from Flutter `_ImageState` in `/widgets/image.dart`
|
||||||
// and `DecorationImagePainter` in `/painting/decoration_image.dart`
|
// and `DecorationImagePainter` in `/painting/decoration_image.dart`
|
||||||
|
@ -15,13 +14,6 @@ class TransitionImage extends StatefulWidget {
|
||||||
final ImageProvider image;
|
final ImageProvider image;
|
||||||
final ValueListenable<double> animation;
|
final ValueListenable<double> animation;
|
||||||
final BoxFit thumbnailFit, viewerFit;
|
final BoxFit thumbnailFit, viewerFit;
|
||||||
final ImageFrameBuilder? frameBuilder;
|
|
||||||
final ImageLoadingBuilder? loadingBuilder;
|
|
||||||
final ImageErrorWidgetBuilder? errorBuilder;
|
|
||||||
final String? semanticLabel;
|
|
||||||
final bool excludeFromSemantics;
|
|
||||||
final double? width, height;
|
|
||||||
final bool gaplessPlayback = false;
|
|
||||||
final Color? background;
|
final Color? background;
|
||||||
|
|
||||||
const TransitionImage({
|
const TransitionImage({
|
||||||
|
@ -30,13 +22,6 @@ class TransitionImage extends StatefulWidget {
|
||||||
required this.animation,
|
required this.animation,
|
||||||
required this.thumbnailFit,
|
required this.thumbnailFit,
|
||||||
required this.viewerFit,
|
required this.viewerFit,
|
||||||
this.frameBuilder,
|
|
||||||
this.loadingBuilder,
|
|
||||||
this.errorBuilder,
|
|
||||||
this.semanticLabel,
|
|
||||||
this.excludeFromSemantics = false,
|
|
||||||
this.width,
|
|
||||||
this.height,
|
|
||||||
this.background,
|
this.background,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -47,13 +32,9 @@ class TransitionImage extends StatefulWidget {
|
||||||
class _TransitionImageState extends State<TransitionImage> with WidgetsBindingObserver {
|
class _TransitionImageState extends State<TransitionImage> with WidgetsBindingObserver {
|
||||||
ImageStream? _imageStream;
|
ImageStream? _imageStream;
|
||||||
ImageInfo? _imageInfo;
|
ImageInfo? _imageInfo;
|
||||||
ImageChunkEvent? _loadingProgress;
|
|
||||||
bool _isListeningToStream = false;
|
bool _isListeningToStream = false;
|
||||||
int? _frameNumber;
|
|
||||||
bool _wasSynchronouslyLoaded = false;
|
bool _wasSynchronouslyLoaded = false;
|
||||||
late DisposableBuildContext<State<TransitionImage>> _scrollAwareContext;
|
late DisposableBuildContext<State<TransitionImage>> _scrollAwareContext;
|
||||||
Object? _lastException;
|
|
||||||
StackTrace? _lastStack;
|
|
||||||
ImageStreamCompleterHandle? _completerHandle;
|
ImageStreamCompleterHandle? _completerHandle;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -90,11 +71,6 @@ class _TransitionImageState extends State<TransitionImage> with WidgetsBindingOb
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(TransitionImage oldWidget) {
|
void didUpdateWidget(TransitionImage oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (_isListeningToStream && (widget.loadingBuilder == null) != (oldWidget.loadingBuilder == null)) {
|
|
||||||
final ImageStreamListener oldListener = _getListener();
|
|
||||||
_imageStream!.addListener(_getListener(recreateListener: true));
|
|
||||||
_imageStream!.removeListener(oldListener);
|
|
||||||
}
|
|
||||||
if (widget.image != oldWidget.image) {
|
if (widget.image != oldWidget.image) {
|
||||||
_resolveImage();
|
_resolveImage();
|
||||||
}
|
}
|
||||||
|
@ -111,10 +87,7 @@ class _TransitionImageState extends State<TransitionImage> with WidgetsBindingOb
|
||||||
context: _scrollAwareContext,
|
context: _scrollAwareContext,
|
||||||
imageProvider: widget.image,
|
imageProvider: widget.image,
|
||||||
);
|
);
|
||||||
final ImageStream newStream = provider.resolve(createLocalImageConfiguration(
|
final ImageStream newStream = provider.resolve(createLocalImageConfiguration(context));
|
||||||
context,
|
|
||||||
size: widget.width != null && widget.height != null ? Size(widget.width!, widget.height!) : null,
|
|
||||||
));
|
|
||||||
_updateSourceStream(newStream);
|
_updateSourceStream(newStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,27 +95,7 @@ class _TransitionImageState extends State<TransitionImage> with WidgetsBindingOb
|
||||||
|
|
||||||
ImageStreamListener _getListener({bool recreateListener = false}) {
|
ImageStreamListener _getListener({bool recreateListener = false}) {
|
||||||
if (_imageStreamListener == null || recreateListener) {
|
if (_imageStreamListener == null || recreateListener) {
|
||||||
_lastException = null;
|
_imageStreamListener = ImageStreamListener(_handleImageFrame);
|
||||||
_lastStack = null;
|
|
||||||
_imageStreamListener = ImageStreamListener(
|
|
||||||
_handleImageFrame,
|
|
||||||
onChunk: widget.loadingBuilder == null ? null : _handleImageChunk,
|
|
||||||
onError: widget.errorBuilder != null || kDebugMode
|
|
||||||
? (error, stackTrace) {
|
|
||||||
setState(() {
|
|
||||||
_lastException = error;
|
|
||||||
_lastStack = stackTrace;
|
|
||||||
});
|
|
||||||
assert(() {
|
|
||||||
if (widget.errorBuilder == null) {
|
|
||||||
// ignore: only_throw_errors, since we're just proxying the error.
|
|
||||||
throw error; // Ensures the error message is printed to the console.
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}());
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return _imageStreamListener!;
|
return _imageStreamListener!;
|
||||||
}
|
}
|
||||||
|
@ -150,23 +103,10 @@ class _TransitionImageState extends State<TransitionImage> with WidgetsBindingOb
|
||||||
void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) {
|
void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_replaceImage(info: imageInfo);
|
_replaceImage(info: imageInfo);
|
||||||
_loadingProgress = null;
|
|
||||||
_lastException = null;
|
|
||||||
_lastStack = null;
|
|
||||||
_frameNumber = _frameNumber == null ? 0 : _frameNumber! + 1;
|
|
||||||
_wasSynchronouslyLoaded = _wasSynchronouslyLoaded | synchronousCall;
|
_wasSynchronouslyLoaded = _wasSynchronouslyLoaded | synchronousCall;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleImageChunk(ImageChunkEvent event) {
|
|
||||||
assert(widget.loadingBuilder != null);
|
|
||||||
setState(() {
|
|
||||||
_loadingProgress = event;
|
|
||||||
_lastException = null;
|
|
||||||
_lastStack = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _replaceImage({required ImageInfo? info}) {
|
void _replaceImage({required ImageInfo? info}) {
|
||||||
final ImageInfo? oldImageInfo = _imageInfo;
|
final ImageInfo? oldImageInfo = _imageInfo;
|
||||||
SchedulerBinding.instance.addPostFrameCallback((_) => oldImageInfo?.dispose());
|
SchedulerBinding.instance.addPostFrameCallback((_) => oldImageInfo?.dispose());
|
||||||
|
@ -185,15 +125,8 @@ class _TransitionImageState extends State<TransitionImage> with WidgetsBindingOb
|
||||||
_imageStream!.removeListener(_getListener());
|
_imageStream!.removeListener(_getListener());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!widget.gaplessPlayback) {
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_replaceImage(info: null);
|
_replaceImage(info: null);
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_loadingProgress = null;
|
|
||||||
_frameNumber = null;
|
|
||||||
_wasSynchronouslyLoaded = false;
|
_wasSynchronouslyLoaded = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -235,46 +168,9 @@ class _TransitionImageState extends State<TransitionImage> with WidgetsBindingOb
|
||||||
_isListeningToStream = false;
|
_isListeningToStream = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _debugBuildErrorWidget(BuildContext context, Object error) {
|
|
||||||
return Stack(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
children: <Widget>[
|
|
||||||
const Positioned.fill(
|
|
||||||
child: Placeholder(
|
|
||||||
color: Color(0xCF8D021F),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(4.0),
|
|
||||||
child: FittedBox(
|
|
||||||
child: Text(
|
|
||||||
'$error',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
style: const TextStyle(
|
|
||||||
shadows: <Shadow>[
|
|
||||||
Shadow(blurRadius: 1.0),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_lastException != null) {
|
return ValueListenableBuilder<double>(
|
||||||
if (widget.errorBuilder != null) {
|
|
||||||
return widget.errorBuilder!(context, _lastException!, _lastStack);
|
|
||||||
}
|
|
||||||
if (kDebugMode) {
|
|
||||||
return _debugBuildErrorWidget(context, _lastException!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget result = ValueListenableBuilder<double>(
|
|
||||||
valueListenable: widget.animation,
|
valueListenable: widget.animation,
|
||||||
builder: (context, t, child) => CustomPaint(
|
builder: (context, t, child) => CustomPaint(
|
||||||
painter: _TransitionImagePainter(
|
painter: _TransitionImagePainter(
|
||||||
|
@ -287,35 +183,6 @@ class _TransitionImageState extends State<TransitionImage> with WidgetsBindingOb
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!widget.excludeFromSemantics) {
|
|
||||||
result = Semantics(
|
|
||||||
container: widget.semanticLabel != null,
|
|
||||||
image: true,
|
|
||||||
label: widget.semanticLabel ?? '',
|
|
||||||
child: result,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (widget.frameBuilder != null) {
|
|
||||||
result = widget.frameBuilder!(context, result, _frameNumber, _wasSynchronouslyLoaded);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (widget.loadingBuilder != null) {
|
|
||||||
result = widget.loadingBuilder!(context, result, _loadingProgress);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void debugFillProperties(DiagnosticPropertiesBuilder description) {
|
|
||||||
super.debugFillProperties(description);
|
|
||||||
description.add(DiagnosticsProperty<ImageStream>('stream', _imageStream));
|
|
||||||
description.add(DiagnosticsProperty<ImageInfo>('pixels', _imageInfo));
|
|
||||||
description.add(DiagnosticsProperty<ImageChunkEvent>('loadingProgress', _loadingProgress));
|
|
||||||
description.add(DiagnosticsProperty<int>('frameNumber', _frameNumber));
|
|
||||||
description.add(DiagnosticsProperty<bool>('wasSynchronouslyLoaded', _wasSynchronouslyLoaded));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -325,6 +192,11 @@ class _TransitionImagePainter extends CustomPainter {
|
||||||
final Color? background;
|
final Color? background;
|
||||||
final BoxFit thumbnailFit, viewerFit;
|
final BoxFit thumbnailFit, viewerFit;
|
||||||
|
|
||||||
|
static final _paint = Paint()
|
||||||
|
..isAntiAlias = false
|
||||||
|
..filterQuality = FilterQuality.low;
|
||||||
|
static const _alignment = Alignment.center;
|
||||||
|
|
||||||
const _TransitionImagePainter({
|
const _TransitionImagePainter({
|
||||||
required this.image,
|
required this.image,
|
||||||
required this.scale,
|
required this.scale,
|
||||||
|
@ -336,20 +208,15 @@ class _TransitionImagePainter extends CustomPainter {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void paint(Canvas canvas, Size size) {
|
void paint(Canvas canvas, Size size) {
|
||||||
if (image == null) return;
|
final _image = image;
|
||||||
|
if (_image == null) return;
|
||||||
|
|
||||||
final paint = Paint()
|
if (size.isEmpty) {
|
||||||
..isAntiAlias = false
|
|
||||||
..filterQuality = FilterQuality.low;
|
|
||||||
const alignment = Alignment.center;
|
|
||||||
|
|
||||||
final rect = Offset.zero & size;
|
|
||||||
if (rect.isEmpty) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final outputSize = rect.size;
|
final outputSize = size;
|
||||||
final inputSize = Size(image!.width.toDouble(), image!.height.toDouble());
|
final inputSize = Size(_image.width.toDouble(), _image.height.toDouble());
|
||||||
|
|
||||||
final thumbnailSizes = applyBoxFit(thumbnailFit, inputSize / scale, size);
|
final thumbnailSizes = applyBoxFit(thumbnailFit, inputSize / scale, size);
|
||||||
final viewerSizes = applyBoxFit(viewerFit, inputSize / scale, size);
|
final viewerSizes = applyBoxFit(viewerFit, inputSize / scale, size);
|
||||||
|
@ -358,11 +225,12 @@ class _TransitionImagePainter extends CustomPainter {
|
||||||
|
|
||||||
final halfWidthDelta = (outputSize.width - destinationSize.width) / 2.0;
|
final halfWidthDelta = (outputSize.width - destinationSize.width) / 2.0;
|
||||||
final halfHeightDelta = (outputSize.height - destinationSize.height) / 2.0;
|
final halfHeightDelta = (outputSize.height - destinationSize.height) / 2.0;
|
||||||
final dx = halfWidthDelta + alignment.x * halfWidthDelta;
|
final dx = halfWidthDelta + _alignment.x * halfWidthDelta;
|
||||||
final dy = halfHeightDelta + alignment.y * halfHeightDelta;
|
final dy = halfHeightDelta + _alignment.y * halfHeightDelta;
|
||||||
final destinationPosition = rect.topLeft.translate(dx, dy);
|
final destinationPosition = Offset(dx, dy);
|
||||||
|
|
||||||
final destinationRect = destinationPosition & destinationSize;
|
final destinationRect = destinationPosition & destinationSize;
|
||||||
final sourceRect = alignment.inscribe(
|
final sourceRect = _alignment.inscribe(
|
||||||
sourceSize,
|
sourceSize,
|
||||||
Offset.zero & inputSize,
|
Offset.zero & inputSize,
|
||||||
);
|
);
|
||||||
|
@ -370,7 +238,7 @@ class _TransitionImagePainter extends CustomPainter {
|
||||||
// deflate to avoid background artifact around opaque image
|
// deflate to avoid background artifact around opaque image
|
||||||
canvas.drawRect(destinationRect.deflate(1), Paint()..color = background!);
|
canvas.drawRect(destinationRect.deflate(1), Paint()..color = background!);
|
||||||
}
|
}
|
||||||
canvas.drawImageRect(image!, sourceRect, destinationRect, paint);
|
canvas.drawImageRect(_image, sourceRect, destinationRect, _paint);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
@ -270,6 +270,9 @@ class _ThumbnailImageState extends State<ThumbnailImage> {
|
||||||
image = Hero(
|
image = Hero(
|
||||||
tag: heroTag,
|
tag: heroTag,
|
||||||
flightShuttleBuilder: (flightContext, animation, flightDirection, fromHeroContext, toHeroContext) {
|
flightShuttleBuilder: (flightContext, animation, flightDirection, fromHeroContext, toHeroContext) {
|
||||||
|
// as of Flutter v3.27.0-0.1.pre, the flight `animation` is incorrect when diverting a pop:
|
||||||
|
// - diverting a push (t = 0 -> 1) with a pop (t = 1 -> 0) works as expected (t = 0 -> [0,1] -> 0)
|
||||||
|
// - diverting a pop (t = 1 -> 0) with a push (t = 0 -> 1) finishes the pop (t = 1 -> [0,1] -> 0) instead of diverting (t = 1 -> [0,1] -> 1)
|
||||||
Widget child = TransitionImage(
|
Widget child = TransitionImage(
|
||||||
image: entry.bestCachedThumbnail,
|
image: entry.bestCachedThumbnail,
|
||||||
animation: animation,
|
animation: animation,
|
||||||
|
|
Loading…
Reference in a new issue