From 6cdd8607626f789ac1dcdf97b2307fd4bf9390d6 Mon Sep 17 00:00:00 2001 From: Renan Date: Sat, 22 Aug 2020 13:03:57 +0100 Subject: [PATCH] Refactor image retrieval (#320) This makes the image retrieval process not rely on a FutureBuilder. Previously we recreated Image behavior by showing a loader whilst the image loads, using a completer to detect when the image had finished load. Now we rely only on the good old state and some ifs in the build method. Also, we have split that code from the custom child code. The main widget had logics for both image and custom child modes. Now we split that into two internal widgets, the wrappers. This should resolve the following issues #316 #303 --- .../lib/screens/examples/hero_example.dart | 14 +- .../lib/screens/examples/network-images.dart | 5 +- lib/photo_view.dart | 231 ++++----------- lib/src/photo_view_wrappers.dart | 280 ++++++++++++++++++ 4 files changed, 356 insertions(+), 174 deletions(-) create mode 100644 lib/src/photo_view_wrappers.dart diff --git a/example/lib/screens/examples/hero_example.dart b/example/lib/screens/examples/hero_example.dart index 736c7bdd..6a5033c6 100644 --- a/example/lib/screens/examples/hero_example.dart +++ b/example/lib/screens/examples/hero_example.dart @@ -15,7 +15,9 @@ class HeroExample extends StatelessWidget { context, MaterialPageRoute( builder: (context) => const HeroPhotoViewRouteWrapper( - imageProvider: AssetImage("assets/large-image.jpg"), + imageProvider: NetworkImage( + "https://source.unsplash.com/4900x3600/?camera,paper", + ), ), ), ); @@ -23,7 +25,12 @@ class HeroExample extends StatelessWidget { child: Container( child: Hero( tag: "someTag", - child: Image.asset("assets/large-image.jpg", width: 150.0), + child: Image.network( + "https://source.unsplash.com/4900x3600/?camera,paper", + width: 350.0, + loadingBuilder: (_, child, chunk) => + chunk != null ? const Text("loading") : child, + ), ), ), ), @@ -35,14 +42,12 @@ class HeroExample extends StatelessWidget { class HeroPhotoViewRouteWrapper extends StatelessWidget { const HeroPhotoViewRouteWrapper({ this.imageProvider, - this.loadingBuilder, this.backgroundDecoration, this.minScale, this.maxScale, }); final ImageProvider imageProvider; - final LoadingBuilder loadingBuilder; final Decoration backgroundDecoration; final dynamic minScale; final dynamic maxScale; @@ -55,7 +60,6 @@ class HeroPhotoViewRouteWrapper extends StatelessWidget { ), child: PhotoView( imageProvider: imageProvider, - loadingBuilder: loadingBuilder, backgroundDecoration: backgroundDecoration, minScale: minScale, maxScale: maxScale, diff --git a/example/lib/screens/examples/network-images.dart b/example/lib/screens/examples/network-images.dart index 6bad6041..db75d05b 100644 --- a/example/lib/screens/examples/network-images.dart +++ b/example/lib/screens/examples/network-images.dart @@ -19,7 +19,8 @@ class NetworkExamples extends StatelessWidget { MaterialPageRoute( builder: (context) => CommonExampleRouteWrapper( imageProvider: const NetworkImage( - "https://source.unsplash.com/1900x3600/?camera,paper"), + "https://source.unsplash.com/1900x3600/?camera,paper", + ), loadingBuilder: (context, event) { if (event == null) { return const Center( @@ -40,7 +41,7 @@ class NetworkExamples extends StatelessWidget { }, ), ExampleButtonNode( - title: "Image from the internet (with custom loader)", + title: "Error image", onPressed: () { Navigator.push( context, diff --git a/lib/photo_view.dart b/lib/photo_view.dart index c5ed9ce7..a84ccd92 100644 --- a/lib/photo_view.dart +++ b/lib/photo_view.dart @@ -1,17 +1,14 @@ library photo_view; -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:photo_view/src/controller/photo_view_controller.dart'; import 'package:photo_view/src/controller/photo_view_scalestate_controller.dart'; import 'package:photo_view/src/core/photo_view_core.dart'; import 'package:photo_view/src/photo_view_computed_scale.dart'; -import 'package:photo_view/src/photo_view_default_widgets.dart'; import 'package:photo_view/src/photo_view_scale_state.dart'; +import 'package:photo_view/src/photo_view_wrappers.dart'; import 'package:photo_view/src/utils/photo_view_hero_attributes.dart'; -import 'package:photo_view/src/utils/photo_view_utils.dart'; export 'src/controller/photo_view_controller.dart'; export 'src/controller/photo_view_scalestate_controller.dart'; @@ -259,6 +256,7 @@ class PhotoView extends StatefulWidget { this.tightMode, this.filterQuality, this.disableGestures, + this.errorBuilder, }) : child = null, childSize = null, super(key: key); @@ -292,6 +290,7 @@ class PhotoView extends StatefulWidget { this.filterQuality, this.disableGestures, }) : loadFailedChild = null, + errorBuilder = null, imageProvider = null, gaplessPlayback = false, loadingBuilder = null, @@ -306,6 +305,10 @@ class PhotoView extends StatefulWidget { final LoadingBuilder loadingBuilder; /// Show loadFailedChild when the image failed to load + final ImageErrorWidgetBuilder errorBuilder; + + /// Show loadFailedChild when the image failed to load + @Deprecated("Use errorBuilder instead") final Widget loadFailedChild; /// Changes the background behind image, defaults to `Colors.black`. @@ -385,6 +388,10 @@ class PhotoView extends StatefulWidget { // Useful when custom gesture detector is used in child widget. final bool disableGestures; + bool get _isCustomChild { + return child != null; + } + @override State createState() { return _PhotoViewState(); @@ -392,67 +399,18 @@ class PhotoView extends StatefulWidget { } class _PhotoViewState extends State { - Size _childSize; - bool _loading; - ImageChunkEvent _imageChunkEvent; + // image retrieval + // controller bool _controlledController; PhotoViewControllerBase _controller; - bool _controlledScaleStateController; PhotoViewScaleStateController _scaleStateController; - Future _getImage() { - final Completer completer = Completer(); - final ImageStream stream = widget.imageProvider.resolve( - const ImageConfiguration(), - ); - final listener = ImageStreamListener(( - ImageInfo info, - bool synchronousCall, - ) { - if (completer.isCompleted) { - return; - } - completer.complete(info); - if (mounted) { - final setupCallback = () { - _childSize = Size( - info.image.width.toDouble(), - info.image.height.toDouble(), - ); - _loading = false; - _imageChunkEvent = null; - }; - synchronousCall ? setupCallback() : setState(setupCallback); - } - }, onChunk: (event) { - if (mounted) { - setState(() => _imageChunkEvent = event); - } - }, onError: (exception, stackTrace) { - if (completer.isCompleted) { - return; - } - completer.completeError(exception, stackTrace); - }); - stream.addListener(listener); - completer.future.then((_) { - stream.removeListener(listener); - }); - return completer.future; - } - @override void initState() { super.initState(); - if (widget.child == null) { - _getImage(); - } else { - _childSize = widget.childSize; - _loading = false; - _imageChunkEvent = null; - } + if (widget.controller == null) { _controlledController = true; _controller = PhotoViewController(); @@ -474,11 +432,6 @@ class _PhotoViewState extends State { @override void didUpdateWidget(PhotoView oldWidget) { - if (oldWidget.childSize != widget.childSize && widget.childSize != null) { - setState(() { - _childSize = widget.childSize; - }); - } if (widget.controller == null) { if (!_controlledController) { _controlledController = true; @@ -525,114 +478,58 @@ class _PhotoViewState extends State { BuildContext context, BoxConstraints constraints, ) { - return widget.child == null - ? _buildImage(context, constraints) - : _buildCustomChild(context, constraints); + final computedOuterSize = widget.customSize ?? constraints.biggest; + + return widget._isCustomChild + ? CustomChildWrapper( + child: widget.child, + childSize: widget.childSize, + backgroundDecoration: widget.backgroundDecoration, + heroAttributes: widget.heroAttributes, + scaleStateChangedCallback: widget.scaleStateChangedCallback, + enableRotation: widget.enableRotation, + controller: _controller, + scaleStateController: _scaleStateController, + maxScale: widget.maxScale, + minScale: widget.minScale, + initialScale: widget.initialScale, + basePosition: widget.basePosition, + scaleStateCycle: widget.scaleStateCycle, + onTapUp: widget.onTapUp, + onTapDown: widget.onTapDown, + outerSize: computedOuterSize, + gestureDetectorBehavior: widget.gestureDetectorBehavior, + tightMode: widget.tightMode, + filterQuality: widget.filterQuality, + disableGestures: widget.disableGestures, + ) + : ImageWrapper( + imageProvider: widget.imageProvider, + loadingBuilder: widget.loadingBuilder, + loadFailedChild: widget.loadFailedChild, + backgroundDecoration: widget.backgroundDecoration, + gaplessPlayback: widget.gaplessPlayback, + heroAttributes: widget.heroAttributes, + scaleStateChangedCallback: widget.scaleStateChangedCallback, + enableRotation: widget.enableRotation, + controller: _controller, + scaleStateController: _scaleStateController, + maxScale: widget.maxScale, + minScale: widget.minScale, + initialScale: widget.initialScale, + basePosition: widget.basePosition, + scaleStateCycle: widget.scaleStateCycle, + onTapUp: widget.onTapUp, + onTapDown: widget.onTapDown, + outerSize: computedOuterSize, + gestureDetectorBehavior: widget.gestureDetectorBehavior, + tightMode: widget.tightMode, + filterQuality: widget.filterQuality, + disableGestures: widget.disableGestures, + ); }, ); } - - Widget _buildCustomChild(BuildContext context, BoxConstraints constraints) { - final _computedOuterSize = widget.customSize ?? constraints.biggest; - - final scaleBoundaries = ScaleBoundaries( - widget.minScale ?? 0.0, - widget.maxScale ?? double.infinity, - widget.initialScale ?? PhotoViewComputedScale.contained, - _computedOuterSize, - _childSize ?? constraints.biggest, - ); - - return PhotoViewCore.customChild( - customChild: widget.child, - backgroundDecoration: widget.backgroundDecoration, - enableRotation: widget.enableRotation, - heroAttributes: widget.heroAttributes, - controller: _controller, - scaleStateController: _scaleStateController, - scaleStateCycle: widget.scaleStateCycle ?? defaultScaleStateCycle, - basePosition: widget.basePosition ?? Alignment.center, - scaleBoundaries: scaleBoundaries, - onTapUp: widget.onTapUp, - onTapDown: widget.onTapDown, - gestureDetectorBehavior: widget.gestureDetectorBehavior, - tightMode: widget.tightMode ?? false, - filterQuality: widget.filterQuality ?? FilterQuality.none, - disableGestures: widget.disableGestures ?? false, - ); - } - - Widget _buildImage(BuildContext context, BoxConstraints constraints) { - return widget.heroAttributes == null - ? _buildAsync(context, constraints) - : _buildSync(context, constraints); - } - - Widget _buildAsync(BuildContext context, BoxConstraints constraints) { - return FutureBuilder( - future: _getImage(), - builder: (BuildContext context, AsyncSnapshot info) { - if (info.hasError) { - return _buildLoadFailed(); - } - if (info.hasData) { - return _buildWrapperImage(context, constraints); - } - return _buildLoading(); - }); - } - - Widget _buildSync(BuildContext context, BoxConstraints constraints) { - if (_loading == null) { - return _buildLoading(); - } - return _buildWrapperImage(context, constraints); - } - - Widget _buildWrapperImage(BuildContext context, BoxConstraints constraints) { - final _computedOuterSize = widget.customSize ?? constraints.biggest; - - final scaleBoundaries = ScaleBoundaries( - widget.minScale ?? 0.0, - widget.maxScale ?? double.infinity, - widget.initialScale ?? PhotoViewComputedScale.contained, - _computedOuterSize, - _childSize, - ); - - return PhotoViewCore( - imageProvider: widget.imageProvider, - backgroundDecoration: widget.backgroundDecoration, - gaplessPlayback: widget.gaplessPlayback, - enableRotation: widget.enableRotation, - heroAttributes: widget.heroAttributes, - basePosition: widget.basePosition ?? Alignment.center, - controller: _controller, - scaleStateController: _scaleStateController, - scaleStateCycle: widget.scaleStateCycle ?? defaultScaleStateCycle, - scaleBoundaries: scaleBoundaries, - onTapUp: widget.onTapUp, - onTapDown: widget.onTapDown, - gestureDetectorBehavior: widget.gestureDetectorBehavior, - tightMode: widget.tightMode ?? false, - filterQuality: widget.filterQuality ?? FilterQuality.none, - disableGestures: widget.disableGestures ?? false, - ); - } - - Widget _buildLoading() { - if (widget.loadingBuilder != null) { - return widget.loadingBuilder(context, _imageChunkEvent); - } - - return PhotoViewDefaultLoading( - event: _imageChunkEvent, - ); - } - - Widget _buildLoadFailed() { - return widget.loadFailedChild ?? PhotoViewDefaultError(); - } } /// The default [ScaleStateCycle] @@ -673,7 +570,7 @@ typedef PhotoViewImageTapDownCallback = Function( PhotoViewControllerValue controllerValue, ); -/// A type definition for a callback to show a widget while a image is loading, a [ImageChunkEvent] is passed to inform progress +/// A type definition for a callback to show a widget while the image is loading, a [ImageChunkEvent] is passed to inform progress typedef LoadingBuilder = Widget Function( BuildContext context, ImageChunkEvent event, diff --git a/lib/src/photo_view_wrappers.dart b/lib/src/photo_view_wrappers.dart new file mode 100644 index 00000000..a3332224 --- /dev/null +++ b/lib/src/photo_view_wrappers.dart @@ -0,0 +1,280 @@ +import 'package:flutter/widgets.dart'; + +import '../photo_view.dart'; +import 'core/photo_view_core.dart'; +import 'photo_view_default_widgets.dart'; +import 'utils/photo_view_utils.dart'; + +class ImageWrapper extends StatefulWidget { + const ImageWrapper({ + Key key, + this.imageProvider, + this.loadingBuilder, + this.loadFailedChild, + this.backgroundDecoration, + this.gaplessPlayback = false, + this.heroAttributes, + this.scaleStateChangedCallback, + this.enableRotation = false, + this.controller, + this.scaleStateController, + this.maxScale, + this.minScale, + this.initialScale, + this.basePosition, + this.scaleStateCycle, + this.onTapUp, + this.onTapDown, + this.outerSize, + this.gestureDetectorBehavior, + this.tightMode, + this.filterQuality, + this.disableGestures, + this.errorBuilder, + }) : super(key: key); + + final ImageProvider imageProvider; + final LoadingBuilder loadingBuilder; + final ImageErrorWidgetBuilder errorBuilder; + final Widget loadFailedChild; + final Decoration backgroundDecoration; + final bool gaplessPlayback; + final PhotoViewHeroAttributes heroAttributes; + final ValueChanged scaleStateChangedCallback; + final bool enableRotation; + final dynamic maxScale; + final dynamic minScale; + final dynamic initialScale; + final PhotoViewControllerBase controller; + final PhotoViewScaleStateController scaleStateController; + final Alignment basePosition; + final ScaleStateCycle scaleStateCycle; + + final PhotoViewImageTapUpCallback onTapUp; + final PhotoViewImageTapDownCallback onTapDown; + + final Size outerSize; + final HitTestBehavior gestureDetectorBehavior; + final bool tightMode; + final FilterQuality filterQuality; + final bool disableGestures; + + @override + _ImageWrapperState createState() => _ImageWrapperState(); +} + +class _ImageWrapperState extends State { + ImageStreamListener _imageStreamListener; + ImageStream _stream; + ImageChunkEvent _imageChunkEvent; + ImageInfo _imageInfo; + bool _loading = true; + Size _imageSize; + Object _lastException; + StackTrace _stackTrace; + + // retrieve image from the provider + void _getImage() { + _stream = widget.imageProvider.resolve( + const ImageConfiguration(), + ); + + void handleImageChunk(ImageChunkEvent event) { + assert(widget.loadingBuilder != null); + setState(() => _imageChunkEvent = event); + } + + void handleImageFrame(ImageInfo info, bool synchronousCall) { + final setupCB = () { + _imageSize = Size( + info.image.width.toDouble(), + info.image.height.toDouble(), + ); + _loading = false; + _imageInfo = _imageInfo; + + _imageChunkEvent = null; + _lastException = null; + _stackTrace = null; + }; + synchronousCall ? setupCB() : setState(setupCB); + } + + void handleError(dynamic error, StackTrace stackTrace) { + setState(() { + _loading = false; + _lastException = error; + _stackTrace = stackTrace; + }); + } + + _imageStreamListener = ImageStreamListener( + handleImageFrame, + onChunk: handleImageChunk, + onError: handleError, + ); + _stream.addListener(_imageStreamListener); + } + + void _stopImageStream() { + if (_stream != null) { + _stream.removeListener(_imageStreamListener); + } + } + + @override + void initState() { + super.initState(); + _getImage(); + } + + @override + void dispose() { + super.dispose(); + _stopImageStream(); + } + + @override + Widget build(BuildContext context) { + if (_loading) { + return _buildLoading(context); + } + + if (_lastException != null) { + return _buildError(context); + } + + final scaleBoundaries = ScaleBoundaries( + widget.minScale ?? 0.0, + widget.maxScale ?? double.infinity, + widget.initialScale ?? PhotoViewComputedScale.contained, + widget.outerSize, + _imageSize, + ); + + return PhotoViewCore( + imageProvider: widget.imageProvider, + backgroundDecoration: widget.backgroundDecoration, + gaplessPlayback: widget.gaplessPlayback, + enableRotation: widget.enableRotation, + heroAttributes: widget.heroAttributes, + basePosition: widget.basePosition ?? Alignment.center, + controller: widget.controller, + scaleStateController: widget.scaleStateController, + scaleStateCycle: widget.scaleStateCycle ?? defaultScaleStateCycle, + scaleBoundaries: scaleBoundaries, + onTapUp: widget.onTapUp, + onTapDown: widget.onTapDown, + gestureDetectorBehavior: widget.gestureDetectorBehavior, + tightMode: widget.tightMode ?? false, + filterQuality: widget.filterQuality ?? FilterQuality.none, + disableGestures: widget.disableGestures ?? false, + ); + } + + Widget _buildLoading(BuildContext context) { + if (widget.loadingBuilder != null) { + return widget.loadingBuilder(context, _imageChunkEvent); + } + + return PhotoViewDefaultLoading( + event: _imageChunkEvent, + ); + } + + Widget _buildError( + BuildContext context, + ) { + if (widget.loadFailedChild != null) { + return widget.loadFailedChild; + } + if (widget.errorBuilder != null) { + return widget.errorBuilder(context, _lastException, _stackTrace); + } + return PhotoViewDefaultError(); + } +} + +class CustomChildWrapper extends StatefulWidget { + const CustomChildWrapper({ + Key key, + this.child, + this.childSize, + this.backgroundDecoration, + this.heroAttributes, + this.scaleStateChangedCallback, + this.enableRotation, + this.controller, + this.scaleStateController, + this.maxScale, + this.minScale, + this.initialScale, + this.basePosition, + this.scaleStateCycle, + this.onTapUp, + this.onTapDown, + this.outerSize, + this.gestureDetectorBehavior, + this.tightMode, + this.filterQuality, + this.disableGestures, + }) : super(key: key); + + final Widget child; + final Size childSize; + final Decoration backgroundDecoration; + final PhotoViewHeroAttributes heroAttributes; + final ValueChanged scaleStateChangedCallback; + final bool enableRotation; + + final PhotoViewControllerBase controller; + final PhotoViewScaleStateController scaleStateController; + + final dynamic maxScale; + final dynamic minScale; + final dynamic initialScale; + + final Alignment basePosition; + final ScaleStateCycle scaleStateCycle; + final PhotoViewImageTapUpCallback onTapUp; + final PhotoViewImageTapDownCallback onTapDown; + final Size outerSize; + final HitTestBehavior gestureDetectorBehavior; + final bool tightMode; + final FilterQuality filterQuality; + final bool disableGestures; + + @override + _CustomChildWrapperState createState() => _CustomChildWrapperState(); +} + +class _CustomChildWrapperState extends State { + @override + Widget build(BuildContext context) { + final scaleBoundaries = ScaleBoundaries( + widget.minScale ?? 0.0, + widget.maxScale ?? double.infinity, + widget.initialScale ?? PhotoViewComputedScale.contained, + widget.outerSize, + widget.childSize ?? widget.outerSize, + ); + + return PhotoViewCore.customChild( + customChild: widget.child, + backgroundDecoration: widget.backgroundDecoration, + enableRotation: widget.enableRotation, + heroAttributes: widget.heroAttributes, + controller: widget.controller, + scaleStateController: widget.scaleStateController, + scaleStateCycle: widget.scaleStateCycle ?? defaultScaleStateCycle, + basePosition: widget.basePosition ?? Alignment.center, + scaleBoundaries: scaleBoundaries, + onTapUp: widget.onTapUp, + onTapDown: widget.onTapDown, + gestureDetectorBehavior: widget.gestureDetectorBehavior, + tightMode: widget.tightMode ?? false, + filterQuality: widget.filterQuality ?? FilterQuality.none, + disableGestures: widget.disableGestures ?? false, + ); + } +}